Completed
Push — master ( 000f41...05a6b7 )
by Joas
104:04 queued 85:15
created
lib/private/Files/Storage/DAV.php 2 patches
Indentation   +796 added lines, -796 removed lines patch added patch discarded remove patch
@@ -57,801 +57,801 @@
 block discarded – undo
57 57
  * @package OC\Files\Storage
58 58
  */
59 59
 class DAV extends Common {
60
-	/** @var string */
61
-	protected $password;
62
-	/** @var string */
63
-	protected $user;
64
-	/** @var string */
65
-	protected $authType;
66
-	/** @var string */
67
-	protected $host;
68
-	/** @var bool */
69
-	protected $secure;
70
-	/** @var string */
71
-	protected $root;
72
-	/** @var string */
73
-	protected $certPath;
74
-	/** @var bool */
75
-	protected $ready;
76
-	/** @var Client */
77
-	protected $client;
78
-	/** @var ArrayCache */
79
-	protected $statCache;
80
-	/** @var \OCP\Http\Client\IClientService */
81
-	protected $httpClientService;
82
-
83
-	/**
84
-	 * @param array $params
85
-	 * @throws \Exception
86
-	 */
87
-	public function __construct($params) {
88
-		$this->statCache = new ArrayCache();
89
-		$this->httpClientService = \OC::$server->getHTTPClientService();
90
-		if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
91
-			$host = $params['host'];
92
-			//remove leading http[s], will be generated in createBaseUri()
93
-			if (substr($host, 0, 8) == "https://") $host = substr($host, 8);
94
-			else if (substr($host, 0, 7) == "http://") $host = substr($host, 7);
95
-			$this->host = $host;
96
-			$this->user = $params['user'];
97
-			$this->password = $params['password'];
98
-			if (isset($params['authType'])) {
99
-				$this->authType = $params['authType'];
100
-			}
101
-			if (isset($params['secure'])) {
102
-				if (is_string($params['secure'])) {
103
-					$this->secure = ($params['secure'] === 'true');
104
-				} else {
105
-					$this->secure = (bool)$params['secure'];
106
-				}
107
-			} else {
108
-				$this->secure = false;
109
-			}
110
-			if ($this->secure === true) {
111
-				// inject mock for testing
112
-				$certManager = \OC::$server->getCertificateManager();
113
-				if (is_null($certManager)) { //no user
114
-					$certManager = \OC::$server->getCertificateManager(null);
115
-				}
116
-				$certPath = $certManager->getAbsoluteBundlePath();
117
-				if (file_exists($certPath)) {
118
-					$this->certPath = $certPath;
119
-				}
120
-			}
121
-			$this->root = $params['root'] ?? '/';
122
-			$this->root = '/' . ltrim($this->root, '/');
123
-			$this->root = rtrim($this->root, '/') . '/';
124
-		} else {
125
-			throw new \Exception('Invalid webdav storage configuration');
126
-		}
127
-	}
128
-
129
-	protected function init() {
130
-		if ($this->ready) {
131
-			return;
132
-		}
133
-		$this->ready = true;
134
-
135
-		$settings = [
136
-			'baseUri' => $this->createBaseUri(),
137
-			'userName' => $this->user,
138
-			'password' => $this->password,
139
-		];
140
-		if (isset($this->authType)) {
141
-			$settings['authType'] = $this->authType;
142
-		}
143
-
144
-		$proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
145
-		if ($proxy !== '') {
146
-			$settings['proxy'] = $proxy;
147
-		}
148
-
149
-		$this->client = new Client($settings);
150
-		$this->client->setThrowExceptions(true);
151
-		if ($this->secure === true && $this->certPath) {
152
-			$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * Clear the stat cache
158
-	 */
159
-	public function clearStatCache() {
160
-		$this->statCache->clear();
161
-	}
162
-
163
-	/** {@inheritdoc} */
164
-	public function getId() {
165
-		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
166
-	}
167
-
168
-	/** {@inheritdoc} */
169
-	public function createBaseUri() {
170
-		$baseUri = 'http';
171
-		if ($this->secure) {
172
-			$baseUri .= 's';
173
-		}
174
-		$baseUri .= '://' . $this->host . $this->root;
175
-		return $baseUri;
176
-	}
177
-
178
-	/** {@inheritdoc} */
179
-	public function mkdir($path) {
180
-		$this->init();
181
-		$path = $this->cleanPath($path);
182
-		$result = $this->simpleResponse('MKCOL', $path, null, 201);
183
-		if ($result) {
184
-			$this->statCache->set($path, true);
185
-		}
186
-		return $result;
187
-	}
188
-
189
-	/** {@inheritdoc} */
190
-	public function rmdir($path) {
191
-		$this->init();
192
-		$path = $this->cleanPath($path);
193
-		// FIXME: some WebDAV impl return 403 when trying to DELETE
194
-		// a non-empty folder
195
-		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
196
-		$this->statCache->clear($path . '/');
197
-		$this->statCache->remove($path);
198
-		return $result;
199
-	}
200
-
201
-	/** {@inheritdoc} */
202
-	public function opendir($path) {
203
-		$this->init();
204
-		$path = $this->cleanPath($path);
205
-		try {
206
-			$response = $this->client->propFind(
207
-				$this->encodePath($path),
208
-				['{DAV:}href'],
209
-				1
210
-			);
211
-			if ($response === false) {
212
-				return false;
213
-			}
214
-			$content = [];
215
-			$files = array_keys($response);
216
-			array_shift($files); //the first entry is the current directory
217
-
218
-			if (!$this->statCache->hasKey($path)) {
219
-				$this->statCache->set($path, true);
220
-			}
221
-			foreach ($files as $file) {
222
-				$file = urldecode($file);
223
-				// do not store the real entry, we might not have all properties
224
-				if (!$this->statCache->hasKey($path)) {
225
-					$this->statCache->set($file, true);
226
-				}
227
-				$file = basename($file);
228
-				$content[] = $file;
229
-			}
230
-			return IteratorDirectory::wrap($content);
231
-		} catch (\Exception $e) {
232
-			$this->convertException($e, $path);
233
-		}
234
-		return false;
235
-	}
236
-
237
-	/**
238
-	 * Propfind call with cache handling.
239
-	 *
240
-	 * First checks if information is cached.
241
-	 * If not, request it from the server then store to cache.
242
-	 *
243
-	 * @param string $path path to propfind
244
-	 *
245
-	 * @return array|boolean propfind response or false if the entry was not found
246
-	 *
247
-	 * @throws ClientHttpException
248
-	 */
249
-	protected function propfind($path) {
250
-		$path = $this->cleanPath($path);
251
-		$cachedResponse = $this->statCache->get($path);
252
-		// we either don't know it, or we know it exists but need more details
253
-		if (is_null($cachedResponse) || $cachedResponse === true) {
254
-			$this->init();
255
-			try {
256
-				$response = $this->client->propFind(
257
-					$this->encodePath($path),
258
-					array(
259
-						'{DAV:}getlastmodified',
260
-						'{DAV:}getcontentlength',
261
-						'{DAV:}getcontenttype',
262
-						'{http://owncloud.org/ns}permissions',
263
-						'{http://open-collaboration-services.org/ns}share-permissions',
264
-						'{DAV:}resourcetype',
265
-						'{DAV:}getetag',
266
-					)
267
-				);
268
-				$this->statCache->set($path, $response);
269
-			} catch (ClientHttpException $e) {
270
-				if ($e->getHttpStatus() === 404) {
271
-					$this->statCache->clear($path . '/');
272
-					$this->statCache->set($path, false);
273
-					return false;
274
-				}
275
-				$this->convertException($e, $path);
276
-			} catch (\Exception $e) {
277
-				$this->convertException($e, $path);
278
-			}
279
-		} else {
280
-			$response = $cachedResponse;
281
-		}
282
-		return $response;
283
-	}
284
-
285
-	/** {@inheritdoc} */
286
-	public function filetype($path) {
287
-		try {
288
-			$response = $this->propfind($path);
289
-			if ($response === false) {
290
-				return false;
291
-			}
292
-			$responseType = [];
293
-			if (isset($response["{DAV:}resourcetype"])) {
294
-				/** @var ResourceType[] $response */
295
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
296
-			}
297
-			return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
298
-		} catch (\Exception $e) {
299
-			$this->convertException($e, $path);
300
-		}
301
-		return false;
302
-	}
303
-
304
-	/** {@inheritdoc} */
305
-	public function file_exists($path) {
306
-		try {
307
-			$path = $this->cleanPath($path);
308
-			$cachedState = $this->statCache->get($path);
309
-			if ($cachedState === false) {
310
-				// we know the file doesn't exist
311
-				return false;
312
-			} else if (!is_null($cachedState)) {
313
-				return true;
314
-			}
315
-			// need to get from server
316
-			return ($this->propfind($path) !== false);
317
-		} catch (\Exception $e) {
318
-			$this->convertException($e, $path);
319
-		}
320
-		return false;
321
-	}
322
-
323
-	/** {@inheritdoc} */
324
-	public function unlink($path) {
325
-		$this->init();
326
-		$path = $this->cleanPath($path);
327
-		$result = $this->simpleResponse('DELETE', $path, null, 204);
328
-		$this->statCache->clear($path . '/');
329
-		$this->statCache->remove($path);
330
-		return $result;
331
-	}
332
-
333
-	/** {@inheritdoc} */
334
-	public function fopen($path, $mode) {
335
-		$this->init();
336
-		$path = $this->cleanPath($path);
337
-		switch ($mode) {
338
-			case 'r':
339
-			case 'rb':
340
-				try {
341
-					$response = $this->httpClientService
342
-						->newClient()
343
-						->get($this->createBaseUri() . $this->encodePath($path), [
344
-							'auth' => [$this->user, $this->password],
345
-							'stream' => true
346
-						]);
347
-				} catch (\GuzzleHttp\Exception\ClientException $e) {
348
-					if ($e->getResponse() instanceof ResponseInterface
349
-						&& $e->getResponse()->getStatusCode() === 404) {
350
-						return false;
351
-					} else {
352
-						throw $e;
353
-					}
354
-				}
355
-
356
-				if ($response->getStatusCode() !== Http::STATUS_OK) {
357
-					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
358
-						throw new \OCP\Lock\LockedException($path);
359
-					} else {
360
-						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
361
-					}
362
-				}
363
-
364
-				return $response->getBody();
365
-			case 'w':
366
-			case 'wb':
367
-			case 'a':
368
-			case 'ab':
369
-			case 'r+':
370
-			case 'w+':
371
-			case 'wb+':
372
-			case 'a+':
373
-			case 'x':
374
-			case 'x+':
375
-			case 'c':
376
-			case 'c+':
377
-				//emulate these
378
-				$tempManager = \OC::$server->getTempManager();
379
-				if (strrpos($path, '.') !== false) {
380
-					$ext = substr($path, strrpos($path, '.'));
381
-				} else {
382
-					$ext = '';
383
-				}
384
-				if ($this->file_exists($path)) {
385
-					if (!$this->isUpdatable($path)) {
386
-						return false;
387
-					}
388
-					if ($mode === 'w' or $mode === 'w+') {
389
-						$tmpFile = $tempManager->getTemporaryFile($ext);
390
-					} else {
391
-						$tmpFile = $this->getCachedFile($path);
392
-					}
393
-				} else {
394
-					if (!$this->isCreatable(dirname($path))) {
395
-						return false;
396
-					}
397
-					$tmpFile = $tempManager->getTemporaryFile($ext);
398
-				}
399
-				$handle = fopen($tmpFile, $mode);
400
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
401
-					$this->writeBack($tmpFile, $path);
402
-				});
403
-		}
404
-	}
405
-
406
-	/**
407
-	 * @param string $tmpFile
408
-	 */
409
-	public function writeBack($tmpFile, $path) {
410
-		$this->uploadFile($tmpFile, $path);
411
-		unlink($tmpFile);
412
-	}
413
-
414
-	/** {@inheritdoc} */
415
-	public function free_space($path) {
416
-		$this->init();
417
-		$path = $this->cleanPath($path);
418
-		try {
419
-			// TODO: cacheable ?
420
-			$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
421
-			if ($response === false) {
422
-				return FileInfo::SPACE_UNKNOWN;
423
-			}
424
-			if (isset($response['{DAV:}quota-available-bytes'])) {
425
-				return (int)$response['{DAV:}quota-available-bytes'];
426
-			} else {
427
-				return FileInfo::SPACE_UNKNOWN;
428
-			}
429
-		} catch (\Exception $e) {
430
-			return FileInfo::SPACE_UNKNOWN;
431
-		}
432
-	}
433
-
434
-	/** {@inheritdoc} */
435
-	public function touch($path, $mtime = null) {
436
-		$this->init();
437
-		if (is_null($mtime)) {
438
-			$mtime = time();
439
-		}
440
-		$path = $this->cleanPath($path);
441
-
442
-		// if file exists, update the mtime, else create a new empty file
443
-		if ($this->file_exists($path)) {
444
-			try {
445
-				$this->statCache->remove($path);
446
-				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
447
-				// non-owncloud clients might not have accepted the property, need to recheck it
448
-				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
449
-				if ($response === false) {
450
-					return false;
451
-				}
452
-				if (isset($response['{DAV:}getlastmodified'])) {
453
-					$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
454
-					if ($remoteMtime !== $mtime) {
455
-						// server has not accepted the mtime
456
-						return false;
457
-					}
458
-				}
459
-			} catch (ClientHttpException $e) {
460
-				if ($e->getHttpStatus() === 501) {
461
-					return false;
462
-				}
463
-				$this->convertException($e, $path);
464
-				return false;
465
-			} catch (\Exception $e) {
466
-				$this->convertException($e, $path);
467
-				return false;
468
-			}
469
-		} else {
470
-			$this->file_put_contents($path, '');
471
-		}
472
-		return true;
473
-	}
474
-
475
-	/**
476
-	 * @param string $path
477
-	 * @param string $data
478
-	 * @return int
479
-	 */
480
-	public function file_put_contents($path, $data) {
481
-		$path = $this->cleanPath($path);
482
-		$result = parent::file_put_contents($path, $data);
483
-		$this->statCache->remove($path);
484
-		return $result;
485
-	}
486
-
487
-	/**
488
-	 * @param string $path
489
-	 * @param string $target
490
-	 */
491
-	protected function uploadFile($path, $target) {
492
-		$this->init();
493
-
494
-		// invalidate
495
-		$target = $this->cleanPath($target);
496
-		$this->statCache->remove($target);
497
-		$source = fopen($path, 'r');
498
-
499
-		$this->httpClientService
500
-			->newClient()
501
-			->put($this->createBaseUri() . $this->encodePath($target), [
502
-				'body' => $source,
503
-				'auth' => [$this->user, $this->password]
504
-			]);
505
-
506
-		$this->removeCachedFile($target);
507
-	}
508
-
509
-	/** {@inheritdoc} */
510
-	public function rename($path1, $path2) {
511
-		$this->init();
512
-		$path1 = $this->cleanPath($path1);
513
-		$path2 = $this->cleanPath($path2);
514
-		try {
515
-			// overwrite directory ?
516
-			if ($this->is_dir($path2)) {
517
-				// needs trailing slash in destination
518
-				$path2 = rtrim($path2, '/') . '/';
519
-			}
520
-			$this->client->request(
521
-				'MOVE',
522
-				$this->encodePath($path1),
523
-				null,
524
-				[
525
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
526
-				]
527
-			);
528
-			$this->statCache->clear($path1 . '/');
529
-			$this->statCache->clear($path2 . '/');
530
-			$this->statCache->set($path1, false);
531
-			$this->statCache->set($path2, true);
532
-			$this->removeCachedFile($path1);
533
-			$this->removeCachedFile($path2);
534
-			return true;
535
-		} catch (\Exception $e) {
536
-			$this->convertException($e);
537
-		}
538
-		return false;
539
-	}
540
-
541
-	/** {@inheritdoc} */
542
-	public function copy($path1, $path2) {
543
-		$this->init();
544
-		$path1 = $this->cleanPath($path1);
545
-		$path2 = $this->cleanPath($path2);
546
-		try {
547
-			// overwrite directory ?
548
-			if ($this->is_dir($path2)) {
549
-				// needs trailing slash in destination
550
-				$path2 = rtrim($path2, '/') . '/';
551
-			}
552
-			$this->client->request(
553
-				'COPY',
554
-				$this->encodePath($path1),
555
-				null,
556
-				[
557
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
558
-				]
559
-			);
560
-			$this->statCache->clear($path2 . '/');
561
-			$this->statCache->set($path2, true);
562
-			$this->removeCachedFile($path2);
563
-			return true;
564
-		} catch (\Exception $e) {
565
-			$this->convertException($e);
566
-		}
567
-		return false;
568
-	}
569
-
570
-	/** {@inheritdoc} */
571
-	public function stat($path) {
572
-		try {
573
-			$response = $this->propfind($path);
574
-			if (!$response) {
575
-				return false;
576
-			}
577
-			return [
578
-				'mtime' => strtotime($response['{DAV:}getlastmodified']),
579
-				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
580
-			];
581
-		} catch (\Exception $e) {
582
-			$this->convertException($e, $path);
583
-		}
584
-		return array();
585
-	}
586
-
587
-	/** {@inheritdoc} */
588
-	public function getMimeType($path) {
589
-		$remoteMimetype = $this->getMimeTypeFromRemote($path);
590
-		if ($remoteMimetype === 'application/octet-stream') {
591
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
592
-		} else {
593
-			return $remoteMimetype;
594
-		}
595
-	}
596
-
597
-	public function getMimeTypeFromRemote($path) {
598
-		try {
599
-			$response = $this->propfind($path);
600
-			if ($response === false) {
601
-				return false;
602
-			}
603
-			$responseType = [];
604
-			if (isset($response["{DAV:}resourcetype"])) {
605
-				/** @var ResourceType[] $response */
606
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
607
-			}
608
-			$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
609
-			if ($type == 'dir') {
610
-				return 'httpd/unix-directory';
611
-			} elseif (isset($response['{DAV:}getcontenttype'])) {
612
-				return $response['{DAV:}getcontenttype'];
613
-			} else {
614
-				return 'application/octet-stream';
615
-			}
616
-		} catch (\Exception $e) {
617
-			return false;
618
-		}
619
-	}
620
-
621
-	/**
622
-	 * @param string $path
623
-	 * @return string
624
-	 */
625
-	public function cleanPath($path) {
626
-		if ($path === '') {
627
-			return $path;
628
-		}
629
-		$path = Filesystem::normalizePath($path);
630
-		// remove leading slash
631
-		return substr($path, 1);
632
-	}
633
-
634
-	/**
635
-	 * URL encodes the given path but keeps the slashes
636
-	 *
637
-	 * @param string $path to encode
638
-	 * @return string encoded path
639
-	 */
640
-	protected function encodePath($path) {
641
-		// slashes need to stay
642
-		return str_replace('%2F', '/', rawurlencode($path));
643
-	}
644
-
645
-	/**
646
-	 * @param string $method
647
-	 * @param string $path
648
-	 * @param string|resource|null $body
649
-	 * @param int $expected
650
-	 * @return bool
651
-	 * @throws StorageInvalidException
652
-	 * @throws StorageNotAvailableException
653
-	 */
654
-	protected function simpleResponse($method, $path, $body, $expected) {
655
-		$path = $this->cleanPath($path);
656
-		try {
657
-			$response = $this->client->request($method, $this->encodePath($path), $body);
658
-			return $response['statusCode'] == $expected;
659
-		} catch (ClientHttpException $e) {
660
-			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
661
-				$this->statCache->clear($path . '/');
662
-				$this->statCache->set($path, false);
663
-				return false;
664
-			}
665
-
666
-			$this->convertException($e, $path);
667
-		} catch (\Exception $e) {
668
-			$this->convertException($e, $path);
669
-		}
670
-		return false;
671
-	}
672
-
673
-	/**
674
-	 * check if curl is installed
675
-	 */
676
-	public static function checkDependencies() {
677
-		return true;
678
-	}
679
-
680
-	/** {@inheritdoc} */
681
-	public function isUpdatable($path) {
682
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
683
-	}
684
-
685
-	/** {@inheritdoc} */
686
-	public function isCreatable($path) {
687
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
688
-	}
689
-
690
-	/** {@inheritdoc} */
691
-	public function isSharable($path) {
692
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
693
-	}
694
-
695
-	/** {@inheritdoc} */
696
-	public function isDeletable($path) {
697
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
698
-	}
699
-
700
-	/** {@inheritdoc} */
701
-	public function getPermissions($path) {
702
-		$this->init();
703
-		$path = $this->cleanPath($path);
704
-		$response = $this->propfind($path);
705
-		if ($response === false) {
706
-			return 0;
707
-		}
708
-		if (isset($response['{http://owncloud.org/ns}permissions'])) {
709
-			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
710
-		} else if ($this->is_dir($path)) {
711
-			return Constants::PERMISSION_ALL;
712
-		} else if ($this->file_exists($path)) {
713
-			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
714
-		} else {
715
-			return 0;
716
-		}
717
-	}
718
-
719
-	/** {@inheritdoc} */
720
-	public function getETag($path) {
721
-		$this->init();
722
-		$path = $this->cleanPath($path);
723
-		$response = $this->propfind($path);
724
-		if ($response === false) {
725
-			return null;
726
-		}
727
-		if (isset($response['{DAV:}getetag'])) {
728
-			return trim($response['{DAV:}getetag'], '"');
729
-		}
730
-		return parent::getEtag($path);
731
-	}
732
-
733
-	/**
734
-	 * @param string $permissionsString
735
-	 * @return int
736
-	 */
737
-	protected function parsePermissions($permissionsString) {
738
-		$permissions = Constants::PERMISSION_READ;
739
-		if (strpos($permissionsString, 'R') !== false) {
740
-			$permissions |= Constants::PERMISSION_SHARE;
741
-		}
742
-		if (strpos($permissionsString, 'D') !== false) {
743
-			$permissions |= Constants::PERMISSION_DELETE;
744
-		}
745
-		if (strpos($permissionsString, 'W') !== false) {
746
-			$permissions |= Constants::PERMISSION_UPDATE;
747
-		}
748
-		if (strpos($permissionsString, 'CK') !== false) {
749
-			$permissions |= Constants::PERMISSION_CREATE;
750
-			$permissions |= Constants::PERMISSION_UPDATE;
751
-		}
752
-		return $permissions;
753
-	}
754
-
755
-	/**
756
-	 * check if a file or folder has been updated since $time
757
-	 *
758
-	 * @param string $path
759
-	 * @param int $time
760
-	 * @throws \OCP\Files\StorageNotAvailableException
761
-	 * @return bool
762
-	 */
763
-	public function hasUpdated($path, $time) {
764
-		$this->init();
765
-		$path = $this->cleanPath($path);
766
-		try {
767
-			// force refresh for $path
768
-			$this->statCache->remove($path);
769
-			$response = $this->propfind($path);
770
-			if ($response === false) {
771
-				if ($path === '') {
772
-					// if root is gone it means the storage is not available
773
-					throw new StorageNotAvailableException('root is gone');
774
-				}
775
-				return false;
776
-			}
777
-			if (isset($response['{DAV:}getetag'])) {
778
-				$cachedData = $this->getCache()->get($path);
779
-				$etag = null;
780
-				if (isset($response['{DAV:}getetag'])) {
781
-					$etag = trim($response['{DAV:}getetag'], '"');
782
-				}
783
-				if (!empty($etag) && $cachedData['etag'] !== $etag) {
784
-					return true;
785
-				} else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
786
-					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
787
-					return $sharePermissions !== $cachedData['permissions'];
788
-				} else if (isset($response['{http://owncloud.org/ns}permissions'])) {
789
-					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
790
-					return $permissions !== $cachedData['permissions'];
791
-				} else {
792
-					return false;
793
-				}
794
-			} else {
795
-				$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
796
-				return $remoteMtime > $time;
797
-			}
798
-		} catch (ClientHttpException $e) {
799
-			if ($e->getHttpStatus() === 405) {
800
-				if ($path === '') {
801
-					// if root is gone it means the storage is not available
802
-					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
803
-				}
804
-				return false;
805
-			}
806
-			$this->convertException($e, $path);
807
-			return false;
808
-		} catch (\Exception $e) {
809
-			$this->convertException($e, $path);
810
-			return false;
811
-		}
812
-	}
813
-
814
-	/**
815
-	 * Interpret the given exception and decide whether it is due to an
816
-	 * unavailable storage, invalid storage or other.
817
-	 * This will either throw StorageInvalidException, StorageNotAvailableException
818
-	 * or do nothing.
819
-	 *
820
-	 * @param Exception $e sabre exception
821
-	 * @param string $path optional path from the operation
822
-	 *
823
-	 * @throws StorageInvalidException if the storage is invalid, for example
824
-	 * when the authentication expired or is invalid
825
-	 * @throws StorageNotAvailableException if the storage is not available,
826
-	 * which might be temporary
827
-	 */
828
-	protected function convertException(Exception $e, $path = '') {
829
-		\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
830
-		if ($e instanceof ClientHttpException) {
831
-			if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
832
-				throw new \OCP\Lock\LockedException($path);
833
-			}
834
-			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
835
-				// either password was changed or was invalid all along
836
-				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
837
-			} else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
838
-				// ignore exception for MethodNotAllowed, false will be returned
839
-				return;
840
-			}
841
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
842
-		} else if ($e instanceof ClientException) {
843
-			// connection timeout or refused, server could be temporarily down
844
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
845
-		} else if ($e instanceof \InvalidArgumentException) {
846
-			// parse error because the server returned HTML instead of XML,
847
-			// possibly temporarily down
848
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
849
-		} else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
850
-			// rethrow
851
-			throw $e;
852
-		}
853
-
854
-		// TODO: only log for now, but in the future need to wrap/rethrow exception
855
-	}
60
+    /** @var string */
61
+    protected $password;
62
+    /** @var string */
63
+    protected $user;
64
+    /** @var string */
65
+    protected $authType;
66
+    /** @var string */
67
+    protected $host;
68
+    /** @var bool */
69
+    protected $secure;
70
+    /** @var string */
71
+    protected $root;
72
+    /** @var string */
73
+    protected $certPath;
74
+    /** @var bool */
75
+    protected $ready;
76
+    /** @var Client */
77
+    protected $client;
78
+    /** @var ArrayCache */
79
+    protected $statCache;
80
+    /** @var \OCP\Http\Client\IClientService */
81
+    protected $httpClientService;
82
+
83
+    /**
84
+     * @param array $params
85
+     * @throws \Exception
86
+     */
87
+    public function __construct($params) {
88
+        $this->statCache = new ArrayCache();
89
+        $this->httpClientService = \OC::$server->getHTTPClientService();
90
+        if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
91
+            $host = $params['host'];
92
+            //remove leading http[s], will be generated in createBaseUri()
93
+            if (substr($host, 0, 8) == "https://") $host = substr($host, 8);
94
+            else if (substr($host, 0, 7) == "http://") $host = substr($host, 7);
95
+            $this->host = $host;
96
+            $this->user = $params['user'];
97
+            $this->password = $params['password'];
98
+            if (isset($params['authType'])) {
99
+                $this->authType = $params['authType'];
100
+            }
101
+            if (isset($params['secure'])) {
102
+                if (is_string($params['secure'])) {
103
+                    $this->secure = ($params['secure'] === 'true');
104
+                } else {
105
+                    $this->secure = (bool)$params['secure'];
106
+                }
107
+            } else {
108
+                $this->secure = false;
109
+            }
110
+            if ($this->secure === true) {
111
+                // inject mock for testing
112
+                $certManager = \OC::$server->getCertificateManager();
113
+                if (is_null($certManager)) { //no user
114
+                    $certManager = \OC::$server->getCertificateManager(null);
115
+                }
116
+                $certPath = $certManager->getAbsoluteBundlePath();
117
+                if (file_exists($certPath)) {
118
+                    $this->certPath = $certPath;
119
+                }
120
+            }
121
+            $this->root = $params['root'] ?? '/';
122
+            $this->root = '/' . ltrim($this->root, '/');
123
+            $this->root = rtrim($this->root, '/') . '/';
124
+        } else {
125
+            throw new \Exception('Invalid webdav storage configuration');
126
+        }
127
+    }
128
+
129
+    protected function init() {
130
+        if ($this->ready) {
131
+            return;
132
+        }
133
+        $this->ready = true;
134
+
135
+        $settings = [
136
+            'baseUri' => $this->createBaseUri(),
137
+            'userName' => $this->user,
138
+            'password' => $this->password,
139
+        ];
140
+        if (isset($this->authType)) {
141
+            $settings['authType'] = $this->authType;
142
+        }
143
+
144
+        $proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
145
+        if ($proxy !== '') {
146
+            $settings['proxy'] = $proxy;
147
+        }
148
+
149
+        $this->client = new Client($settings);
150
+        $this->client->setThrowExceptions(true);
151
+        if ($this->secure === true && $this->certPath) {
152
+            $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
153
+        }
154
+    }
155
+
156
+    /**
157
+     * Clear the stat cache
158
+     */
159
+    public function clearStatCache() {
160
+        $this->statCache->clear();
161
+    }
162
+
163
+    /** {@inheritdoc} */
164
+    public function getId() {
165
+        return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
166
+    }
167
+
168
+    /** {@inheritdoc} */
169
+    public function createBaseUri() {
170
+        $baseUri = 'http';
171
+        if ($this->secure) {
172
+            $baseUri .= 's';
173
+        }
174
+        $baseUri .= '://' . $this->host . $this->root;
175
+        return $baseUri;
176
+    }
177
+
178
+    /** {@inheritdoc} */
179
+    public function mkdir($path) {
180
+        $this->init();
181
+        $path = $this->cleanPath($path);
182
+        $result = $this->simpleResponse('MKCOL', $path, null, 201);
183
+        if ($result) {
184
+            $this->statCache->set($path, true);
185
+        }
186
+        return $result;
187
+    }
188
+
189
+    /** {@inheritdoc} */
190
+    public function rmdir($path) {
191
+        $this->init();
192
+        $path = $this->cleanPath($path);
193
+        // FIXME: some WebDAV impl return 403 when trying to DELETE
194
+        // a non-empty folder
195
+        $result = $this->simpleResponse('DELETE', $path . '/', null, 204);
196
+        $this->statCache->clear($path . '/');
197
+        $this->statCache->remove($path);
198
+        return $result;
199
+    }
200
+
201
+    /** {@inheritdoc} */
202
+    public function opendir($path) {
203
+        $this->init();
204
+        $path = $this->cleanPath($path);
205
+        try {
206
+            $response = $this->client->propFind(
207
+                $this->encodePath($path),
208
+                ['{DAV:}href'],
209
+                1
210
+            );
211
+            if ($response === false) {
212
+                return false;
213
+            }
214
+            $content = [];
215
+            $files = array_keys($response);
216
+            array_shift($files); //the first entry is the current directory
217
+
218
+            if (!$this->statCache->hasKey($path)) {
219
+                $this->statCache->set($path, true);
220
+            }
221
+            foreach ($files as $file) {
222
+                $file = urldecode($file);
223
+                // do not store the real entry, we might not have all properties
224
+                if (!$this->statCache->hasKey($path)) {
225
+                    $this->statCache->set($file, true);
226
+                }
227
+                $file = basename($file);
228
+                $content[] = $file;
229
+            }
230
+            return IteratorDirectory::wrap($content);
231
+        } catch (\Exception $e) {
232
+            $this->convertException($e, $path);
233
+        }
234
+        return false;
235
+    }
236
+
237
+    /**
238
+     * Propfind call with cache handling.
239
+     *
240
+     * First checks if information is cached.
241
+     * If not, request it from the server then store to cache.
242
+     *
243
+     * @param string $path path to propfind
244
+     *
245
+     * @return array|boolean propfind response or false if the entry was not found
246
+     *
247
+     * @throws ClientHttpException
248
+     */
249
+    protected function propfind($path) {
250
+        $path = $this->cleanPath($path);
251
+        $cachedResponse = $this->statCache->get($path);
252
+        // we either don't know it, or we know it exists but need more details
253
+        if (is_null($cachedResponse) || $cachedResponse === true) {
254
+            $this->init();
255
+            try {
256
+                $response = $this->client->propFind(
257
+                    $this->encodePath($path),
258
+                    array(
259
+                        '{DAV:}getlastmodified',
260
+                        '{DAV:}getcontentlength',
261
+                        '{DAV:}getcontenttype',
262
+                        '{http://owncloud.org/ns}permissions',
263
+                        '{http://open-collaboration-services.org/ns}share-permissions',
264
+                        '{DAV:}resourcetype',
265
+                        '{DAV:}getetag',
266
+                    )
267
+                );
268
+                $this->statCache->set($path, $response);
269
+            } catch (ClientHttpException $e) {
270
+                if ($e->getHttpStatus() === 404) {
271
+                    $this->statCache->clear($path . '/');
272
+                    $this->statCache->set($path, false);
273
+                    return false;
274
+                }
275
+                $this->convertException($e, $path);
276
+            } catch (\Exception $e) {
277
+                $this->convertException($e, $path);
278
+            }
279
+        } else {
280
+            $response = $cachedResponse;
281
+        }
282
+        return $response;
283
+    }
284
+
285
+    /** {@inheritdoc} */
286
+    public function filetype($path) {
287
+        try {
288
+            $response = $this->propfind($path);
289
+            if ($response === false) {
290
+                return false;
291
+            }
292
+            $responseType = [];
293
+            if (isset($response["{DAV:}resourcetype"])) {
294
+                /** @var ResourceType[] $response */
295
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
296
+            }
297
+            return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
298
+        } catch (\Exception $e) {
299
+            $this->convertException($e, $path);
300
+        }
301
+        return false;
302
+    }
303
+
304
+    /** {@inheritdoc} */
305
+    public function file_exists($path) {
306
+        try {
307
+            $path = $this->cleanPath($path);
308
+            $cachedState = $this->statCache->get($path);
309
+            if ($cachedState === false) {
310
+                // we know the file doesn't exist
311
+                return false;
312
+            } else if (!is_null($cachedState)) {
313
+                return true;
314
+            }
315
+            // need to get from server
316
+            return ($this->propfind($path) !== false);
317
+        } catch (\Exception $e) {
318
+            $this->convertException($e, $path);
319
+        }
320
+        return false;
321
+    }
322
+
323
+    /** {@inheritdoc} */
324
+    public function unlink($path) {
325
+        $this->init();
326
+        $path = $this->cleanPath($path);
327
+        $result = $this->simpleResponse('DELETE', $path, null, 204);
328
+        $this->statCache->clear($path . '/');
329
+        $this->statCache->remove($path);
330
+        return $result;
331
+    }
332
+
333
+    /** {@inheritdoc} */
334
+    public function fopen($path, $mode) {
335
+        $this->init();
336
+        $path = $this->cleanPath($path);
337
+        switch ($mode) {
338
+            case 'r':
339
+            case 'rb':
340
+                try {
341
+                    $response = $this->httpClientService
342
+                        ->newClient()
343
+                        ->get($this->createBaseUri() . $this->encodePath($path), [
344
+                            'auth' => [$this->user, $this->password],
345
+                            'stream' => true
346
+                        ]);
347
+                } catch (\GuzzleHttp\Exception\ClientException $e) {
348
+                    if ($e->getResponse() instanceof ResponseInterface
349
+                        && $e->getResponse()->getStatusCode() === 404) {
350
+                        return false;
351
+                    } else {
352
+                        throw $e;
353
+                    }
354
+                }
355
+
356
+                if ($response->getStatusCode() !== Http::STATUS_OK) {
357
+                    if ($response->getStatusCode() === Http::STATUS_LOCKED) {
358
+                        throw new \OCP\Lock\LockedException($path);
359
+                    } else {
360
+                        Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
361
+                    }
362
+                }
363
+
364
+                return $response->getBody();
365
+            case 'w':
366
+            case 'wb':
367
+            case 'a':
368
+            case 'ab':
369
+            case 'r+':
370
+            case 'w+':
371
+            case 'wb+':
372
+            case 'a+':
373
+            case 'x':
374
+            case 'x+':
375
+            case 'c':
376
+            case 'c+':
377
+                //emulate these
378
+                $tempManager = \OC::$server->getTempManager();
379
+                if (strrpos($path, '.') !== false) {
380
+                    $ext = substr($path, strrpos($path, '.'));
381
+                } else {
382
+                    $ext = '';
383
+                }
384
+                if ($this->file_exists($path)) {
385
+                    if (!$this->isUpdatable($path)) {
386
+                        return false;
387
+                    }
388
+                    if ($mode === 'w' or $mode === 'w+') {
389
+                        $tmpFile = $tempManager->getTemporaryFile($ext);
390
+                    } else {
391
+                        $tmpFile = $this->getCachedFile($path);
392
+                    }
393
+                } else {
394
+                    if (!$this->isCreatable(dirname($path))) {
395
+                        return false;
396
+                    }
397
+                    $tmpFile = $tempManager->getTemporaryFile($ext);
398
+                }
399
+                $handle = fopen($tmpFile, $mode);
400
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
401
+                    $this->writeBack($tmpFile, $path);
402
+                });
403
+        }
404
+    }
405
+
406
+    /**
407
+     * @param string $tmpFile
408
+     */
409
+    public function writeBack($tmpFile, $path) {
410
+        $this->uploadFile($tmpFile, $path);
411
+        unlink($tmpFile);
412
+    }
413
+
414
+    /** {@inheritdoc} */
415
+    public function free_space($path) {
416
+        $this->init();
417
+        $path = $this->cleanPath($path);
418
+        try {
419
+            // TODO: cacheable ?
420
+            $response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
421
+            if ($response === false) {
422
+                return FileInfo::SPACE_UNKNOWN;
423
+            }
424
+            if (isset($response['{DAV:}quota-available-bytes'])) {
425
+                return (int)$response['{DAV:}quota-available-bytes'];
426
+            } else {
427
+                return FileInfo::SPACE_UNKNOWN;
428
+            }
429
+        } catch (\Exception $e) {
430
+            return FileInfo::SPACE_UNKNOWN;
431
+        }
432
+    }
433
+
434
+    /** {@inheritdoc} */
435
+    public function touch($path, $mtime = null) {
436
+        $this->init();
437
+        if (is_null($mtime)) {
438
+            $mtime = time();
439
+        }
440
+        $path = $this->cleanPath($path);
441
+
442
+        // if file exists, update the mtime, else create a new empty file
443
+        if ($this->file_exists($path)) {
444
+            try {
445
+                $this->statCache->remove($path);
446
+                $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
447
+                // non-owncloud clients might not have accepted the property, need to recheck it
448
+                $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
449
+                if ($response === false) {
450
+                    return false;
451
+                }
452
+                if (isset($response['{DAV:}getlastmodified'])) {
453
+                    $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
454
+                    if ($remoteMtime !== $mtime) {
455
+                        // server has not accepted the mtime
456
+                        return false;
457
+                    }
458
+                }
459
+            } catch (ClientHttpException $e) {
460
+                if ($e->getHttpStatus() === 501) {
461
+                    return false;
462
+                }
463
+                $this->convertException($e, $path);
464
+                return false;
465
+            } catch (\Exception $e) {
466
+                $this->convertException($e, $path);
467
+                return false;
468
+            }
469
+        } else {
470
+            $this->file_put_contents($path, '');
471
+        }
472
+        return true;
473
+    }
474
+
475
+    /**
476
+     * @param string $path
477
+     * @param string $data
478
+     * @return int
479
+     */
480
+    public function file_put_contents($path, $data) {
481
+        $path = $this->cleanPath($path);
482
+        $result = parent::file_put_contents($path, $data);
483
+        $this->statCache->remove($path);
484
+        return $result;
485
+    }
486
+
487
+    /**
488
+     * @param string $path
489
+     * @param string $target
490
+     */
491
+    protected function uploadFile($path, $target) {
492
+        $this->init();
493
+
494
+        // invalidate
495
+        $target = $this->cleanPath($target);
496
+        $this->statCache->remove($target);
497
+        $source = fopen($path, 'r');
498
+
499
+        $this->httpClientService
500
+            ->newClient()
501
+            ->put($this->createBaseUri() . $this->encodePath($target), [
502
+                'body' => $source,
503
+                'auth' => [$this->user, $this->password]
504
+            ]);
505
+
506
+        $this->removeCachedFile($target);
507
+    }
508
+
509
+    /** {@inheritdoc} */
510
+    public function rename($path1, $path2) {
511
+        $this->init();
512
+        $path1 = $this->cleanPath($path1);
513
+        $path2 = $this->cleanPath($path2);
514
+        try {
515
+            // overwrite directory ?
516
+            if ($this->is_dir($path2)) {
517
+                // needs trailing slash in destination
518
+                $path2 = rtrim($path2, '/') . '/';
519
+            }
520
+            $this->client->request(
521
+                'MOVE',
522
+                $this->encodePath($path1),
523
+                null,
524
+                [
525
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
526
+                ]
527
+            );
528
+            $this->statCache->clear($path1 . '/');
529
+            $this->statCache->clear($path2 . '/');
530
+            $this->statCache->set($path1, false);
531
+            $this->statCache->set($path2, true);
532
+            $this->removeCachedFile($path1);
533
+            $this->removeCachedFile($path2);
534
+            return true;
535
+        } catch (\Exception $e) {
536
+            $this->convertException($e);
537
+        }
538
+        return false;
539
+    }
540
+
541
+    /** {@inheritdoc} */
542
+    public function copy($path1, $path2) {
543
+        $this->init();
544
+        $path1 = $this->cleanPath($path1);
545
+        $path2 = $this->cleanPath($path2);
546
+        try {
547
+            // overwrite directory ?
548
+            if ($this->is_dir($path2)) {
549
+                // needs trailing slash in destination
550
+                $path2 = rtrim($path2, '/') . '/';
551
+            }
552
+            $this->client->request(
553
+                'COPY',
554
+                $this->encodePath($path1),
555
+                null,
556
+                [
557
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
558
+                ]
559
+            );
560
+            $this->statCache->clear($path2 . '/');
561
+            $this->statCache->set($path2, true);
562
+            $this->removeCachedFile($path2);
563
+            return true;
564
+        } catch (\Exception $e) {
565
+            $this->convertException($e);
566
+        }
567
+        return false;
568
+    }
569
+
570
+    /** {@inheritdoc} */
571
+    public function stat($path) {
572
+        try {
573
+            $response = $this->propfind($path);
574
+            if (!$response) {
575
+                return false;
576
+            }
577
+            return [
578
+                'mtime' => strtotime($response['{DAV:}getlastmodified']),
579
+                'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
580
+            ];
581
+        } catch (\Exception $e) {
582
+            $this->convertException($e, $path);
583
+        }
584
+        return array();
585
+    }
586
+
587
+    /** {@inheritdoc} */
588
+    public function getMimeType($path) {
589
+        $remoteMimetype = $this->getMimeTypeFromRemote($path);
590
+        if ($remoteMimetype === 'application/octet-stream') {
591
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
592
+        } else {
593
+            return $remoteMimetype;
594
+        }
595
+    }
596
+
597
+    public function getMimeTypeFromRemote($path) {
598
+        try {
599
+            $response = $this->propfind($path);
600
+            if ($response === false) {
601
+                return false;
602
+            }
603
+            $responseType = [];
604
+            if (isset($response["{DAV:}resourcetype"])) {
605
+                /** @var ResourceType[] $response */
606
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
607
+            }
608
+            $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
609
+            if ($type == 'dir') {
610
+                return 'httpd/unix-directory';
611
+            } elseif (isset($response['{DAV:}getcontenttype'])) {
612
+                return $response['{DAV:}getcontenttype'];
613
+            } else {
614
+                return 'application/octet-stream';
615
+            }
616
+        } catch (\Exception $e) {
617
+            return false;
618
+        }
619
+    }
620
+
621
+    /**
622
+     * @param string $path
623
+     * @return string
624
+     */
625
+    public function cleanPath($path) {
626
+        if ($path === '') {
627
+            return $path;
628
+        }
629
+        $path = Filesystem::normalizePath($path);
630
+        // remove leading slash
631
+        return substr($path, 1);
632
+    }
633
+
634
+    /**
635
+     * URL encodes the given path but keeps the slashes
636
+     *
637
+     * @param string $path to encode
638
+     * @return string encoded path
639
+     */
640
+    protected function encodePath($path) {
641
+        // slashes need to stay
642
+        return str_replace('%2F', '/', rawurlencode($path));
643
+    }
644
+
645
+    /**
646
+     * @param string $method
647
+     * @param string $path
648
+     * @param string|resource|null $body
649
+     * @param int $expected
650
+     * @return bool
651
+     * @throws StorageInvalidException
652
+     * @throws StorageNotAvailableException
653
+     */
654
+    protected function simpleResponse($method, $path, $body, $expected) {
655
+        $path = $this->cleanPath($path);
656
+        try {
657
+            $response = $this->client->request($method, $this->encodePath($path), $body);
658
+            return $response['statusCode'] == $expected;
659
+        } catch (ClientHttpException $e) {
660
+            if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
661
+                $this->statCache->clear($path . '/');
662
+                $this->statCache->set($path, false);
663
+                return false;
664
+            }
665
+
666
+            $this->convertException($e, $path);
667
+        } catch (\Exception $e) {
668
+            $this->convertException($e, $path);
669
+        }
670
+        return false;
671
+    }
672
+
673
+    /**
674
+     * check if curl is installed
675
+     */
676
+    public static function checkDependencies() {
677
+        return true;
678
+    }
679
+
680
+    /** {@inheritdoc} */
681
+    public function isUpdatable($path) {
682
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
683
+    }
684
+
685
+    /** {@inheritdoc} */
686
+    public function isCreatable($path) {
687
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
688
+    }
689
+
690
+    /** {@inheritdoc} */
691
+    public function isSharable($path) {
692
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
693
+    }
694
+
695
+    /** {@inheritdoc} */
696
+    public function isDeletable($path) {
697
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
698
+    }
699
+
700
+    /** {@inheritdoc} */
701
+    public function getPermissions($path) {
702
+        $this->init();
703
+        $path = $this->cleanPath($path);
704
+        $response = $this->propfind($path);
705
+        if ($response === false) {
706
+            return 0;
707
+        }
708
+        if (isset($response['{http://owncloud.org/ns}permissions'])) {
709
+            return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
710
+        } else if ($this->is_dir($path)) {
711
+            return Constants::PERMISSION_ALL;
712
+        } else if ($this->file_exists($path)) {
713
+            return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
714
+        } else {
715
+            return 0;
716
+        }
717
+    }
718
+
719
+    /** {@inheritdoc} */
720
+    public function getETag($path) {
721
+        $this->init();
722
+        $path = $this->cleanPath($path);
723
+        $response = $this->propfind($path);
724
+        if ($response === false) {
725
+            return null;
726
+        }
727
+        if (isset($response['{DAV:}getetag'])) {
728
+            return trim($response['{DAV:}getetag'], '"');
729
+        }
730
+        return parent::getEtag($path);
731
+    }
732
+
733
+    /**
734
+     * @param string $permissionsString
735
+     * @return int
736
+     */
737
+    protected function parsePermissions($permissionsString) {
738
+        $permissions = Constants::PERMISSION_READ;
739
+        if (strpos($permissionsString, 'R') !== false) {
740
+            $permissions |= Constants::PERMISSION_SHARE;
741
+        }
742
+        if (strpos($permissionsString, 'D') !== false) {
743
+            $permissions |= Constants::PERMISSION_DELETE;
744
+        }
745
+        if (strpos($permissionsString, 'W') !== false) {
746
+            $permissions |= Constants::PERMISSION_UPDATE;
747
+        }
748
+        if (strpos($permissionsString, 'CK') !== false) {
749
+            $permissions |= Constants::PERMISSION_CREATE;
750
+            $permissions |= Constants::PERMISSION_UPDATE;
751
+        }
752
+        return $permissions;
753
+    }
754
+
755
+    /**
756
+     * check if a file or folder has been updated since $time
757
+     *
758
+     * @param string $path
759
+     * @param int $time
760
+     * @throws \OCP\Files\StorageNotAvailableException
761
+     * @return bool
762
+     */
763
+    public function hasUpdated($path, $time) {
764
+        $this->init();
765
+        $path = $this->cleanPath($path);
766
+        try {
767
+            // force refresh for $path
768
+            $this->statCache->remove($path);
769
+            $response = $this->propfind($path);
770
+            if ($response === false) {
771
+                if ($path === '') {
772
+                    // if root is gone it means the storage is not available
773
+                    throw new StorageNotAvailableException('root is gone');
774
+                }
775
+                return false;
776
+            }
777
+            if (isset($response['{DAV:}getetag'])) {
778
+                $cachedData = $this->getCache()->get($path);
779
+                $etag = null;
780
+                if (isset($response['{DAV:}getetag'])) {
781
+                    $etag = trim($response['{DAV:}getetag'], '"');
782
+                }
783
+                if (!empty($etag) && $cachedData['etag'] !== $etag) {
784
+                    return true;
785
+                } else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
786
+                    $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
787
+                    return $sharePermissions !== $cachedData['permissions'];
788
+                } else if (isset($response['{http://owncloud.org/ns}permissions'])) {
789
+                    $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
790
+                    return $permissions !== $cachedData['permissions'];
791
+                } else {
792
+                    return false;
793
+                }
794
+            } else {
795
+                $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
796
+                return $remoteMtime > $time;
797
+            }
798
+        } catch (ClientHttpException $e) {
799
+            if ($e->getHttpStatus() === 405) {
800
+                if ($path === '') {
801
+                    // if root is gone it means the storage is not available
802
+                    throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
803
+                }
804
+                return false;
805
+            }
806
+            $this->convertException($e, $path);
807
+            return false;
808
+        } catch (\Exception $e) {
809
+            $this->convertException($e, $path);
810
+            return false;
811
+        }
812
+    }
813
+
814
+    /**
815
+     * Interpret the given exception and decide whether it is due to an
816
+     * unavailable storage, invalid storage or other.
817
+     * This will either throw StorageInvalidException, StorageNotAvailableException
818
+     * or do nothing.
819
+     *
820
+     * @param Exception $e sabre exception
821
+     * @param string $path optional path from the operation
822
+     *
823
+     * @throws StorageInvalidException if the storage is invalid, for example
824
+     * when the authentication expired or is invalid
825
+     * @throws StorageNotAvailableException if the storage is not available,
826
+     * which might be temporary
827
+     */
828
+    protected function convertException(Exception $e, $path = '') {
829
+        \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
830
+        if ($e instanceof ClientHttpException) {
831
+            if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
832
+                throw new \OCP\Lock\LockedException($path);
833
+            }
834
+            if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
835
+                // either password was changed or was invalid all along
836
+                throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
837
+            } else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
838
+                // ignore exception for MethodNotAllowed, false will be returned
839
+                return;
840
+            }
841
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
842
+        } else if ($e instanceof ClientException) {
843
+            // connection timeout or refused, server could be temporarily down
844
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
845
+        } else if ($e instanceof \InvalidArgumentException) {
846
+            // parse error because the server returned HTML instead of XML,
847
+            // possibly temporarily down
848
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
849
+        } else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
850
+            // rethrow
851
+            throw $e;
852
+        }
853
+
854
+        // TODO: only log for now, but in the future need to wrap/rethrow exception
855
+    }
856 856
 }
857 857
 
Please login to merge, or discard this patch.
Spacing   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -102,7 +102,7 @@  discard block
 block discarded – undo
102 102
 				if (is_string($params['secure'])) {
103 103
 					$this->secure = ($params['secure'] === 'true');
104 104
 				} else {
105
-					$this->secure = (bool)$params['secure'];
105
+					$this->secure = (bool) $params['secure'];
106 106
 				}
107 107
 			} else {
108 108
 				$this->secure = false;
@@ -119,8 +119,8 @@  discard block
 block discarded – undo
119 119
 				}
120 120
 			}
121 121
 			$this->root = $params['root'] ?? '/';
122
-			$this->root = '/' . ltrim($this->root, '/');
123
-			$this->root = rtrim($this->root, '/') . '/';
122
+			$this->root = '/'.ltrim($this->root, '/');
123
+			$this->root = rtrim($this->root, '/').'/';
124 124
 		} else {
125 125
 			throw new \Exception('Invalid webdav storage configuration');
126 126
 		}
@@ -162,7 +162,7 @@  discard block
 block discarded – undo
162 162
 
163 163
 	/** {@inheritdoc} */
164 164
 	public function getId() {
165
-		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
165
+		return 'webdav::'.$this->user.'@'.$this->host.'/'.$this->root;
166 166
 	}
167 167
 
168 168
 	/** {@inheritdoc} */
@@ -171,7 +171,7 @@  discard block
 block discarded – undo
171 171
 		if ($this->secure) {
172 172
 			$baseUri .= 's';
173 173
 		}
174
-		$baseUri .= '://' . $this->host . $this->root;
174
+		$baseUri .= '://'.$this->host.$this->root;
175 175
 		return $baseUri;
176 176
 	}
177 177
 
@@ -192,8 +192,8 @@  discard block
 block discarded – undo
192 192
 		$path = $this->cleanPath($path);
193 193
 		// FIXME: some WebDAV impl return 403 when trying to DELETE
194 194
 		// a non-empty folder
195
-		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
196
-		$this->statCache->clear($path . '/');
195
+		$result = $this->simpleResponse('DELETE', $path.'/', null, 204);
196
+		$this->statCache->clear($path.'/');
197 197
 		$this->statCache->remove($path);
198 198
 		return $result;
199 199
 	}
@@ -268,7 +268,7 @@  discard block
 block discarded – undo
268 268
 				$this->statCache->set($path, $response);
269 269
 			} catch (ClientHttpException $e) {
270 270
 				if ($e->getHttpStatus() === 404) {
271
-					$this->statCache->clear($path . '/');
271
+					$this->statCache->clear($path.'/');
272 272
 					$this->statCache->set($path, false);
273 273
 					return false;
274 274
 				}
@@ -325,7 +325,7 @@  discard block
 block discarded – undo
325 325
 		$this->init();
326 326
 		$path = $this->cleanPath($path);
327 327
 		$result = $this->simpleResponse('DELETE', $path, null, 204);
328
-		$this->statCache->clear($path . '/');
328
+		$this->statCache->clear($path.'/');
329 329
 		$this->statCache->remove($path);
330 330
 		return $result;
331 331
 	}
@@ -340,7 +340,7 @@  discard block
 block discarded – undo
340 340
 				try {
341 341
 					$response = $this->httpClientService
342 342
 						->newClient()
343
-						->get($this->createBaseUri() . $this->encodePath($path), [
343
+						->get($this->createBaseUri().$this->encodePath($path), [
344 344
 							'auth' => [$this->user, $this->password],
345 345
 							'stream' => true
346 346
 						]);
@@ -357,7 +357,7 @@  discard block
 block discarded – undo
357 357
 					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
358 358
 						throw new \OCP\Lock\LockedException($path);
359 359
 					} else {
360
-						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
360
+						Util::writeLog("webdav client", 'Guzzle get returned status code '.$response->getStatusCode(), Util::ERROR);
361 361
 					}
362 362
 				}
363 363
 
@@ -397,7 +397,7 @@  discard block
 block discarded – undo
397 397
 					$tmpFile = $tempManager->getTemporaryFile($ext);
398 398
 				}
399 399
 				$handle = fopen($tmpFile, $mode);
400
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
400
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
401 401
 					$this->writeBack($tmpFile, $path);
402 402
 				});
403 403
 		}
@@ -422,7 +422,7 @@  discard block
 block discarded – undo
422 422
 				return FileInfo::SPACE_UNKNOWN;
423 423
 			}
424 424
 			if (isset($response['{DAV:}quota-available-bytes'])) {
425
-				return (int)$response['{DAV:}quota-available-bytes'];
425
+				return (int) $response['{DAV:}quota-available-bytes'];
426 426
 			} else {
427 427
 				return FileInfo::SPACE_UNKNOWN;
428 428
 			}
@@ -498,7 +498,7 @@  discard block
 block discarded – undo
498 498
 
499 499
 		$this->httpClientService
500 500
 			->newClient()
501
-			->put($this->createBaseUri() . $this->encodePath($target), [
501
+			->put($this->createBaseUri().$this->encodePath($target), [
502 502
 				'body' => $source,
503 503
 				'auth' => [$this->user, $this->password]
504 504
 			]);
@@ -515,18 +515,18 @@  discard block
 block discarded – undo
515 515
 			// overwrite directory ?
516 516
 			if ($this->is_dir($path2)) {
517 517
 				// needs trailing slash in destination
518
-				$path2 = rtrim($path2, '/') . '/';
518
+				$path2 = rtrim($path2, '/').'/';
519 519
 			}
520 520
 			$this->client->request(
521 521
 				'MOVE',
522 522
 				$this->encodePath($path1),
523 523
 				null,
524 524
 				[
525
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
525
+					'Destination' => $this->createBaseUri().$this->encodePath($path2),
526 526
 				]
527 527
 			);
528
-			$this->statCache->clear($path1 . '/');
529
-			$this->statCache->clear($path2 . '/');
528
+			$this->statCache->clear($path1.'/');
529
+			$this->statCache->clear($path2.'/');
530 530
 			$this->statCache->set($path1, false);
531 531
 			$this->statCache->set($path2, true);
532 532
 			$this->removeCachedFile($path1);
@@ -547,17 +547,17 @@  discard block
 block discarded – undo
547 547
 			// overwrite directory ?
548 548
 			if ($this->is_dir($path2)) {
549 549
 				// needs trailing slash in destination
550
-				$path2 = rtrim($path2, '/') . '/';
550
+				$path2 = rtrim($path2, '/').'/';
551 551
 			}
552 552
 			$this->client->request(
553 553
 				'COPY',
554 554
 				$this->encodePath($path1),
555 555
 				null,
556 556
 				[
557
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
557
+					'Destination' => $this->createBaseUri().$this->encodePath($path2),
558 558
 				]
559 559
 			);
560
-			$this->statCache->clear($path2 . '/');
560
+			$this->statCache->clear($path2.'/');
561 561
 			$this->statCache->set($path2, true);
562 562
 			$this->removeCachedFile($path2);
563 563
 			return true;
@@ -576,7 +576,7 @@  discard block
 block discarded – undo
576 576
 			}
577 577
 			return [
578 578
 				'mtime' => strtotime($response['{DAV:}getlastmodified']),
579
-				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
579
+				'size' => (int) isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
580 580
 			];
581 581
 		} catch (\Exception $e) {
582 582
 			$this->convertException($e, $path);
@@ -658,7 +658,7 @@  discard block
 block discarded – undo
658 658
 			return $response['statusCode'] == $expected;
659 659
 		} catch (ClientHttpException $e) {
660 660
 			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
661
-				$this->statCache->clear($path . '/');
661
+				$this->statCache->clear($path.'/');
662 662
 				$this->statCache->set($path, false);
663 663
 				return false;
664 664
 			}
@@ -679,22 +679,22 @@  discard block
 block discarded – undo
679 679
 
680 680
 	/** {@inheritdoc} */
681 681
 	public function isUpdatable($path) {
682
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
682
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
683 683
 	}
684 684
 
685 685
 	/** {@inheritdoc} */
686 686
 	public function isCreatable($path) {
687
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
687
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_CREATE);
688 688
 	}
689 689
 
690 690
 	/** {@inheritdoc} */
691 691
 	public function isSharable($path) {
692
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
692
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
693 693
 	}
694 694
 
695 695
 	/** {@inheritdoc} */
696 696
 	public function isDeletable($path) {
697
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
697
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_DELETE);
698 698
 	}
699 699
 
700 700
 	/** {@inheritdoc} */
@@ -783,7 +783,7 @@  discard block
 block discarded – undo
783 783
 				if (!empty($etag) && $cachedData['etag'] !== $etag) {
784 784
 					return true;
785 785
 				} else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
786
-					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
786
+					$sharePermissions = (int) $response['{http://open-collaboration-services.org/ns}share-permissions'];
787 787
 					return $sharePermissions !== $cachedData['permissions'];
788 788
 				} else if (isset($response['{http://owncloud.org/ns}permissions'])) {
789 789
 					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
@@ -799,7 +799,7 @@  discard block
 block discarded – undo
799 799
 			if ($e->getHttpStatus() === 405) {
800 800
 				if ($path === '') {
801 801
 					// if root is gone it means the storage is not available
802
-					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
802
+					throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
803 803
 				}
804 804
 				return false;
805 805
 			}
@@ -833,19 +833,19 @@  discard block
 block discarded – undo
833 833
 			}
834 834
 			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
835 835
 				// either password was changed or was invalid all along
836
-				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
836
+				throw new StorageInvalidException(get_class($e).': '.$e->getMessage());
837 837
 			} else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
838 838
 				// ignore exception for MethodNotAllowed, false will be returned
839 839
 				return;
840 840
 			}
841
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
841
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
842 842
 		} else if ($e instanceof ClientException) {
843 843
 			// connection timeout or refused, server could be temporarily down
844
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
844
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
845 845
 		} else if ($e instanceof \InvalidArgumentException) {
846 846
 			// parse error because the server returned HTML instead of XML,
847 847
 			// possibly temporarily down
848
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
848
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
849 849
 		} else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
850 850
 			// rethrow
851 851
 			throw $e;
Please login to merge, or discard this patch.
lib/private/Files/ObjectStore/SwiftFactory.php 1 patch
Indentation   +168 added lines, -168 removed lines patch added patch discarded remove patch
@@ -41,172 +41,172 @@
 block discarded – undo
41 41
 use OpenStack\ObjectStore\v1\Models\Container;
42 42
 
43 43
 class SwiftFactory {
44
-	private $cache;
45
-	private $params;
46
-	/** @var Container|null */
47
-	private $container = null;
48
-	private $logger;
49
-
50
-	public function __construct(ICache $cache, array $params, ILogger $logger) {
51
-		$this->cache = $cache;
52
-		$this->params = $params;
53
-		$this->logger = $logger;
54
-	}
55
-
56
-	private function getCachedToken(string $cacheKey) {
57
-		$cachedTokenString = $this->cache->get($cacheKey . '/token');
58
-		if ($cachedTokenString) {
59
-			return json_decode($cachedTokenString);
60
-		} else {
61
-			return null;
62
-		}
63
-	}
64
-
65
-	private function cacheToken(Token $token, string $cacheKey) {
66
-		if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
67
-			$value = json_encode($token->export());
68
-		} else {
69
-			$value = json_encode($token);
70
-		}
71
-		$this->cache->set($cacheKey . '/token', $value);
72
-	}
73
-
74
-	/**
75
-	 * @return OpenStack
76
-	 * @throws StorageAuthException
77
-	 */
78
-	private function getClient() {
79
-		if (isset($this->params['bucket'])) {
80
-			$this->params['container'] = $this->params['bucket'];
81
-		}
82
-		if (!isset($this->params['container'])) {
83
-			$this->params['container'] = 'nextcloud';
84
-		}
85
-		if (!isset($this->params['autocreate'])) {
86
-			// should only be true for tests
87
-			$this->params['autocreate'] = false;
88
-		}
89
-		if (isset($this->params['user']) && is_array($this->params['user'])) {
90
-			$userName = $this->params['user']['name'];
91
-		} else {
92
-			if (!isset($this->params['username']) && isset($this->params['user'])) {
93
-				$this->params['username'] = $this->params['user'];
94
-			}
95
-			$userName = $this->params['username'];
96
-		}
97
-		if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
98
-			$this->params['tenantName'] = $this->params['tenant'];
99
-		}
100
-
101
-		$cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
102
-		$token = $this->getCachedToken($cacheKey);
103
-		$this->params['cachedToken'] = $token;
104
-
105
-		$httpClient = new Client([
106
-			'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
107
-			'handler' => HandlerStack::create()
108
-		]);
109
-
110
-		if (isset($this->params['user']) && isset($this->params['user']['name'])) {
111
-			return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
112
-		} else {
113
-			return $this->auth(IdentityV2Service::factory($httpClient), $cacheKey);
114
-		}
115
-	}
116
-
117
-	/**
118
-	 * @param IdentityV2Service|IdentityV3Service $authService
119
-	 * @param string $cacheKey
120
-	 * @return OpenStack
121
-	 * @throws StorageAuthException
122
-	 */
123
-	private function auth($authService, string $cacheKey) {
124
-		$this->params['identityService'] = $authService;
125
-		$this->params['authUrl'] = $this->params['url'];
126
-		$client = new OpenStack($this->params);
127
-
128
-		$cachedToken = $this->params['cachedToken'];
129
-		$hasValidCachedToken = false;
130
-		if (is_array($cachedToken)) {
131
-			$token = $authService->generateTokenFromCache($cachedToken);
132
-			if (is_null($token->catalog)) {
133
-				$this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
134
-			} else if ($token->hasExpired()) {
135
-				$this->logger->debug('Cached token for swift expired');
136
-			} else {
137
-				$hasValidCachedToken = true;
138
-			}
139
-		}
140
-
141
-		if (!$hasValidCachedToken) {
142
-			try {
143
-				$token = $authService->generateToken($this->params);
144
-				$this->cacheToken($token, $cacheKey);
145
-			} catch (ConnectException $e) {
146
-				throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
147
-			} catch (ClientException $e) {
148
-				$statusCode = $e->getResponse()->getStatusCode();
149
-				if ($statusCode === 404) {
150
-					throw new StorageAuthException('Keystone not found, verify the keystone url', $e);
151
-				} else if ($statusCode === 412) {
152
-					throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
153
-				} else if ($statusCode === 401) {
154
-					throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
155
-				} else {
156
-					throw new StorageAuthException('Unknown error', $e);
157
-				}
158
-			} catch (RequestException $e) {
159
-				throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
160
-			}
161
-		}
162
-
163
-		return $client;
164
-	}
165
-
166
-	/**
167
-	 * @return \OpenStack\ObjectStore\v1\Models\Container
168
-	 * @throws StorageAuthException
169
-	 * @throws StorageNotAvailableException
170
-	 */
171
-	public function getContainer() {
172
-		if (is_null($this->container)) {
173
-			$this->container = $this->createContainer();
174
-		}
175
-
176
-		return $this->container;
177
-	}
178
-
179
-	/**
180
-	 * @return \OpenStack\ObjectStore\v1\Models\Container
181
-	 * @throws StorageAuthException
182
-	 * @throws StorageNotAvailableException
183
-	 */
184
-	private function createContainer() {
185
-		$client = $this->getClient();
186
-		$objectStoreService = $client->objectStoreV1();
187
-
188
-		$autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
189
-		try {
190
-			$container = $objectStoreService->getContainer($this->params['container']);
191
-			if ($autoCreate) {
192
-				$container->getMetadata();
193
-			}
194
-			return $container;
195
-		} catch (BadResponseError $ex) {
196
-			// if the container does not exist and autocreate is true try to create the container on the fly
197
-			if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
198
-				return $objectStoreService->createContainer([
199
-					'name' => $this->params['container']
200
-				]);
201
-			} else {
202
-				throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
203
-			}
204
-		} catch (ConnectException $e) {
205
-			/** @var RequestInterface $request */
206
-			$request = $e->getRequest();
207
-			$host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
208
-			\OC::$server->getLogger()->error("Can't connect to object storage server at $host");
209
-			throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
210
-		}
211
-	}
44
+    private $cache;
45
+    private $params;
46
+    /** @var Container|null */
47
+    private $container = null;
48
+    private $logger;
49
+
50
+    public function __construct(ICache $cache, array $params, ILogger $logger) {
51
+        $this->cache = $cache;
52
+        $this->params = $params;
53
+        $this->logger = $logger;
54
+    }
55
+
56
+    private function getCachedToken(string $cacheKey) {
57
+        $cachedTokenString = $this->cache->get($cacheKey . '/token');
58
+        if ($cachedTokenString) {
59
+            return json_decode($cachedTokenString);
60
+        } else {
61
+            return null;
62
+        }
63
+    }
64
+
65
+    private function cacheToken(Token $token, string $cacheKey) {
66
+        if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
67
+            $value = json_encode($token->export());
68
+        } else {
69
+            $value = json_encode($token);
70
+        }
71
+        $this->cache->set($cacheKey . '/token', $value);
72
+    }
73
+
74
+    /**
75
+     * @return OpenStack
76
+     * @throws StorageAuthException
77
+     */
78
+    private function getClient() {
79
+        if (isset($this->params['bucket'])) {
80
+            $this->params['container'] = $this->params['bucket'];
81
+        }
82
+        if (!isset($this->params['container'])) {
83
+            $this->params['container'] = 'nextcloud';
84
+        }
85
+        if (!isset($this->params['autocreate'])) {
86
+            // should only be true for tests
87
+            $this->params['autocreate'] = false;
88
+        }
89
+        if (isset($this->params['user']) && is_array($this->params['user'])) {
90
+            $userName = $this->params['user']['name'];
91
+        } else {
92
+            if (!isset($this->params['username']) && isset($this->params['user'])) {
93
+                $this->params['username'] = $this->params['user'];
94
+            }
95
+            $userName = $this->params['username'];
96
+        }
97
+        if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
98
+            $this->params['tenantName'] = $this->params['tenant'];
99
+        }
100
+
101
+        $cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
102
+        $token = $this->getCachedToken($cacheKey);
103
+        $this->params['cachedToken'] = $token;
104
+
105
+        $httpClient = new Client([
106
+            'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
107
+            'handler' => HandlerStack::create()
108
+        ]);
109
+
110
+        if (isset($this->params['user']) && isset($this->params['user']['name'])) {
111
+            return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
112
+        } else {
113
+            return $this->auth(IdentityV2Service::factory($httpClient), $cacheKey);
114
+        }
115
+    }
116
+
117
+    /**
118
+     * @param IdentityV2Service|IdentityV3Service $authService
119
+     * @param string $cacheKey
120
+     * @return OpenStack
121
+     * @throws StorageAuthException
122
+     */
123
+    private function auth($authService, string $cacheKey) {
124
+        $this->params['identityService'] = $authService;
125
+        $this->params['authUrl'] = $this->params['url'];
126
+        $client = new OpenStack($this->params);
127
+
128
+        $cachedToken = $this->params['cachedToken'];
129
+        $hasValidCachedToken = false;
130
+        if (is_array($cachedToken)) {
131
+            $token = $authService->generateTokenFromCache($cachedToken);
132
+            if (is_null($token->catalog)) {
133
+                $this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
134
+            } else if ($token->hasExpired()) {
135
+                $this->logger->debug('Cached token for swift expired');
136
+            } else {
137
+                $hasValidCachedToken = true;
138
+            }
139
+        }
140
+
141
+        if (!$hasValidCachedToken) {
142
+            try {
143
+                $token = $authService->generateToken($this->params);
144
+                $this->cacheToken($token, $cacheKey);
145
+            } catch (ConnectException $e) {
146
+                throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
147
+            } catch (ClientException $e) {
148
+                $statusCode = $e->getResponse()->getStatusCode();
149
+                if ($statusCode === 404) {
150
+                    throw new StorageAuthException('Keystone not found, verify the keystone url', $e);
151
+                } else if ($statusCode === 412) {
152
+                    throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
153
+                } else if ($statusCode === 401) {
154
+                    throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
155
+                } else {
156
+                    throw new StorageAuthException('Unknown error', $e);
157
+                }
158
+            } catch (RequestException $e) {
159
+                throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
160
+            }
161
+        }
162
+
163
+        return $client;
164
+    }
165
+
166
+    /**
167
+     * @return \OpenStack\ObjectStore\v1\Models\Container
168
+     * @throws StorageAuthException
169
+     * @throws StorageNotAvailableException
170
+     */
171
+    public function getContainer() {
172
+        if (is_null($this->container)) {
173
+            $this->container = $this->createContainer();
174
+        }
175
+
176
+        return $this->container;
177
+    }
178
+
179
+    /**
180
+     * @return \OpenStack\ObjectStore\v1\Models\Container
181
+     * @throws StorageAuthException
182
+     * @throws StorageNotAvailableException
183
+     */
184
+    private function createContainer() {
185
+        $client = $this->getClient();
186
+        $objectStoreService = $client->objectStoreV1();
187
+
188
+        $autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
189
+        try {
190
+            $container = $objectStoreService->getContainer($this->params['container']);
191
+            if ($autoCreate) {
192
+                $container->getMetadata();
193
+            }
194
+            return $container;
195
+        } catch (BadResponseError $ex) {
196
+            // if the container does not exist and autocreate is true try to create the container on the fly
197
+            if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
198
+                return $objectStoreService->createContainer([
199
+                    'name' => $this->params['container']
200
+                ]);
201
+            } else {
202
+                throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
203
+            }
204
+        } catch (ConnectException $e) {
205
+            /** @var RequestInterface $request */
206
+            $request = $e->getRequest();
207
+            $host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
208
+            \OC::$server->getLogger()->error("Can't connect to object storage server at $host");
209
+            throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
210
+        }
211
+    }
212 212
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 2 patches
Indentation   +548 added lines, -548 removed lines patch added patch discarded remove patch
@@ -52,552 +52,552 @@
 block discarded – undo
52 52
  * This class provides the functionality needed to install, update and remove apps
53 53
  */
54 54
 class Installer {
55
-	/** @var AppFetcher */
56
-	private $appFetcher;
57
-	/** @var IClientService */
58
-	private $clientService;
59
-	/** @var ITempManager */
60
-	private $tempManager;
61
-	/** @var ILogger */
62
-	private $logger;
63
-	/** @var IConfig */
64
-	private $config;
65
-	/** @var array - for caching the result of app fetcher */
66
-	private $apps = null;
67
-	/** @var bool|null - for caching the result of the ready status */
68
-	private $isInstanceReadyForUpdates = null;
69
-
70
-	/**
71
-	 * @param AppFetcher $appFetcher
72
-	 * @param IClientService $clientService
73
-	 * @param ITempManager $tempManager
74
-	 * @param ILogger $logger
75
-	 * @param IConfig $config
76
-	 */
77
-	public function __construct(AppFetcher $appFetcher,
78
-								IClientService $clientService,
79
-								ITempManager $tempManager,
80
-								ILogger $logger,
81
-								IConfig $config) {
82
-		$this->appFetcher = $appFetcher;
83
-		$this->clientService = $clientService;
84
-		$this->tempManager = $tempManager;
85
-		$this->logger = $logger;
86
-		$this->config = $config;
87
-	}
88
-
89
-	/**
90
-	 * Installs an app that is located in one of the app folders already
91
-	 *
92
-	 * @param string $appId App to install
93
-	 * @throws \Exception
94
-	 * @return string app ID
95
-	 */
96
-	public function installApp($appId) {
97
-		$app = \OC_App::findAppInDirectories($appId);
98
-		if($app === false) {
99
-			throw new \Exception('App not found in any app directory');
100
-		}
101
-
102
-		$basedir = $app['path'].'/'.$appId;
103
-		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
104
-
105
-		$l = \OC::$server->getL10N('core');
106
-
107
-		if(!is_array($info)) {
108
-			throw new \Exception(
109
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
110
-					[$appId]
111
-				)
112
-			);
113
-		}
114
-
115
-		$version = implode('.', \OCP\Util::getVersion());
116
-		if (!\OC_App::isAppCompatible($version, $info)) {
117
-			throw new \Exception(
118
-				// TODO $l
119
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
120
-					[$info['name']]
121
-				)
122
-			);
123
-		}
124
-
125
-		// check for required dependencies
126
-		\OC_App::checkAppDependencies($this->config, $l, $info);
127
-		\OC_App::registerAutoloading($appId, $basedir);
128
-
129
-		//install the database
130
-		if(is_file($basedir.'/appinfo/database.xml')) {
131
-			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
132
-				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
133
-			} else {
134
-				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
135
-			}
136
-		} else {
137
-			$ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
138
-			$ms->migrate();
139
-		}
140
-
141
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
142
-
143
-		//run appinfo/install.php
144
-		self::includeAppScript($basedir . '/appinfo/install.php');
145
-
146
-		$appData = OC_App::getAppInfo($appId);
147
-		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
148
-
149
-		//set the installed version
150
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
151
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
152
-
153
-		//set remote/public handlers
154
-		foreach($info['remote'] as $name=>$path) {
155
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
156
-		}
157
-		foreach($info['public'] as $name=>$path) {
158
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
159
-		}
160
-
161
-		OC_App::setAppTypes($info['id']);
162
-
163
-		return $info['id'];
164
-	}
165
-
166
-	/**
167
-	 * Updates the specified app from the appstore
168
-	 *
169
-	 * @param string $appId
170
-	 * @return bool
171
-	 */
172
-	public function updateAppstoreApp($appId) {
173
-		if($this->isUpdateAvailable($appId)) {
174
-			try {
175
-				$this->downloadApp($appId);
176
-			} catch (\Exception $e) {
177
-				$this->logger->logException($e, [
178
-					'level' => \OCP\Util::ERROR,
179
-					'app' => 'core',
180
-				]);
181
-				return false;
182
-			}
183
-			return OC_App::updateApp($appId);
184
-		}
185
-
186
-		return false;
187
-	}
188
-
189
-	/**
190
-	 * Downloads an app and puts it into the app directory
191
-	 *
192
-	 * @param string $appId
193
-	 *
194
-	 * @throws \Exception If the installation was not successful
195
-	 */
196
-	public function downloadApp($appId) {
197
-		$appId = strtolower($appId);
198
-
199
-		$apps = $this->appFetcher->get();
200
-		foreach($apps as $app) {
201
-			if($app['id'] === $appId) {
202
-				// Load the certificate
203
-				$certificate = new X509();
204
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
205
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
206
-
207
-				// Verify if the certificate has been revoked
208
-				$crl = new X509();
209
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
210
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
211
-				if($crl->validateSignature() !== true) {
212
-					throw new \Exception('Could not validate CRL signature');
213
-				}
214
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
215
-				$revoked = $crl->getRevoked($csn);
216
-				if ($revoked !== false) {
217
-					throw new \Exception(
218
-						sprintf(
219
-							'Certificate "%s" has been revoked',
220
-							$csn
221
-						)
222
-					);
223
-				}
224
-
225
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
226
-				if($certificate->validateSignature() !== true) {
227
-					throw new \Exception(
228
-						sprintf(
229
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
230
-							$appId
231
-						)
232
-					);
233
-				}
234
-
235
-				// Verify if the certificate is issued for the requested app id
236
-				$certInfo = openssl_x509_parse($app['certificate']);
237
-				if(!isset($certInfo['subject']['CN'])) {
238
-					throw new \Exception(
239
-						sprintf(
240
-							'App with id %s has a cert with no CN',
241
-							$appId
242
-						)
243
-					);
244
-				}
245
-				if($certInfo['subject']['CN'] !== $appId) {
246
-					throw new \Exception(
247
-						sprintf(
248
-							'App with id %s has a cert issued to %s',
249
-							$appId,
250
-							$certInfo['subject']['CN']
251
-						)
252
-					);
253
-				}
254
-
255
-				// Download the release
256
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
257
-				$client = $this->clientService->newClient();
258
-				$client->get($app['releases'][0]['download'], ['save_to' => $tempFile]);
259
-
260
-				// Check if the signature actually matches the downloaded content
261
-				$certificate = openssl_get_publickey($app['certificate']);
262
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
263
-				openssl_free_key($certificate);
264
-
265
-				if($verified === true) {
266
-					// Seems to match, let's proceed
267
-					$extractDir = $this->tempManager->getTemporaryFolder();
268
-					$archive = new TAR($tempFile);
269
-
270
-					if($archive) {
271
-						if (!$archive->extract($extractDir)) {
272
-							throw new \Exception(
273
-								sprintf(
274
-									'Could not extract app %s',
275
-									$appId
276
-								)
277
-							);
278
-						}
279
-						$allFiles = scandir($extractDir);
280
-						$folders = array_diff($allFiles, ['.', '..']);
281
-						$folders = array_values($folders);
282
-
283
-						if(count($folders) > 1) {
284
-							throw new \Exception(
285
-								sprintf(
286
-									'Extracted app %s has more than 1 folder',
287
-									$appId
288
-								)
289
-							);
290
-						}
291
-
292
-						// Check if appinfo/info.xml has the same app ID as well
293
-						$loadEntities = libxml_disable_entity_loader(false);
294
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
295
-						libxml_disable_entity_loader($loadEntities);
296
-						if((string)$xml->id !== $appId) {
297
-							throw new \Exception(
298
-								sprintf(
299
-									'App for id %s has a wrong app ID in info.xml: %s',
300
-									$appId,
301
-									(string)$xml->id
302
-								)
303
-							);
304
-						}
305
-
306
-						// Check if the version is lower than before
307
-						$currentVersion = OC_App::getAppVersion($appId);
308
-						$newVersion = (string)$xml->version;
309
-						if(version_compare($currentVersion, $newVersion) === 1) {
310
-							throw new \Exception(
311
-								sprintf(
312
-									'App for id %s has version %s and tried to update to lower version %s',
313
-									$appId,
314
-									$currentVersion,
315
-									$newVersion
316
-								)
317
-							);
318
-						}
319
-
320
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
321
-						// Remove old app with the ID if existent
322
-						OC_Helper::rmdirr($baseDir);
323
-						// Move to app folder
324
-						if(@mkdir($baseDir)) {
325
-							$extractDir .= '/' . $folders[0];
326
-							OC_Helper::copyr($extractDir, $baseDir);
327
-						}
328
-						OC_Helper::copyr($extractDir, $baseDir);
329
-						OC_Helper::rmdirr($extractDir);
330
-						return;
331
-					} else {
332
-						throw new \Exception(
333
-							sprintf(
334
-								'Could not extract app with ID %s to %s',
335
-								$appId,
336
-								$extractDir
337
-							)
338
-						);
339
-					}
340
-				} else {
341
-					// Signature does not match
342
-					throw new \Exception(
343
-						sprintf(
344
-							'App with id %s has invalid signature',
345
-							$appId
346
-						)
347
-					);
348
-				}
349
-			}
350
-		}
351
-
352
-		throw new \Exception(
353
-			sprintf(
354
-				'Could not download app %s',
355
-				$appId
356
-			)
357
-		);
358
-	}
359
-
360
-	/**
361
-	 * Check if an update for the app is available
362
-	 *
363
-	 * @param string $appId
364
-	 * @return string|false false or the version number of the update
365
-	 */
366
-	public function isUpdateAvailable($appId) {
367
-		if ($this->isInstanceReadyForUpdates === null) {
368
-			$installPath = OC_App::getInstallPath();
369
-			if ($installPath === false || $installPath === null) {
370
-				$this->isInstanceReadyForUpdates = false;
371
-			} else {
372
-				$this->isInstanceReadyForUpdates = true;
373
-			}
374
-		}
375
-
376
-		if ($this->isInstanceReadyForUpdates === false) {
377
-			return false;
378
-		}
379
-
380
-		if ($this->isInstalledFromGit($appId) === true) {
381
-			return false;
382
-		}
383
-
384
-		if ($this->apps === null) {
385
-			$this->apps = $this->appFetcher->get();
386
-		}
387
-
388
-		foreach($this->apps as $app) {
389
-			if($app['id'] === $appId) {
390
-				$currentVersion = OC_App::getAppVersion($appId);
391
-				$newestVersion = $app['releases'][0]['version'];
392
-				if (version_compare($newestVersion, $currentVersion, '>')) {
393
-					return $newestVersion;
394
-				} else {
395
-					return false;
396
-				}
397
-			}
398
-		}
399
-
400
-		return false;
401
-	}
402
-
403
-	/**
404
-	 * Check if app has been installed from git
405
-	 * @param string $name name of the application to remove
406
-	 * @return boolean
407
-	 *
408
-	 * The function will check if the path contains a .git folder
409
-	 */
410
-	private function isInstalledFromGit($appId) {
411
-		$app = \OC_App::findAppInDirectories($appId);
412
-		if($app === false) {
413
-			return false;
414
-		}
415
-		$basedir = $app['path'].'/'.$appId;
416
-		return file_exists($basedir.'/.git/');
417
-	}
418
-
419
-	/**
420
-	 * Check if app is already downloaded
421
-	 * @param string $name name of the application to remove
422
-	 * @return boolean
423
-	 *
424
-	 * The function will check if the app is already downloaded in the apps repository
425
-	 */
426
-	public function isDownloaded($name) {
427
-		foreach(\OC::$APPSROOTS as $dir) {
428
-			$dirToTest  = $dir['path'];
429
-			$dirToTest .= '/';
430
-			$dirToTest .= $name;
431
-			$dirToTest .= '/';
432
-
433
-			if (is_dir($dirToTest)) {
434
-				return true;
435
-			}
436
-		}
437
-
438
-		return false;
439
-	}
440
-
441
-	/**
442
-	 * Removes an app
443
-	 * @param string $appId ID of the application to remove
444
-	 * @return boolean
445
-	 *
446
-	 *
447
-	 * This function works as follows
448
-	 *   -# call uninstall repair steps
449
-	 *   -# removing the files
450
-	 *
451
-	 * The function will not delete preferences, tables and the configuration,
452
-	 * this has to be done by the function oc_app_uninstall().
453
-	 */
454
-	public function removeApp($appId) {
455
-		if($this->isDownloaded( $appId )) {
456
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
457
-				return false;
458
-			}
459
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
460
-			OC_Helper::rmdirr($appDir);
461
-			return true;
462
-		}else{
463
-			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR);
464
-
465
-			return false;
466
-		}
467
-
468
-	}
469
-
470
-	/**
471
-	 * Installs the app within the bundle and marks the bundle as installed
472
-	 *
473
-	 * @param Bundle $bundle
474
-	 * @throws \Exception If app could not get installed
475
-	 */
476
-	public function installAppBundle(Bundle $bundle) {
477
-		$appIds = $bundle->getAppIdentifiers();
478
-		foreach($appIds as $appId) {
479
-			if(!$this->isDownloaded($appId)) {
480
-				$this->downloadApp($appId);
481
-			}
482
-			$this->installApp($appId);
483
-			$app = new OC_App();
484
-			$app->enable($appId);
485
-		}
486
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
487
-		$bundles[] = $bundle->getIdentifier();
488
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
489
-	}
490
-
491
-	/**
492
-	 * Installs shipped apps
493
-	 *
494
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
495
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
496
-	 *                         working ownCloud at the end instead of an aborted update.
497
-	 * @return array Array of error messages (appid => Exception)
498
-	 */
499
-	public static function installShippedApps($softErrors = false) {
500
-		$appManager = \OC::$server->getAppManager();
501
-		$config = \OC::$server->getConfig();
502
-		$errors = [];
503
-		foreach(\OC::$APPSROOTS as $app_dir) {
504
-			if($dir = opendir( $app_dir['path'] )) {
505
-				while( false !== ( $filename = readdir( $dir ))) {
506
-					if( $filename[0] !== '.' and is_dir($app_dir['path']."/$filename") ) {
507
-						if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) {
508
-							if($config->getAppValue($filename, "installed_version", null) === null) {
509
-								$info=OC_App::getAppInfo($filename);
510
-								$enabled = isset($info['default_enable']);
511
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
512
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
513
-									if ($softErrors) {
514
-										try {
515
-											Installer::installShippedApp($filename);
516
-										} catch (HintException $e) {
517
-											if ($e->getPrevious() instanceof TableExistsException) {
518
-												$errors[$filename] = $e;
519
-												continue;
520
-											}
521
-											throw $e;
522
-										}
523
-									} else {
524
-										Installer::installShippedApp($filename);
525
-									}
526
-									$config->setAppValue($filename, 'enabled', 'yes');
527
-								}
528
-							}
529
-						}
530
-					}
531
-				}
532
-				closedir( $dir );
533
-			}
534
-		}
535
-
536
-		return $errors;
537
-	}
538
-
539
-	/**
540
-	 * install an app already placed in the app folder
541
-	 * @param string $app id of the app to install
542
-	 * @return integer
543
-	 */
544
-	public static function installShippedApp($app) {
545
-		//install the database
546
-		$appPath = OC_App::getAppPath($app);
547
-		\OC_App::registerAutoloading($app, $appPath);
548
-
549
-		if(is_file("$appPath/appinfo/database.xml")) {
550
-			try {
551
-				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
552
-			} catch (TableExistsException $e) {
553
-				throw new HintException(
554
-					'Failed to enable app ' . $app,
555
-					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
556
-					0, $e
557
-				);
558
-			}
559
-		} else {
560
-			$ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
561
-			$ms->migrate();
562
-		}
563
-
564
-		//run appinfo/install.php
565
-		self::includeAppScript("$appPath/appinfo/install.php");
566
-
567
-		$info = OC_App::getAppInfo($app);
568
-		if (is_null($info)) {
569
-			return false;
570
-		}
571
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
572
-
573
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
574
-
575
-		$config = \OC::$server->getConfig();
576
-
577
-		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
578
-		if (array_key_exists('ocsid', $info)) {
579
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
580
-		}
581
-
582
-		//set remote/public handlers
583
-		foreach($info['remote'] as $name=>$path) {
584
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
585
-		}
586
-		foreach($info['public'] as $name=>$path) {
587
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
588
-		}
589
-
590
-		OC_App::setAppTypes($info['id']);
591
-
592
-		return $info['id'];
593
-	}
594
-
595
-	/**
596
-	 * @param string $script
597
-	 */
598
-	private static function includeAppScript($script) {
599
-		if ( file_exists($script) ){
600
-			include $script;
601
-		}
602
-	}
55
+    /** @var AppFetcher */
56
+    private $appFetcher;
57
+    /** @var IClientService */
58
+    private $clientService;
59
+    /** @var ITempManager */
60
+    private $tempManager;
61
+    /** @var ILogger */
62
+    private $logger;
63
+    /** @var IConfig */
64
+    private $config;
65
+    /** @var array - for caching the result of app fetcher */
66
+    private $apps = null;
67
+    /** @var bool|null - for caching the result of the ready status */
68
+    private $isInstanceReadyForUpdates = null;
69
+
70
+    /**
71
+     * @param AppFetcher $appFetcher
72
+     * @param IClientService $clientService
73
+     * @param ITempManager $tempManager
74
+     * @param ILogger $logger
75
+     * @param IConfig $config
76
+     */
77
+    public function __construct(AppFetcher $appFetcher,
78
+                                IClientService $clientService,
79
+                                ITempManager $tempManager,
80
+                                ILogger $logger,
81
+                                IConfig $config) {
82
+        $this->appFetcher = $appFetcher;
83
+        $this->clientService = $clientService;
84
+        $this->tempManager = $tempManager;
85
+        $this->logger = $logger;
86
+        $this->config = $config;
87
+    }
88
+
89
+    /**
90
+     * Installs an app that is located in one of the app folders already
91
+     *
92
+     * @param string $appId App to install
93
+     * @throws \Exception
94
+     * @return string app ID
95
+     */
96
+    public function installApp($appId) {
97
+        $app = \OC_App::findAppInDirectories($appId);
98
+        if($app === false) {
99
+            throw new \Exception('App not found in any app directory');
100
+        }
101
+
102
+        $basedir = $app['path'].'/'.$appId;
103
+        $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
104
+
105
+        $l = \OC::$server->getL10N('core');
106
+
107
+        if(!is_array($info)) {
108
+            throw new \Exception(
109
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
110
+                    [$appId]
111
+                )
112
+            );
113
+        }
114
+
115
+        $version = implode('.', \OCP\Util::getVersion());
116
+        if (!\OC_App::isAppCompatible($version, $info)) {
117
+            throw new \Exception(
118
+                // TODO $l
119
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
120
+                    [$info['name']]
121
+                )
122
+            );
123
+        }
124
+
125
+        // check for required dependencies
126
+        \OC_App::checkAppDependencies($this->config, $l, $info);
127
+        \OC_App::registerAutoloading($appId, $basedir);
128
+
129
+        //install the database
130
+        if(is_file($basedir.'/appinfo/database.xml')) {
131
+            if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
132
+                OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
133
+            } else {
134
+                OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
135
+            }
136
+        } else {
137
+            $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
138
+            $ms->migrate();
139
+        }
140
+
141
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
142
+
143
+        //run appinfo/install.php
144
+        self::includeAppScript($basedir . '/appinfo/install.php');
145
+
146
+        $appData = OC_App::getAppInfo($appId);
147
+        OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
148
+
149
+        //set the installed version
150
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
151
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
152
+
153
+        //set remote/public handlers
154
+        foreach($info['remote'] as $name=>$path) {
155
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
156
+        }
157
+        foreach($info['public'] as $name=>$path) {
158
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
159
+        }
160
+
161
+        OC_App::setAppTypes($info['id']);
162
+
163
+        return $info['id'];
164
+    }
165
+
166
+    /**
167
+     * Updates the specified app from the appstore
168
+     *
169
+     * @param string $appId
170
+     * @return bool
171
+     */
172
+    public function updateAppstoreApp($appId) {
173
+        if($this->isUpdateAvailable($appId)) {
174
+            try {
175
+                $this->downloadApp($appId);
176
+            } catch (\Exception $e) {
177
+                $this->logger->logException($e, [
178
+                    'level' => \OCP\Util::ERROR,
179
+                    'app' => 'core',
180
+                ]);
181
+                return false;
182
+            }
183
+            return OC_App::updateApp($appId);
184
+        }
185
+
186
+        return false;
187
+    }
188
+
189
+    /**
190
+     * Downloads an app and puts it into the app directory
191
+     *
192
+     * @param string $appId
193
+     *
194
+     * @throws \Exception If the installation was not successful
195
+     */
196
+    public function downloadApp($appId) {
197
+        $appId = strtolower($appId);
198
+
199
+        $apps = $this->appFetcher->get();
200
+        foreach($apps as $app) {
201
+            if($app['id'] === $appId) {
202
+                // Load the certificate
203
+                $certificate = new X509();
204
+                $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
205
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
206
+
207
+                // Verify if the certificate has been revoked
208
+                $crl = new X509();
209
+                $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
210
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
211
+                if($crl->validateSignature() !== true) {
212
+                    throw new \Exception('Could not validate CRL signature');
213
+                }
214
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
215
+                $revoked = $crl->getRevoked($csn);
216
+                if ($revoked !== false) {
217
+                    throw new \Exception(
218
+                        sprintf(
219
+                            'Certificate "%s" has been revoked',
220
+                            $csn
221
+                        )
222
+                    );
223
+                }
224
+
225
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
226
+                if($certificate->validateSignature() !== true) {
227
+                    throw new \Exception(
228
+                        sprintf(
229
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
230
+                            $appId
231
+                        )
232
+                    );
233
+                }
234
+
235
+                // Verify if the certificate is issued for the requested app id
236
+                $certInfo = openssl_x509_parse($app['certificate']);
237
+                if(!isset($certInfo['subject']['CN'])) {
238
+                    throw new \Exception(
239
+                        sprintf(
240
+                            'App with id %s has a cert with no CN',
241
+                            $appId
242
+                        )
243
+                    );
244
+                }
245
+                if($certInfo['subject']['CN'] !== $appId) {
246
+                    throw new \Exception(
247
+                        sprintf(
248
+                            'App with id %s has a cert issued to %s',
249
+                            $appId,
250
+                            $certInfo['subject']['CN']
251
+                        )
252
+                    );
253
+                }
254
+
255
+                // Download the release
256
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
257
+                $client = $this->clientService->newClient();
258
+                $client->get($app['releases'][0]['download'], ['save_to' => $tempFile]);
259
+
260
+                // Check if the signature actually matches the downloaded content
261
+                $certificate = openssl_get_publickey($app['certificate']);
262
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
263
+                openssl_free_key($certificate);
264
+
265
+                if($verified === true) {
266
+                    // Seems to match, let's proceed
267
+                    $extractDir = $this->tempManager->getTemporaryFolder();
268
+                    $archive = new TAR($tempFile);
269
+
270
+                    if($archive) {
271
+                        if (!$archive->extract($extractDir)) {
272
+                            throw new \Exception(
273
+                                sprintf(
274
+                                    'Could not extract app %s',
275
+                                    $appId
276
+                                )
277
+                            );
278
+                        }
279
+                        $allFiles = scandir($extractDir);
280
+                        $folders = array_diff($allFiles, ['.', '..']);
281
+                        $folders = array_values($folders);
282
+
283
+                        if(count($folders) > 1) {
284
+                            throw new \Exception(
285
+                                sprintf(
286
+                                    'Extracted app %s has more than 1 folder',
287
+                                    $appId
288
+                                )
289
+                            );
290
+                        }
291
+
292
+                        // Check if appinfo/info.xml has the same app ID as well
293
+                        $loadEntities = libxml_disable_entity_loader(false);
294
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
295
+                        libxml_disable_entity_loader($loadEntities);
296
+                        if((string)$xml->id !== $appId) {
297
+                            throw new \Exception(
298
+                                sprintf(
299
+                                    'App for id %s has a wrong app ID in info.xml: %s',
300
+                                    $appId,
301
+                                    (string)$xml->id
302
+                                )
303
+                            );
304
+                        }
305
+
306
+                        // Check if the version is lower than before
307
+                        $currentVersion = OC_App::getAppVersion($appId);
308
+                        $newVersion = (string)$xml->version;
309
+                        if(version_compare($currentVersion, $newVersion) === 1) {
310
+                            throw new \Exception(
311
+                                sprintf(
312
+                                    'App for id %s has version %s and tried to update to lower version %s',
313
+                                    $appId,
314
+                                    $currentVersion,
315
+                                    $newVersion
316
+                                )
317
+                            );
318
+                        }
319
+
320
+                        $baseDir = OC_App::getInstallPath() . '/' . $appId;
321
+                        // Remove old app with the ID if existent
322
+                        OC_Helper::rmdirr($baseDir);
323
+                        // Move to app folder
324
+                        if(@mkdir($baseDir)) {
325
+                            $extractDir .= '/' . $folders[0];
326
+                            OC_Helper::copyr($extractDir, $baseDir);
327
+                        }
328
+                        OC_Helper::copyr($extractDir, $baseDir);
329
+                        OC_Helper::rmdirr($extractDir);
330
+                        return;
331
+                    } else {
332
+                        throw new \Exception(
333
+                            sprintf(
334
+                                'Could not extract app with ID %s to %s',
335
+                                $appId,
336
+                                $extractDir
337
+                            )
338
+                        );
339
+                    }
340
+                } else {
341
+                    // Signature does not match
342
+                    throw new \Exception(
343
+                        sprintf(
344
+                            'App with id %s has invalid signature',
345
+                            $appId
346
+                        )
347
+                    );
348
+                }
349
+            }
350
+        }
351
+
352
+        throw new \Exception(
353
+            sprintf(
354
+                'Could not download app %s',
355
+                $appId
356
+            )
357
+        );
358
+    }
359
+
360
+    /**
361
+     * Check if an update for the app is available
362
+     *
363
+     * @param string $appId
364
+     * @return string|false false or the version number of the update
365
+     */
366
+    public function isUpdateAvailable($appId) {
367
+        if ($this->isInstanceReadyForUpdates === null) {
368
+            $installPath = OC_App::getInstallPath();
369
+            if ($installPath === false || $installPath === null) {
370
+                $this->isInstanceReadyForUpdates = false;
371
+            } else {
372
+                $this->isInstanceReadyForUpdates = true;
373
+            }
374
+        }
375
+
376
+        if ($this->isInstanceReadyForUpdates === false) {
377
+            return false;
378
+        }
379
+
380
+        if ($this->isInstalledFromGit($appId) === true) {
381
+            return false;
382
+        }
383
+
384
+        if ($this->apps === null) {
385
+            $this->apps = $this->appFetcher->get();
386
+        }
387
+
388
+        foreach($this->apps as $app) {
389
+            if($app['id'] === $appId) {
390
+                $currentVersion = OC_App::getAppVersion($appId);
391
+                $newestVersion = $app['releases'][0]['version'];
392
+                if (version_compare($newestVersion, $currentVersion, '>')) {
393
+                    return $newestVersion;
394
+                } else {
395
+                    return false;
396
+                }
397
+            }
398
+        }
399
+
400
+        return false;
401
+    }
402
+
403
+    /**
404
+     * Check if app has been installed from git
405
+     * @param string $name name of the application to remove
406
+     * @return boolean
407
+     *
408
+     * The function will check if the path contains a .git folder
409
+     */
410
+    private function isInstalledFromGit($appId) {
411
+        $app = \OC_App::findAppInDirectories($appId);
412
+        if($app === false) {
413
+            return false;
414
+        }
415
+        $basedir = $app['path'].'/'.$appId;
416
+        return file_exists($basedir.'/.git/');
417
+    }
418
+
419
+    /**
420
+     * Check if app is already downloaded
421
+     * @param string $name name of the application to remove
422
+     * @return boolean
423
+     *
424
+     * The function will check if the app is already downloaded in the apps repository
425
+     */
426
+    public function isDownloaded($name) {
427
+        foreach(\OC::$APPSROOTS as $dir) {
428
+            $dirToTest  = $dir['path'];
429
+            $dirToTest .= '/';
430
+            $dirToTest .= $name;
431
+            $dirToTest .= '/';
432
+
433
+            if (is_dir($dirToTest)) {
434
+                return true;
435
+            }
436
+        }
437
+
438
+        return false;
439
+    }
440
+
441
+    /**
442
+     * Removes an app
443
+     * @param string $appId ID of the application to remove
444
+     * @return boolean
445
+     *
446
+     *
447
+     * This function works as follows
448
+     *   -# call uninstall repair steps
449
+     *   -# removing the files
450
+     *
451
+     * The function will not delete preferences, tables and the configuration,
452
+     * this has to be done by the function oc_app_uninstall().
453
+     */
454
+    public function removeApp($appId) {
455
+        if($this->isDownloaded( $appId )) {
456
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
457
+                return false;
458
+            }
459
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
460
+            OC_Helper::rmdirr($appDir);
461
+            return true;
462
+        }else{
463
+            \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR);
464
+
465
+            return false;
466
+        }
467
+
468
+    }
469
+
470
+    /**
471
+     * Installs the app within the bundle and marks the bundle as installed
472
+     *
473
+     * @param Bundle $bundle
474
+     * @throws \Exception If app could not get installed
475
+     */
476
+    public function installAppBundle(Bundle $bundle) {
477
+        $appIds = $bundle->getAppIdentifiers();
478
+        foreach($appIds as $appId) {
479
+            if(!$this->isDownloaded($appId)) {
480
+                $this->downloadApp($appId);
481
+            }
482
+            $this->installApp($appId);
483
+            $app = new OC_App();
484
+            $app->enable($appId);
485
+        }
486
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
487
+        $bundles[] = $bundle->getIdentifier();
488
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
489
+    }
490
+
491
+    /**
492
+     * Installs shipped apps
493
+     *
494
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
495
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
496
+     *                         working ownCloud at the end instead of an aborted update.
497
+     * @return array Array of error messages (appid => Exception)
498
+     */
499
+    public static function installShippedApps($softErrors = false) {
500
+        $appManager = \OC::$server->getAppManager();
501
+        $config = \OC::$server->getConfig();
502
+        $errors = [];
503
+        foreach(\OC::$APPSROOTS as $app_dir) {
504
+            if($dir = opendir( $app_dir['path'] )) {
505
+                while( false !== ( $filename = readdir( $dir ))) {
506
+                    if( $filename[0] !== '.' and is_dir($app_dir['path']."/$filename") ) {
507
+                        if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) {
508
+                            if($config->getAppValue($filename, "installed_version", null) === null) {
509
+                                $info=OC_App::getAppInfo($filename);
510
+                                $enabled = isset($info['default_enable']);
511
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
512
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
513
+                                    if ($softErrors) {
514
+                                        try {
515
+                                            Installer::installShippedApp($filename);
516
+                                        } catch (HintException $e) {
517
+                                            if ($e->getPrevious() instanceof TableExistsException) {
518
+                                                $errors[$filename] = $e;
519
+                                                continue;
520
+                                            }
521
+                                            throw $e;
522
+                                        }
523
+                                    } else {
524
+                                        Installer::installShippedApp($filename);
525
+                                    }
526
+                                    $config->setAppValue($filename, 'enabled', 'yes');
527
+                                }
528
+                            }
529
+                        }
530
+                    }
531
+                }
532
+                closedir( $dir );
533
+            }
534
+        }
535
+
536
+        return $errors;
537
+    }
538
+
539
+    /**
540
+     * install an app already placed in the app folder
541
+     * @param string $app id of the app to install
542
+     * @return integer
543
+     */
544
+    public static function installShippedApp($app) {
545
+        //install the database
546
+        $appPath = OC_App::getAppPath($app);
547
+        \OC_App::registerAutoloading($app, $appPath);
548
+
549
+        if(is_file("$appPath/appinfo/database.xml")) {
550
+            try {
551
+                OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
552
+            } catch (TableExistsException $e) {
553
+                throw new HintException(
554
+                    'Failed to enable app ' . $app,
555
+                    'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
556
+                    0, $e
557
+                );
558
+            }
559
+        } else {
560
+            $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
561
+            $ms->migrate();
562
+        }
563
+
564
+        //run appinfo/install.php
565
+        self::includeAppScript("$appPath/appinfo/install.php");
566
+
567
+        $info = OC_App::getAppInfo($app);
568
+        if (is_null($info)) {
569
+            return false;
570
+        }
571
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
572
+
573
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
574
+
575
+        $config = \OC::$server->getConfig();
576
+
577
+        $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
578
+        if (array_key_exists('ocsid', $info)) {
579
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
580
+        }
581
+
582
+        //set remote/public handlers
583
+        foreach($info['remote'] as $name=>$path) {
584
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
585
+        }
586
+        foreach($info['public'] as $name=>$path) {
587
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
588
+        }
589
+
590
+        OC_App::setAppTypes($info['id']);
591
+
592
+        return $info['id'];
593
+    }
594
+
595
+    /**
596
+     * @param string $script
597
+     */
598
+    private static function includeAppScript($script) {
599
+        if ( file_exists($script) ){
600
+            include $script;
601
+        }
602
+    }
603 603
 }
Please login to merge, or discard this patch.
Spacing   +50 added lines, -50 removed lines patch added patch discarded remove patch
@@ -95,7 +95,7 @@  discard block
 block discarded – undo
95 95
 	 */
96 96
 	public function installApp($appId) {
97 97
 		$app = \OC_App::findAppInDirectories($appId);
98
-		if($app === false) {
98
+		if ($app === false) {
99 99
 			throw new \Exception('App not found in any app directory');
100 100
 		}
101 101
 
@@ -104,7 +104,7 @@  discard block
 block discarded – undo
104 104
 
105 105
 		$l = \OC::$server->getL10N('core');
106 106
 
107
-		if(!is_array($info)) {
107
+		if (!is_array($info)) {
108 108
 			throw new \Exception(
109 109
 				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
110 110
 					[$appId]
@@ -127,7 +127,7 @@  discard block
 block discarded – undo
127 127
 		\OC_App::registerAutoloading($appId, $basedir);
128 128
 
129 129
 		//install the database
130
-		if(is_file($basedir.'/appinfo/database.xml')) {
130
+		if (is_file($basedir.'/appinfo/database.xml')) {
131 131
 			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
132 132
 				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
133 133
 			} else {
@@ -141,7 +141,7 @@  discard block
 block discarded – undo
141 141
 		\OC_App::setupBackgroundJobs($info['background-jobs']);
142 142
 
143 143
 		//run appinfo/install.php
144
-		self::includeAppScript($basedir . '/appinfo/install.php');
144
+		self::includeAppScript($basedir.'/appinfo/install.php');
145 145
 
146 146
 		$appData = OC_App::getAppInfo($appId);
147 147
 		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
@@ -151,10 +151,10 @@  discard block
 block discarded – undo
151 151
 		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
152 152
 
153 153
 		//set remote/public handlers
154
-		foreach($info['remote'] as $name=>$path) {
154
+		foreach ($info['remote'] as $name=>$path) {
155 155
 			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
156 156
 		}
157
-		foreach($info['public'] as $name=>$path) {
157
+		foreach ($info['public'] as $name=>$path) {
158 158
 			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
159 159
 		}
160 160
 
@@ -170,7 +170,7 @@  discard block
 block discarded – undo
170 170
 	 * @return bool
171 171
 	 */
172 172
 	public function updateAppstoreApp($appId) {
173
-		if($this->isUpdateAvailable($appId)) {
173
+		if ($this->isUpdateAvailable($appId)) {
174 174
 			try {
175 175
 				$this->downloadApp($appId);
176 176
 			} catch (\Exception $e) {
@@ -197,18 +197,18 @@  discard block
 block discarded – undo
197 197
 		$appId = strtolower($appId);
198 198
 
199 199
 		$apps = $this->appFetcher->get();
200
-		foreach($apps as $app) {
201
-			if($app['id'] === $appId) {
200
+		foreach ($apps as $app) {
201
+			if ($app['id'] === $appId) {
202 202
 				// Load the certificate
203 203
 				$certificate = new X509();
204
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
204
+				$certificate->loadCA(file_get_contents(__DIR__.'/../../resources/codesigning/root.crt'));
205 205
 				$loadedCertificate = $certificate->loadX509($app['certificate']);
206 206
 
207 207
 				// Verify if the certificate has been revoked
208 208
 				$crl = new X509();
209
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
210
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
211
-				if($crl->validateSignature() !== true) {
209
+				$crl->loadCA(file_get_contents(__DIR__.'/../../resources/codesigning/root.crt'));
210
+				$crl->loadCRL(file_get_contents(__DIR__.'/../../resources/codesigning/root.crl'));
211
+				if ($crl->validateSignature() !== true) {
212 212
 					throw new \Exception('Could not validate CRL signature');
213 213
 				}
214 214
 				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
@@ -223,7 +223,7 @@  discard block
 block discarded – undo
223 223
 				}
224 224
 
225 225
 				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
226
-				if($certificate->validateSignature() !== true) {
226
+				if ($certificate->validateSignature() !== true) {
227 227
 					throw new \Exception(
228 228
 						sprintf(
229 229
 							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
@@ -234,7 +234,7 @@  discard block
 block discarded – undo
234 234
 
235 235
 				// Verify if the certificate is issued for the requested app id
236 236
 				$certInfo = openssl_x509_parse($app['certificate']);
237
-				if(!isset($certInfo['subject']['CN'])) {
237
+				if (!isset($certInfo['subject']['CN'])) {
238 238
 					throw new \Exception(
239 239
 						sprintf(
240 240
 							'App with id %s has a cert with no CN',
@@ -242,7 +242,7 @@  discard block
 block discarded – undo
242 242
 						)
243 243
 					);
244 244
 				}
245
-				if($certInfo['subject']['CN'] !== $appId) {
245
+				if ($certInfo['subject']['CN'] !== $appId) {
246 246
 					throw new \Exception(
247 247
 						sprintf(
248 248
 							'App with id %s has a cert issued to %s',
@@ -259,15 +259,15 @@  discard block
 block discarded – undo
259 259
 
260 260
 				// Check if the signature actually matches the downloaded content
261 261
 				$certificate = openssl_get_publickey($app['certificate']);
262
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
262
+				$verified = (bool) openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
263 263
 				openssl_free_key($certificate);
264 264
 
265
-				if($verified === true) {
265
+				if ($verified === true) {
266 266
 					// Seems to match, let's proceed
267 267
 					$extractDir = $this->tempManager->getTemporaryFolder();
268 268
 					$archive = new TAR($tempFile);
269 269
 
270
-					if($archive) {
270
+					if ($archive) {
271 271
 						if (!$archive->extract($extractDir)) {
272 272
 							throw new \Exception(
273 273
 								sprintf(
@@ -280,7 +280,7 @@  discard block
 block discarded – undo
280 280
 						$folders = array_diff($allFiles, ['.', '..']);
281 281
 						$folders = array_values($folders);
282 282
 
283
-						if(count($folders) > 1) {
283
+						if (count($folders) > 1) {
284 284
 							throw new \Exception(
285 285
 								sprintf(
286 286
 									'Extracted app %s has more than 1 folder',
@@ -291,22 +291,22 @@  discard block
 block discarded – undo
291 291
 
292 292
 						// Check if appinfo/info.xml has the same app ID as well
293 293
 						$loadEntities = libxml_disable_entity_loader(false);
294
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
294
+						$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
295 295
 						libxml_disable_entity_loader($loadEntities);
296
-						if((string)$xml->id !== $appId) {
296
+						if ((string) $xml->id !== $appId) {
297 297
 							throw new \Exception(
298 298
 								sprintf(
299 299
 									'App for id %s has a wrong app ID in info.xml: %s',
300 300
 									$appId,
301
-									(string)$xml->id
301
+									(string) $xml->id
302 302
 								)
303 303
 							);
304 304
 						}
305 305
 
306 306
 						// Check if the version is lower than before
307 307
 						$currentVersion = OC_App::getAppVersion($appId);
308
-						$newVersion = (string)$xml->version;
309
-						if(version_compare($currentVersion, $newVersion) === 1) {
308
+						$newVersion = (string) $xml->version;
309
+						if (version_compare($currentVersion, $newVersion) === 1) {
310 310
 							throw new \Exception(
311 311
 								sprintf(
312 312
 									'App for id %s has version %s and tried to update to lower version %s',
@@ -317,12 +317,12 @@  discard block
 block discarded – undo
317 317
 							);
318 318
 						}
319 319
 
320
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
320
+						$baseDir = OC_App::getInstallPath().'/'.$appId;
321 321
 						// Remove old app with the ID if existent
322 322
 						OC_Helper::rmdirr($baseDir);
323 323
 						// Move to app folder
324
-						if(@mkdir($baseDir)) {
325
-							$extractDir .= '/' . $folders[0];
324
+						if (@mkdir($baseDir)) {
325
+							$extractDir .= '/'.$folders[0];
326 326
 							OC_Helper::copyr($extractDir, $baseDir);
327 327
 						}
328 328
 						OC_Helper::copyr($extractDir, $baseDir);
@@ -385,8 +385,8 @@  discard block
 block discarded – undo
385 385
 			$this->apps = $this->appFetcher->get();
386 386
 		}
387 387
 
388
-		foreach($this->apps as $app) {
389
-			if($app['id'] === $appId) {
388
+		foreach ($this->apps as $app) {
389
+			if ($app['id'] === $appId) {
390 390
 				$currentVersion = OC_App::getAppVersion($appId);
391 391
 				$newestVersion = $app['releases'][0]['version'];
392 392
 				if (version_compare($newestVersion, $currentVersion, '>')) {
@@ -409,7 +409,7 @@  discard block
 block discarded – undo
409 409
 	 */
410 410
 	private function isInstalledFromGit($appId) {
411 411
 		$app = \OC_App::findAppInDirectories($appId);
412
-		if($app === false) {
412
+		if ($app === false) {
413 413
 			return false;
414 414
 		}
415 415
 		$basedir = $app['path'].'/'.$appId;
@@ -424,7 +424,7 @@  discard block
 block discarded – undo
424 424
 	 * The function will check if the app is already downloaded in the apps repository
425 425
 	 */
426 426
 	public function isDownloaded($name) {
427
-		foreach(\OC::$APPSROOTS as $dir) {
427
+		foreach (\OC::$APPSROOTS as $dir) {
428 428
 			$dirToTest  = $dir['path'];
429 429
 			$dirToTest .= '/';
430 430
 			$dirToTest .= $name;
@@ -452,14 +452,14 @@  discard block
 block discarded – undo
452 452
 	 * this has to be done by the function oc_app_uninstall().
453 453
 	 */
454 454
 	public function removeApp($appId) {
455
-		if($this->isDownloaded( $appId )) {
455
+		if ($this->isDownloaded($appId)) {
456 456
 			if (\OC::$server->getAppManager()->isShipped($appId)) {
457 457
 				return false;
458 458
 			}
459
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
459
+			$appDir = OC_App::getInstallPath().'/'.$appId;
460 460
 			OC_Helper::rmdirr($appDir);
461 461
 			return true;
462
-		}else{
462
+		} else {
463 463
 			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR);
464 464
 
465 465
 			return false;
@@ -475,8 +475,8 @@  discard block
 block discarded – undo
475 475
 	 */
476 476
 	public function installAppBundle(Bundle $bundle) {
477 477
 		$appIds = $bundle->getAppIdentifiers();
478
-		foreach($appIds as $appId) {
479
-			if(!$this->isDownloaded($appId)) {
478
+		foreach ($appIds as $appId) {
479
+			if (!$this->isDownloaded($appId)) {
480 480
 				$this->downloadApp($appId);
481 481
 			}
482 482
 			$this->installApp($appId);
@@ -500,13 +500,13 @@  discard block
 block discarded – undo
500 500
 		$appManager = \OC::$server->getAppManager();
501 501
 		$config = \OC::$server->getConfig();
502 502
 		$errors = [];
503
-		foreach(\OC::$APPSROOTS as $app_dir) {
504
-			if($dir = opendir( $app_dir['path'] )) {
505
-				while( false !== ( $filename = readdir( $dir ))) {
506
-					if( $filename[0] !== '.' and is_dir($app_dir['path']."/$filename") ) {
507
-						if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) {
508
-							if($config->getAppValue($filename, "installed_version", null) === null) {
509
-								$info=OC_App::getAppInfo($filename);
503
+		foreach (\OC::$APPSROOTS as $app_dir) {
504
+			if ($dir = opendir($app_dir['path'])) {
505
+				while (false !== ($filename = readdir($dir))) {
506
+					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
507
+						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
508
+							if ($config->getAppValue($filename, "installed_version", null) === null) {
509
+								$info = OC_App::getAppInfo($filename);
510 510
 								$enabled = isset($info['default_enable']);
511 511
 								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
512 512
 									  && $config->getAppValue($filename, 'enabled') !== 'no') {
@@ -529,7 +529,7 @@  discard block
 block discarded – undo
529 529
 						}
530 530
 					}
531 531
 				}
532
-				closedir( $dir );
532
+				closedir($dir);
533 533
 			}
534 534
 		}
535 535
 
@@ -546,12 +546,12 @@  discard block
 block discarded – undo
546 546
 		$appPath = OC_App::getAppPath($app);
547 547
 		\OC_App::registerAutoloading($app, $appPath);
548 548
 
549
-		if(is_file("$appPath/appinfo/database.xml")) {
549
+		if (is_file("$appPath/appinfo/database.xml")) {
550 550
 			try {
551 551
 				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
552 552
 			} catch (TableExistsException $e) {
553 553
 				throw new HintException(
554
-					'Failed to enable app ' . $app,
554
+					'Failed to enable app '.$app,
555 555
 					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
556 556
 					0, $e
557 557
 				);
@@ -580,10 +580,10 @@  discard block
 block discarded – undo
580 580
 		}
581 581
 
582 582
 		//set remote/public handlers
583
-		foreach($info['remote'] as $name=>$path) {
583
+		foreach ($info['remote'] as $name=>$path) {
584 584
 			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
585 585
 		}
586
-		foreach($info['public'] as $name=>$path) {
586
+		foreach ($info['public'] as $name=>$path) {
587 587
 			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
588 588
 		}
589 589
 
@@ -596,7 +596,7 @@  discard block
 block discarded – undo
596 596
 	 * @param string $script
597 597
 	 */
598 598
 	private static function includeAppScript($script) {
599
-		if ( file_exists($script) ){
599
+		if (file_exists($script)) {
600 600
 			include $script;
601 601
 		}
602 602
 	}
Please login to merge, or discard this patch.