Completed
Pull Request — master (#26700)
by Philipp
08:19
created

files_external/lib/Service/StoragesService.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @author Jesús Macias <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Robin McCorkell <[email protected]>
8
 * @author Stefan Weil <[email protected]>
9
 * @author Vincent Petry <[email protected]>
10
 *
11
 * @copyright Copyright (c) 2016, ownCloud GmbH.
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OCA\Files_External\Service;
29
30
use \OC\Files\Filesystem;
31
use OCA\Files_External\Lib\StorageConfig;
32
use OCA\Files_External\NotFoundException;
33
use \OCA\Files_External\Lib\Backend\Backend;
34
use \OCA\Files_External\Lib\Auth\AuthMechanism;
35
use OCP\Files\Config\IUserMountCache;
36
use \OCP\Files\StorageNotAvailableException;
37
38
/**
39
 * Service class to manage external storages
40
 */
41
abstract class StoragesService {
42
43
	/** @var BackendService */
44
	protected $backendService;
45
46
	/**
47
	 * @var DBConfigService
48
	 */
49
	protected $dbConfig;
50
51
	/**
52
	 * @var IUserMountCache
53
	 */
54
	protected $userMountCache;
55
56
	/**
57
	 * @param BackendService $backendService
58
	 * @param DBConfigService $dbConfigService
59
	 * @param IUserMountCache $userMountCache
60
	 */
61
	public function __construct(BackendService $backendService, DBConfigService $dbConfigService, IUserMountCache $userMountCache) {
62
		$this->backendService = $backendService;
63
		$this->dbConfig = $dbConfigService;
64
		$this->userMountCache = $userMountCache;
65
	}
66
67
	protected function readDBConfig() {
68
		return $this->dbConfig->getAdminMounts();
69
	}
70
71
	protected function getStorageConfigFromDBMount(array $mount) {
72
		$applicableUsers = array_filter($mount['applicable'], function ($applicable) {
73
			return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER;
74
		});
75
		$applicableUsers = array_map(function ($applicable) {
76
			return $applicable['value'];
77
		}, $applicableUsers);
78
79
		$applicableGroups = array_filter($mount['applicable'], function ($applicable) {
80
			return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP;
81
		});
82
		$applicableGroups = array_map(function ($applicable) {
83
			return $applicable['value'];
84
		}, $applicableGroups);
85
86
		try {
87
			$config = $this->createStorage(
88
				$mount['mount_point'],
89
				$mount['storage_backend'],
90
				$mount['auth_backend'],
91
				$mount['config'],
92
				$mount['options'],
93
				array_values($applicableUsers),
94
				array_values($applicableGroups),
95
				$mount['priority']
96
			);
97
			$config->setType($mount['type']);
98
			$config->setId((int)$mount['mount_id']);
99
			return $config;
100
		} catch (\UnexpectedValueException $e) {
101
			// don't die if a storage backend doesn't exist
102
			\OCP\Util::writeLog(
103
				'files_external',
104
				'Could not load storage: "' . $e->getMessage() . '"',
105
				\OCP\Util::ERROR
106
			);
107
			return null;
108
		} catch (\InvalidArgumentException $e) {
109
			\OCP\Util::writeLog(
110
				'files_external',
111
				'Could not load storage: "' . $e->getMessage() . '"',
112
				\OCP\Util::ERROR
113
			);
114
			return null;
115
		}
116
	}
117
118
	/**
119
	 * Read the external storages config
120
	 *
121
	 * @return array map of storage id to storage config
122
	 */
123 View Code Duplication
	protected function readConfig() {
124
		$mounts = $this->readDBConfig();
125
		$configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
126
		$configs = array_filter($configs, function ($config) {
127
			return $config instanceof StorageConfig;
128
		});
129
130
		$keys = array_map(function (StorageConfig $config) {
131
			return $config->getId();
132
		}, $configs);
133
134
		return array_combine($keys, $configs);
135
	}
136
137
	/**
138
	 * Get a storage with status
139
	 *
140
	 * @param int $id storage id
141
	 *
142
	 * @return StorageConfig
143
	 * @throws NotFoundException if the storage with the given id was not found
144
	 */
145
	public function getStorage($id) {
146
		$mount = $this->dbConfig->getMountById($id);
147
148
		if (!is_array($mount)) {
149
			throw new NotFoundException('Storage with id "' . $id . '" not found');
150
		}
151
152
		$config = $this->getStorageConfigFromDBMount($mount);
153
		if ($this->isApplicable($config)) {
0 ignored issues
show
It seems like $config defined by $this->getStorageConfigFromDBMount($mount) on line 152 can be null; however, OCA\Files_External\Servi...Service::isApplicable() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
154
			return $config;
155
		} else {
156
			throw new NotFoundException('Storage with id "' . $id . '" not found');
157
		}
158
	}
159
160
	/**
161
	 * Check whether this storage service should provide access to a storage
162
	 *
163
	 * @param StorageConfig $config
164
	 * @return bool
165
	 */
166
	abstract protected function isApplicable(StorageConfig $config);
167
168
	/**
169
	 * Gets all storages, valid or not
170
	 *
171
	 * @return StorageConfig[] array of storage configs
172
	 */
173
	public function getAllStorages() {
174
		return $this->readConfig();
175
	}
176
177
	/**
178
	 * Gets all valid storages
179
	 *
180
	 * @return StorageConfig[]
181
	 */
182
	public function getStorages() {
183
		return array_filter($this->getAllStorages(), [$this, 'validateStorage']);
184
	}
185
186
	/**
187
	 * Validate storage
188
	 * FIXME: De-duplicate with StoragesController::validate()
189
	 *
190
	 * @param StorageConfig $storage
191
	 * @return bool
192
	 */
193
	protected function validateStorage(StorageConfig $storage) {
194
		/** @var Backend */
195
		$backend = $storage->getBackend();
196
		/** @var AuthMechanism */
197
		$authMechanism = $storage->getAuthMechanism();
198
199
		if (!$backend->isVisibleFor($this->getVisibilityType())) {
200
			// not permitted to use backend
201
			return false;
202
		}
203
		if (!$authMechanism->isVisibleFor($this->getVisibilityType())) {
204
			// not permitted to use auth mechanism
205
			return false;
206
		}
207
208
		return true;
209
	}
210
211
	/**
212
	 * Get the visibility type for this controller, used in validation
213
	 *
214
	 * @return string BackendService::VISIBILITY_* constants
215
	 */
216
	abstract public function getVisibilityType();
217
218
	/**
219
	 * @return integer
220
	 */
221
	protected function getType() {
222
		return DBConfigService::MOUNT_TYPE_ADMIN;
223
	}
224
225
	/**
226
	 * Add new storage to the configuration
227
	 *
228
	 * @param StorageConfig $newStorage storage attributes
229
	 *
230
	 * @return StorageConfig storage config, with added id
231
	 */
232
	public function addStorage(StorageConfig $newStorage) {
233
		$allStorages = $this->readConfig();
234
235
		$configId = $this->dbConfig->addMount(
236
			$newStorage->getMountPoint(),
237
			$newStorage->getBackend()->getIdentifier(),
238
			$newStorage->getAuthMechanism()->getIdentifier(),
239
			$newStorage->getPriority(),
240
			$this->getType()
241
		);
242
243
		$newStorage->setId($configId);
244
245
		foreach ($newStorage->getApplicableUsers() as $user) {
246
			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_USER, $user);
247
		}
248
		foreach ($newStorage->getApplicableGroups() as $group) {
249
			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
250
		}
251
		foreach ($newStorage->getBackendOptions() as $key => $value) {
252
			$this->dbConfig->setConfig($configId, $key, $value);
253
		}
254
		foreach ($newStorage->getMountOptions() as $key => $value) {
255
			$this->dbConfig->setOption($configId, $key, $value);
256
		}
257
258
		if (count($newStorage->getApplicableUsers()) === 0 && count($newStorage->getApplicableGroups()) === 0) {
259
			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
260
		}
261
262
		// add new storage
263
		$allStorages[$configId] = $newStorage;
264
265
		$this->triggerHooks($newStorage, Filesystem::signal_create_mount);
266
267
		$newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS);
268
		return $newStorage;
269
	}
270
271
	/**
272
	 * Create a storage from its parameters
273
	 *
274
	 * @param string $mountPoint storage mount point
275
	 * @param string $backendIdentifier backend identifier
276
	 * @param string $authMechanismIdentifier authentication mechanism identifier
277
	 * @param array $backendOptions backend-specific options
278
	 * @param array|null $mountOptions mount-specific options
279
	 * @param array|null $applicableUsers users for which to mount the storage
280
	 * @param array|null $applicableGroups groups for which to mount the storage
281
	 * @param int|null $priority priority
282
	 *
283
	 * @return StorageConfig
284
	 */
285
	public function createStorage(
286
		$mountPoint,
287
		$backendIdentifier,
288
		$authMechanismIdentifier,
289
		$backendOptions,
290
		$mountOptions = null,
291
		$applicableUsers = null,
292
		$applicableGroups = null,
293
		$priority = null
294
	) {
295
		$backend = $this->backendService->getBackend($backendIdentifier);
296
		if (!$backend) {
297
			throw new \InvalidArgumentException('Unable to get backend for ' . $backendIdentifier);
298
		}
299
		$authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
300
		if (!$authMechanism) {
301
			throw new \InvalidArgumentException('Unable to get authentication mechanism for ' . $authMechanismIdentifier);
302
		}
303
		$newStorage = new StorageConfig();
304
		$newStorage->setMountPoint($mountPoint);
305
		$newStorage->setBackend($backend);
306
		$newStorage->setAuthMechanism($authMechanism);
307
		$newStorage->setBackendOptions($backendOptions);
308
		if (isset($mountOptions)) {
309
			$newStorage->setMountOptions($mountOptions);
310
		}
311
		if (isset($applicableUsers)) {
312
			$newStorage->setApplicableUsers($applicableUsers);
313
		}
314
		if (isset($applicableGroups)) {
315
			$newStorage->setApplicableGroups($applicableGroups);
316
		}
317
		if (isset($priority)) {
318
			$newStorage->setPriority($priority);
319
		}
320
321
		return $newStorage;
322
	}
323
324
	/**
325
	 * Triggers the given hook signal for all the applicables given
326
	 *
327
	 * @param string $signal signal
328
	 * @param string $mountPoint hook mount pount param
329
	 * @param string $mountType hook mount type param
330
	 * @param array $applicableArray array of applicable users/groups for which to trigger the hook
331
	 */
332
	protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray) {
333
		foreach ($applicableArray as $applicable) {
334
			\OCP\Util::emitHook(
335
				Filesystem::CLASSNAME,
336
				$signal,
337
				[
338
					Filesystem::signal_param_path => $mountPoint,
339
					Filesystem::signal_param_mount_type => $mountType,
340
					Filesystem::signal_param_users => $applicable,
341
				]
342
			);
343
		}
344
	}
345
346
	/**
347
	 * Triggers $signal for all applicable users of the given
348
	 * storage
349
	 *
350
	 * @param StorageConfig $storage storage data
351
	 * @param string $signal signal to trigger
352
	 */
353
	abstract protected function triggerHooks(StorageConfig $storage, $signal);
354
355
	/**
356
	 * Triggers signal_create_mount or signal_delete_mount to
357
	 * accommodate for additions/deletions in applicableUsers
358
	 * and applicableGroups fields.
359
	 *
360
	 * @param StorageConfig $oldStorage old storage data
361
	 * @param StorageConfig $newStorage new storage data
362
	 */
363
	abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage);
364
365
	/**
366
	 * Update storage to the configuration
367
	 *
368
	 * @param StorageConfig $updatedStorage storage attributes
369
	 *
370
	 * @return StorageConfig storage config
371
	 * @throws NotFoundException if the given storage does not exist in the config
372
	 */
373
	public function updateStorage(StorageConfig $updatedStorage) {
374
		$id = $updatedStorage->getId();
375
376
		$existingMount = $this->dbConfig->getMountById($id);
377
378
		if (!is_array($existingMount)) {
379
			throw new NotFoundException('Storage with id "' . $id . '" not found while updating storage');
380
		}
381
382
		$oldStorage = $this->getStorageConfigFromDBMount($existingMount);
383
384
		$removedUsers = array_diff($oldStorage->getApplicableUsers(), $updatedStorage->getApplicableUsers());
385
		$removedGroups = array_diff($oldStorage->getApplicableGroups(), $updatedStorage->getApplicableGroups());
386
		$addedUsers = array_diff($updatedStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
387
		$addedGroups = array_diff($updatedStorage->getApplicableGroups(), $oldStorage->getApplicableGroups());
388
389
		$oldUserCount = count($oldStorage->getApplicableUsers());
390
		$oldGroupCount = count($oldStorage->getApplicableGroups());
391
		$newUserCount = count($updatedStorage->getApplicableUsers());
392
		$newGroupCount = count($updatedStorage->getApplicableGroups());
393
		$wasGlobal = ($oldUserCount + $oldGroupCount) === 0;
394
		$isGlobal = ($newUserCount + $newGroupCount) === 0;
395
396
		foreach ($removedUsers as $user) {
397
			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
398
		}
399
		foreach ($removedGroups as $group) {
400
			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
401
		}
402
		foreach ($addedUsers as $user) {
403
			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
404
		}
405
		foreach ($addedGroups as $group) {
406
			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
407
		}
408
409
		if ($wasGlobal && !$isGlobal) {
410
			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
411
		} else if (!$wasGlobal && $isGlobal) {
412
			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
413
		}
414
415
		$changedConfig = array_diff_assoc($updatedStorage->getBackendOptions(), $oldStorage->getBackendOptions());
416
		$changedOptions = array_diff_assoc($updatedStorage->getMountOptions(), $oldStorage->getMountOptions());
417
418
		foreach ($changedConfig as $key => $value) {
419
			$this->dbConfig->setConfig($id, $key, $value);
420
		}
421
		foreach ($changedOptions as $key => $value) {
422
			$this->dbConfig->setOption($id, $key, $value);
423
		}
424
425
		if ($updatedStorage->getMountPoint() !== $oldStorage->getMountPoint()) {
426
			$this->dbConfig->setMountPoint($id, $updatedStorage->getMountPoint());
427
		}
428
429
		if ($updatedStorage->getAuthMechanism()->getIdentifier() !== $oldStorage->getAuthMechanism()->getIdentifier()) {
430
			$this->dbConfig->setAuthBackend($id, $updatedStorage->getAuthMechanism()->getIdentifier());
431
		}
432
433
		$this->triggerChangeHooks($oldStorage, $updatedStorage);
434
435
		if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly
436
			$this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage));
437
		} else {
438
			$storageId = $this->getStorageId($updatedStorage);
439
			foreach ($removedUsers as $userId) {
440
				$this->userMountCache->removeUserStorageMount($storageId, $userId);
441
			}
442
		}
443
444
		return $this->getStorage($id);
445
	}
446
447
	/**
448
	 * Delete the storage with the given id.
449
	 *
450
	 * @param int $id storage id
451
	 *
452
	 * @throws NotFoundException if no storage was found with the given id
453
	 */
454
	public function removeStorage($id) {
455
		$existingMount = $this->dbConfig->getMountById($id);
456
457
		if (!is_array($existingMount)) {
458
			throw new NotFoundException('Storage with id "' . $id . '" not found');
459
		}
460
461
		$this->dbConfig->removeMount($id);
462
463
		$deletedStorage = $this->getStorageConfigFromDBMount($existingMount);
464
		$this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount);
465
466
		// delete oc_storages entries and oc_filecache
467
		try {
468
			$rustyStorageId = $this->getRustyStorageIdFromConfig($deletedStorage);
469
			\OC\Files\Cache\Storage::remove($rustyStorageId);
470
		} catch (\Exception $e) {
471
			// can happen either for invalid configs where the storage could not
472
			// be instantiated or whenever $user vars where used, in which case
473
			// the storage id could not be computed
474
			\OCP\Util::writeLog(
475
				'files_external',
476
				'Exception: "' . $e->getMessage() . '"',
477
				\OCP\Util::ERROR
478
			);
479
		}
480
	}
481
482
	/**
483
	 * Returns the rusty storage id from oc_storages from the given storage config.
484
	 *
485
	 * @param StorageConfig $storageConfig
486
	 * @return string rusty storage id
487
	 */
488
	private function getRustyStorageIdFromConfig(StorageConfig $storageConfig) {
489
		// if any of the storage options contains $user, it is not possible
490
		// to compute the possible storage id as we don't know which users
491
		// mounted it already (and we certainly don't want to iterate over ALL users)
492
		foreach ($storageConfig->getBackendOptions() as $value) {
493
			if (strpos($value, '$user') !== false) {
494
				throw new \Exception('Cannot compute storage id for deletion due to $user vars in the configuration');
495
			}
496
		}
497
498
		// note: similar to ConfigAdapter->prepateStorageConfig()
499
		$storageConfig->getAuthMechanism()->manipulateStorageConfig($storageConfig);
500
		$storageConfig->getBackend()->manipulateStorageConfig($storageConfig);
501
502
		$class = $storageConfig->getBackend()->getStorageClass();
503
		$storageImpl = new $class($storageConfig->getBackendOptions());
504
505
		return $storageImpl->getId();
506
	}
507
508
	/**
509
	 * Construct the storage implementation
510
	 *
511
	 * @param StorageConfig $storageConfig
512
	 * @return int
513
	 */
514
	private function getStorageId(StorageConfig $storageConfig) {
515
		try {
516
			$class = $storageConfig->getBackend()->getStorageClass();
517
			/** @var \OC\Files\Storage\Storage $storage */
518
			$storage = new $class($storageConfig->getBackendOptions());
519
520
			// auth mechanism should fire first
521
			$storage = $storageConfig->getBackend()->wrapStorage($storage);
522
			$storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
523
524
			return $storage->getStorageCache()->getNumericId();
525
		} catch (\Exception $e) {
526
			return -1;
527
		}
528
	}
529
}
530