MountConfig.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Files_External;
  8. use OCA\Files_External\Config\IConfigHandler;
  9. use OCA\Files_External\Config\UserContext;
  10. use OCA\Files_External\Lib\Backend\Backend;
  11. use OCA\Files_External\Service\BackendService;
  12. use OCA\Files_External\Service\GlobalStoragesService;
  13. use OCA\Files_External\Service\UserGlobalStoragesService;
  14. use OCA\Files_External\Service\UserStoragesService;
  15. use OCP\Files\StorageNotAvailableException;
  16. use phpseclib\Crypt\AES;
  17. use Psr\Log\LoggerInterface;
  18. /**
  19. * Class to configure mount.json globally and for users
  20. */
  21. class MountConfig {
  22. // TODO: make this class non-static and give it a proper namespace
  23. public const MOUNT_TYPE_GLOBAL = 'global';
  24. public const MOUNT_TYPE_GROUP = 'group';
  25. public const MOUNT_TYPE_USER = 'user';
  26. public const MOUNT_TYPE_PERSONAL = 'personal';
  27. // whether to skip backend test (for unit tests, as this static class is not mockable)
  28. public static $skipTest = false;
  29. /** @var UserGlobalStoragesService */
  30. private $userGlobalStorageService;
  31. /** @var UserStoragesService */
  32. private $userStorageService;
  33. /** @var GlobalStoragesService */
  34. private $globalStorageService;
  35. public function __construct(
  36. UserGlobalStoragesService $userGlobalStorageService,
  37. UserStoragesService $userStorageService,
  38. GlobalStoragesService $globalStorageService
  39. ) {
  40. $this->userGlobalStorageService = $userGlobalStorageService;
  41. $this->userStorageService = $userStorageService;
  42. $this->globalStorageService = $globalStorageService;
  43. }
  44. /**
  45. * @param mixed $input
  46. * @param string|null $userId
  47. * @return mixed
  48. * @throws \OCP\AppFramework\QueryException
  49. * @since 16.0.0
  50. */
  51. public static function substitutePlaceholdersInConfig($input, ?string $userId = null) {
  52. /** @var BackendService $backendService */
  53. $backendService = \OC::$server->get(BackendService::class);
  54. /** @var IConfigHandler[] $handlers */
  55. $handlers = $backendService->getConfigHandlers();
  56. foreach ($handlers as $handler) {
  57. if ($handler instanceof UserContext && $userId !== null) {
  58. $handler->setUserId($userId);
  59. }
  60. $input = $handler->handle($input);
  61. }
  62. return $input;
  63. }
  64. /**
  65. * Test connecting using the given backend configuration
  66. *
  67. * @param string $class backend class name
  68. * @param array $options backend configuration options
  69. * @param boolean $isPersonal
  70. * @return int see self::STATUS_*
  71. * @throws \Exception
  72. */
  73. public static function getBackendStatus($class, $options, $isPersonal, $testOnly = true) {
  74. if (self::$skipTest) {
  75. return StorageNotAvailableException::STATUS_SUCCESS;
  76. }
  77. foreach ($options as $key => &$option) {
  78. if ($key === 'password') {
  79. // no replacements in passwords
  80. continue;
  81. }
  82. $option = self::substitutePlaceholdersInConfig($option);
  83. }
  84. if (class_exists($class)) {
  85. try {
  86. /** @var \OC\Files\Storage\Common $storage */
  87. $storage = new $class($options);
  88. try {
  89. $result = $storage->test($isPersonal, $testOnly);
  90. $storage->setAvailability($result);
  91. if ($result) {
  92. return StorageNotAvailableException::STATUS_SUCCESS;
  93. }
  94. } catch (\Exception $e) {
  95. $storage->setAvailability(false);
  96. throw $e;
  97. }
  98. } catch (\Exception $exception) {
  99. \OC::$server->get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception, 'app' => 'files_external']);
  100. throw $exception;
  101. }
  102. }
  103. return StorageNotAvailableException::STATUS_ERROR;
  104. }
  105. /**
  106. * Get backend dependency message
  107. * TODO: move into AppFramework along with templates
  108. *
  109. * @param Backend[] $backends
  110. */
  111. public static function dependencyMessage(array $backends): string {
  112. $l = \OCP\Util::getL10N('files_external');
  113. $message = '';
  114. $dependencyGroups = [];
  115. foreach ($backends as $backend) {
  116. foreach ($backend->checkDependencies() as $dependency) {
  117. $dependencyMessage = $dependency->getMessage();
  118. if ($dependencyMessage !== null) {
  119. $message .= '<p>' . $dependencyMessage . '</p>';
  120. } else {
  121. $dependencyGroups[$dependency->getDependency()][] = $backend;
  122. }
  123. }
  124. }
  125. foreach ($dependencyGroups as $module => $dependants) {
  126. $backends = implode(', ', array_map(function (Backend $backend): string {
  127. return '"' . $backend->getText() . '"';
  128. }, $dependants));
  129. $message .= '<p>' . MountConfig::getSingleDependencyMessage($l, $module, $backends) . '</p>';
  130. }
  131. return $message;
  132. }
  133. /**
  134. * Returns a dependency missing message
  135. */
  136. private static function getSingleDependencyMessage(\OCP\IL10N $l, string $module, string $backend): string {
  137. switch (strtolower($module)) {
  138. case 'curl':
  139. return $l->t('The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]);
  140. case 'ftp':
  141. return $l->t('The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]);
  142. default:
  143. return $l->t('"%1$s" is not installed. Mounting of %2$s is not possible. Please ask your system administrator to install it.', [$module, $backend]);
  144. }
  145. }
  146. /**
  147. * Encrypt passwords in the given config options
  148. *
  149. * @param array $options mount options
  150. * @return array updated options
  151. */
  152. public static function encryptPasswords($options) {
  153. if (isset($options['password'])) {
  154. $options['password_encrypted'] = self::encryptPassword($options['password']);
  155. // do not unset the password, we want to keep the keys order
  156. // on load... because that's how the UI currently works
  157. $options['password'] = '';
  158. }
  159. return $options;
  160. }
  161. /**
  162. * Decrypt passwords in the given config options
  163. *
  164. * @param array $options mount options
  165. * @return array updated options
  166. */
  167. public static function decryptPasswords($options) {
  168. // note: legacy options might still have the unencrypted password in the "password" field
  169. if (isset($options['password_encrypted'])) {
  170. $options['password'] = self::decryptPassword($options['password_encrypted']);
  171. unset($options['password_encrypted']);
  172. }
  173. return $options;
  174. }
  175. /**
  176. * Encrypt a single password
  177. *
  178. * @param string $password plain text password
  179. * @return string encrypted password
  180. */
  181. private static function encryptPassword($password) {
  182. $cipher = self::getCipher();
  183. $iv = \OC::$server->getSecureRandom()->generate(16);
  184. $cipher->setIV($iv);
  185. return base64_encode($iv . $cipher->encrypt($password));
  186. }
  187. /**
  188. * Decrypts a single password
  189. *
  190. * @param string $encryptedPassword encrypted password
  191. * @return string plain text password
  192. */
  193. private static function decryptPassword($encryptedPassword) {
  194. $cipher = self::getCipher();
  195. $binaryPassword = base64_decode($encryptedPassword);
  196. $iv = substr($binaryPassword, 0, 16);
  197. $cipher->setIV($iv);
  198. $binaryPassword = substr($binaryPassword, 16);
  199. return $cipher->decrypt($binaryPassword);
  200. }
  201. /**
  202. * Returns the encryption cipher
  203. *
  204. * @return AES
  205. */
  206. private static function getCipher() {
  207. $cipher = new AES(AES::MODE_CBC);
  208. $cipher->setKey(\OC::$server->getConfig()->getSystemValue('passwordsalt', null));
  209. return $cipher;
  210. }
  211. /**
  212. * Computes a hash based on the given configuration.
  213. * This is mostly used to find out whether configurations
  214. * are the same.
  215. *
  216. * @param array $config
  217. * @return string
  218. */
  219. public static function makeConfigHash($config) {
  220. $data = json_encode(
  221. [
  222. 'c' => $config['backend'],
  223. 'a' => $config['authMechanism'],
  224. 'm' => $config['mountpoint'],
  225. 'o' => $config['options'],
  226. 'p' => $config['priority'] ?? -1,
  227. 'mo' => $config['mountOptions'] ?? [],
  228. ]
  229. );
  230. return hash('md5', $data);
  231. }
  232. }