Manager.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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 OC\Files\Node\Folder;
  29. use OCP\AppFramework\Http\NotFoundResponse;
  30. use OCP\AppFramework\Http\Response;
  31. use OCP\AppFramework\Http\TemplateResponse;
  32. use OCP\DB\QueryBuilder\IQueryBuilder;
  33. use OCP\DirectEditing\ACreateFromTemplate;
  34. use OCP\DirectEditing\IEditor;
  35. use \OCP\DirectEditing\IManager;
  36. use OCP\DirectEditing\IToken;
  37. use OCP\Encryption\IManager as EncryptionManager;
  38. use OCP\Files\File;
  39. use OCP\Files\IRootFolder;
  40. use OCP\Files\Node;
  41. use OCP\Files\NotFoundException;
  42. use OCP\IDBConnection;
  43. use OCP\IL10N;
  44. use OCP\IUserSession;
  45. use OCP\L10N\IFactory;
  46. use OCP\Security\ISecureRandom;
  47. use OCP\Share\IShare;
  48. use Throwable;
  49. use function array_key_exists;
  50. use function in_array;
  51. class Manager implements IManager {
  52. private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
  53. public const TABLE_TOKENS = 'direct_edit';
  54. /** @var IEditor[] */
  55. private $editors = [];
  56. /** @var IDBConnection */
  57. private $connection;
  58. /** @var ISecureRandom */
  59. private $random;
  60. /** @var string|null */
  61. private $userId;
  62. /** @var IRootFolder */
  63. private $rootFolder;
  64. /** @var IL10N */
  65. private $l10n;
  66. /** @var EncryptionManager */
  67. private $encryptionManager;
  68. public function __construct(
  69. ISecureRandom $random,
  70. IDBConnection $connection,
  71. IUserSession $userSession,
  72. IRootFolder $rootFolder,
  73. IFactory $l10nFactory,
  74. EncryptionManager $encryptionManager
  75. ) {
  76. $this->random = $random;
  77. $this->connection = $connection;
  78. $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
  79. $this->rootFolder = $rootFolder;
  80. $this->l10n = $l10nFactory->get('core');
  81. $this->encryptionManager = $encryptionManager;
  82. }
  83. public function registerDirectEditor(IEditor $directEditor): void {
  84. $this->editors[$directEditor->getId()] = $directEditor;
  85. }
  86. public function getEditors(): array {
  87. return $this->editors;
  88. }
  89. public function getTemplates(string $editor, string $type): array {
  90. if (!array_key_exists($editor, $this->editors)) {
  91. throw new \RuntimeException('No matching editor found');
  92. }
  93. $templates = [];
  94. foreach ($this->editors[$editor]->getCreators() as $creator) {
  95. if ($creator->getId() === $type) {
  96. $templates = [
  97. 'empty' => [
  98. 'id' => 'empty',
  99. 'title' => $this->l10n->t('Empty file'),
  100. 'preview' => null
  101. ]
  102. ];
  103. if ($creator instanceof ACreateFromTemplate) {
  104. $templates = $creator->getTemplates();
  105. }
  106. $templates = array_map(function ($template) use ($creator) {
  107. $template['extension'] = $creator->getExtension();
  108. $template['mimetype'] = $creator->getMimetype();
  109. return $template;
  110. }, $templates);
  111. }
  112. }
  113. $return = [];
  114. $return['templates'] = $templates;
  115. return $return;
  116. }
  117. public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
  118. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  119. if ($userFolder->nodeExists($path)) {
  120. throw new \RuntimeException('File already exists');
  121. } else {
  122. if (!$userFolder->nodeExists(dirname($path))) {
  123. throw new \RuntimeException('Invalid path');
  124. }
  125. /** @var Folder $folder */
  126. $folder = $userFolder->get(dirname($path));
  127. $file = $folder->newFile(basename($path));
  128. $editor = $this->getEditor($editorId);
  129. $creators = $editor->getCreators();
  130. foreach ($creators as $creator) {
  131. if ($creator->getId() === $creatorId) {
  132. $creator->create($file, $creatorId, $templateId);
  133. return $this->createToken($editorId, $file, $path);
  134. }
  135. }
  136. }
  137. throw new \RuntimeException('No creator found');
  138. }
  139. public function open(string $filePath, string $editorId = null): string {
  140. /** @var File $file */
  141. $file = $this->rootFolder->getUserFolder($this->userId)->get($filePath);
  142. if ($editorId === null) {
  143. $editorId = $this->findEditorForFile($file);
  144. }
  145. if (!array_key_exists($editorId, $this->editors)) {
  146. throw new \RuntimeException("Editor $editorId is unknown");
  147. }
  148. return $this->createToken($editorId, $file, $filePath);
  149. }
  150. private function findEditorForFile(File $file) {
  151. foreach ($this->editors as $editor) {
  152. if (in_array($file->getMimeType(), $editor->getMimetypes())) {
  153. return $editor->getId();
  154. }
  155. }
  156. throw new \RuntimeException('No default editor found for files mimetype');
  157. }
  158. public function edit(string $token): Response {
  159. try {
  160. /** @var IEditor $editor */
  161. $tokenObject = $this->getToken($token);
  162. if ($tokenObject->hasBeenAccessed()) {
  163. throw new \RuntimeException('Token has already been used and can only be used for followup requests');
  164. }
  165. $editor = $this->getEditor($tokenObject->getEditor());
  166. $this->accessToken($token);
  167. } catch (Throwable $throwable) {
  168. $this->invalidateToken($token);
  169. return new NotFoundResponse();
  170. }
  171. return $editor->open($tokenObject);
  172. }
  173. public function editSecure(File $file, string $editorId): TemplateResponse {
  174. // TODO: Implementation in follow up
  175. }
  176. private function getEditor($editorId): IEditor {
  177. if (!array_key_exists($editorId, $this->editors)) {
  178. throw new \RuntimeException('No editor found');
  179. }
  180. return $this->editors[$editorId];
  181. }
  182. public function getToken(string $token): IToken {
  183. $query = $this->connection->getQueryBuilder();
  184. $query->select('*')->from(self::TABLE_TOKENS)
  185. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  186. $result = $query->execute();
  187. if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
  188. return new Token($this, $tokenRow);
  189. }
  190. throw new \RuntimeException('Failed to validate the token');
  191. }
  192. public function cleanup(): int {
  193. $query = $this->connection->getQueryBuilder();
  194. $query->delete(self::TABLE_TOKENS)
  195. ->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
  196. return $query->execute();
  197. }
  198. public function refreshToken(string $token): bool {
  199. $query = $this->connection->getQueryBuilder();
  200. $query->update(self::TABLE_TOKENS)
  201. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  202. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  203. $result = $query->execute();
  204. return $result !== 0;
  205. }
  206. public function invalidateToken(string $token): bool {
  207. $query = $this->connection->getQueryBuilder();
  208. $query->delete(self::TABLE_TOKENS)
  209. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  210. $result = $query->execute();
  211. return $result !== 0;
  212. }
  213. public function accessToken(string $token): bool {
  214. $query = $this->connection->getQueryBuilder();
  215. $query->update(self::TABLE_TOKENS)
  216. ->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
  217. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  218. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  219. $result = $query->execute();
  220. return $result !== 0;
  221. }
  222. public function invokeTokenScope($userId): void {
  223. \OC_User::setIncognitoMode(true);
  224. \OC_User::setUserId($userId);
  225. }
  226. public function createToken($editorId, File $file, string $filePath, IShare $share = null): string {
  227. $token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
  228. $query = $this->connection->getQueryBuilder();
  229. $query->insert(self::TABLE_TOKENS)
  230. ->values([
  231. 'token' => $query->createNamedParameter($token),
  232. 'editor_id' => $query->createNamedParameter($editorId),
  233. 'file_id' => $query->createNamedParameter($file->getId()),
  234. 'file_path' => $query->createNamedParameter($filePath),
  235. 'user_id' => $query->createNamedParameter($this->userId),
  236. 'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
  237. 'timestamp' => $query->createNamedParameter(time())
  238. ]);
  239. $query->execute();
  240. return $token;
  241. }
  242. /**
  243. * @param $userId
  244. * @param $fileId
  245. * @param null $filePath
  246. * @return Node
  247. * @throws NotFoundException
  248. */
  249. public function getFileForToken($userId, $fileId, $filePath = null): Node {
  250. $userFolder = $this->rootFolder->getUserFolder($userId);
  251. if ($filePath !== null) {
  252. return $userFolder->get($filePath);
  253. }
  254. $files = $userFolder->getById($fileId);
  255. if (count($files) === 0) {
  256. throw new NotFoundException('File nound found by id ' . $fileId);
  257. }
  258. return $files[0];
  259. }
  260. public function isEnabled(): bool {
  261. if (!$this->encryptionManager->isEnabled()) {
  262. return true;
  263. }
  264. try {
  265. $moduleId = $this->encryptionManager->getDefaultEncryptionModuleId();
  266. $module = $this->encryptionManager->getEncryptionModule($moduleId);
  267. /** @var \OCA\Encryption\Util $util */
  268. $util = \OC::$server->get(\OCA\Encryption\Util::class);
  269. if ($module->isReadyForUser($this->userId) && $util->isMasterKeyEnabled()) {
  270. return true;
  271. }
  272. } catch (Throwable $e) {
  273. }
  274. return false;
  275. }
  276. }