You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
9.5 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Julius Härtl <jus@bitgrid.net>
  7. * @author Robin Appelman <robin@icewind.nl>
  8. * @author Tobias Kaminsky <tobias@kaminsky.me>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OC\DirectEditing;
  27. use Doctrine\DBAL\FetchMode;
  28. use OCP\AppFramework\Http\NotFoundResponse;
  29. use OCP\AppFramework\Http\Response;
  30. use OCP\AppFramework\Http\TemplateResponse;
  31. use OCP\DB\QueryBuilder\IQueryBuilder;
  32. use OCP\DirectEditing\ACreateFromTemplate;
  33. use OCP\DirectEditing\IEditor;
  34. use \OCP\DirectEditing\IManager;
  35. use OCP\DirectEditing\IToken;
  36. use OCP\Encryption\IManager as EncryptionManager;
  37. use OCP\Files\File;
  38. use OCP\Files\IRootFolder;
  39. use OCP\Files\Node;
  40. use OCP\Files\NotFoundException;
  41. use OCP\IDBConnection;
  42. use OCP\IL10N;
  43. use OCP\IUserSession;
  44. use OCP\L10N\IFactory;
  45. use OCP\Security\ISecureRandom;
  46. use OCP\Share\IShare;
  47. use Throwable;
  48. use function array_key_exists;
  49. use function in_array;
  50. class Manager implements IManager {
  51. private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
  52. public const TABLE_TOKENS = 'direct_edit';
  53. /** @var IEditor[] */
  54. private $editors = [];
  55. /** @var IDBConnection */
  56. private $connection;
  57. /** @var ISecureRandom */
  58. private $random;
  59. /** @var string|null */
  60. private $userId;
  61. /** @var IRootFolder */
  62. private $rootFolder;
  63. /** @var IL10N */
  64. private $l10n;
  65. /** @var EncryptionManager */
  66. private $encryptionManager;
  67. public function __construct(
  68. ISecureRandom $random,
  69. IDBConnection $connection,
  70. IUserSession $userSession,
  71. IRootFolder $rootFolder,
  72. IFactory $l10nFactory,
  73. EncryptionManager $encryptionManager
  74. ) {
  75. $this->random = $random;
  76. $this->connection = $connection;
  77. $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
  78. $this->rootFolder = $rootFolder;
  79. $this->l10n = $l10nFactory->get('core');
  80. $this->encryptionManager = $encryptionManager;
  81. }
  82. public function registerDirectEditor(IEditor $directEditor): void {
  83. $this->editors[$directEditor->getId()] = $directEditor;
  84. }
  85. public function getEditors(): array {
  86. return $this->editors;
  87. }
  88. public function getTemplates(string $editor, string $type): array {
  89. if (!array_key_exists($editor, $this->editors)) {
  90. throw new \RuntimeException('No matching editor found');
  91. }
  92. $templates = [];
  93. foreach ($this->editors[$editor]->getCreators() as $creator) {
  94. if ($creator->getId() === $type) {
  95. $templates = [
  96. 'empty' => [
  97. 'id' => 'empty',
  98. 'title' => $this->l10n->t('Empty file'),
  99. 'preview' => null
  100. ]
  101. ];
  102. if ($creator instanceof ACreateFromTemplate) {
  103. $templates = $creator->getTemplates();
  104. }
  105. $templates = array_map(function ($template) use ($creator) {
  106. $template['extension'] = $creator->getExtension();
  107. $template['mimetype'] = $creator->getMimetype();
  108. return $template;
  109. }, $templates);
  110. }
  111. }
  112. $return = [];
  113. $return['templates'] = $templates;
  114. return $return;
  115. }
  116. public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
  117. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  118. if ($userFolder->nodeExists($path)) {
  119. throw new \RuntimeException('File already exists');
  120. } else {
  121. $file = $userFolder->newFile($path);
  122. $editor = $this->getEditor($editorId);
  123. $creators = $editor->getCreators();
  124. foreach ($creators as $creator) {
  125. if ($creator->getId() === $creatorId) {
  126. $creator->create($file, $creatorId, $templateId);
  127. return $this->createToken($editorId, $file, $path);
  128. }
  129. }
  130. }
  131. throw new \RuntimeException('No creator found');
  132. }
  133. public function open(string $filePath, string $editorId = null): string {
  134. /** @var File $file */
  135. $file = $this->rootFolder->getUserFolder($this->userId)->get($filePath);
  136. if ($editorId === null) {
  137. $editorId = $this->findEditorForFile($file);
  138. }
  139. if (!array_key_exists($editorId, $this->editors)) {
  140. throw new \RuntimeException("Editor $editorId is unknown");
  141. }
  142. return $this->createToken($editorId, $file, $filePath);
  143. }
  144. private function findEditorForFile(File $file) {
  145. foreach ($this->editors as $editor) {
  146. if (in_array($file->getMimeType(), $editor->getMimetypes())) {
  147. return $editor->getId();
  148. }
  149. }
  150. throw new \RuntimeException('No default editor found for files mimetype');
  151. }
  152. public function edit(string $token): Response {
  153. try {
  154. /** @var IEditor $editor */
  155. $tokenObject = $this->getToken($token);
  156. if ($tokenObject->hasBeenAccessed()) {
  157. throw new \RuntimeException('Token has already been used and can only be used for followup requests');
  158. }
  159. $editor = $this->getEditor($tokenObject->getEditor());
  160. $this->accessToken($token);
  161. } catch (Throwable $throwable) {
  162. $this->invalidateToken($token);
  163. return new NotFoundResponse();
  164. }
  165. return $editor->open($tokenObject);
  166. }
  167. public function editSecure(File $file, string $editorId): TemplateResponse {
  168. // TODO: Implementation in follow up
  169. }
  170. private function getEditor($editorId): IEditor {
  171. if (!array_key_exists($editorId, $this->editors)) {
  172. throw new \RuntimeException('No editor found');
  173. }
  174. return $this->editors[$editorId];
  175. }
  176. public function getToken(string $token): IToken {
  177. $query = $this->connection->getQueryBuilder();
  178. $query->select('*')->from(self::TABLE_TOKENS)
  179. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  180. $result = $query->execute();
  181. if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
  182. return new Token($this, $tokenRow);
  183. }
  184. throw new \RuntimeException('Failed to validate the token');
  185. }
  186. public function cleanup(): int {
  187. $query = $this->connection->getQueryBuilder();
  188. $query->delete(self::TABLE_TOKENS)
  189. ->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
  190. return $query->execute();
  191. }
  192. public function refreshToken(string $token): bool {
  193. $query = $this->connection->getQueryBuilder();
  194. $query->update(self::TABLE_TOKENS)
  195. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  196. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  197. $result = $query->execute();
  198. return $result !== 0;
  199. }
  200. public function invalidateToken(string $token): bool {
  201. $query = $this->connection->getQueryBuilder();
  202. $query->delete(self::TABLE_TOKENS)
  203. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  204. $result = $query->execute();
  205. return $result !== 0;
  206. }
  207. public function accessToken(string $token): bool {
  208. $query = $this->connection->getQueryBuilder();
  209. $query->update(self::TABLE_TOKENS)
  210. ->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
  211. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  212. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  213. $result = $query->execute();
  214. return $result !== 0;
  215. }
  216. public function invokeTokenScope($userId): void {
  217. \OC_User::setIncognitoMode(true);
  218. \OC_User::setUserId($userId);
  219. }
  220. public function createToken($editorId, File $file, string $filePath, IShare $share = null): string {
  221. $token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
  222. $query = $this->connection->getQueryBuilder();
  223. $query->insert(self::TABLE_TOKENS)
  224. ->values([
  225. 'token' => $query->createNamedParameter($token),
  226. 'editor_id' => $query->createNamedParameter($editorId),
  227. 'file_id' => $query->createNamedParameter($file->getId()),
  228. 'file_path' => $query->createNamedParameter($filePath),
  229. 'user_id' => $query->createNamedParameter($this->userId),
  230. 'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
  231. 'timestamp' => $query->createNamedParameter(time())
  232. ]);
  233. $query->execute();
  234. return $token;
  235. }
  236. /**
  237. * @param $userId
  238. * @param $fileId
  239. * @param null $filePath
  240. * @return Node
  241. * @throws NotFoundException
  242. */
  243. public function getFileForToken($userId, $fileId, $filePath = null): Node {
  244. $userFolder = $this->rootFolder->getUserFolder($userId);
  245. if ($filePath !== null) {
  246. return $userFolder->get($filePath);
  247. }
  248. $files = $userFolder->getById($fileId);
  249. if (count($files) === 0) {
  250. throw new NotFoundException('File nound found by id ' . $fileId);
  251. }
  252. return $files[0];
  253. }
  254. public function isEnabled(): bool {
  255. if (!$this->encryptionManager->isEnabled()) {
  256. return true;
  257. }
  258. try {
  259. $moduleId = $this->encryptionManager->getDefaultEncryptionModuleId();
  260. $module = $this->encryptionManager->getEncryptionModule($moduleId);
  261. /** @var \OCA\Encryption\Util $util */
  262. $util = \OC::$server->get(\OCA\Encryption\Util::class);
  263. if ($module->isReadyForUser($this->userId) && $util->isMasterKeyEnabled()) {
  264. return true;
  265. }
  266. } catch (Throwable $e) {
  267. }
  268. return false;
  269. }
  270. }