Manager.php 11 KB

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