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.

Manager.php 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
  4. *
  5. * @author Julius Härtl <jus@bitgrid.net>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. namespace OC\DirectEditing;
  24. use Doctrine\DBAL\FetchMode;
  25. use OCP\AppFramework\Http\NotFoundResponse;
  26. use OCP\AppFramework\Http\Response;
  27. use OCP\AppFramework\Http\TemplateResponse;
  28. use OCP\DB\QueryBuilder\IQueryBuilder;
  29. use OCP\DirectEditing\ACreateFromTemplate;
  30. use OCP\DirectEditing\IEditor;
  31. use \OCP\DirectEditing\IManager;
  32. use OCP\DirectEditing\IToken;
  33. use OCP\Files\File;
  34. use OCP\Files\IRootFolder;
  35. use OCP\Files\NotFoundException;
  36. use OCP\IDBConnection;
  37. use OCP\IUserSession;
  38. use OCP\Security\ISecureRandom;
  39. use OCP\Share\IShare;
  40. class Manager implements IManager {
  41. private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
  42. public const TABLE_TOKENS = 'direct_edit';
  43. /** @var IEditor[] */
  44. private $editors;
  45. /** @var IDBConnection */
  46. private $connection;
  47. /**
  48. * @var ISecureRandom
  49. */
  50. private $random;
  51. private $userId;
  52. private $rootFolder;
  53. public function __construct(
  54. ISecureRandom $random,
  55. IDBConnection $connection,
  56. IUserSession $userSession,
  57. IRootFolder $rootFolder
  58. ) {
  59. $this->random = $random;
  60. $this->connection = $connection;
  61. $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
  62. $this->rootFolder = $rootFolder;
  63. }
  64. public function registerDirectEditor(IEditor $directEditor): void {
  65. $this->editors[$directEditor->getId()] = $directEditor;
  66. }
  67. public function getEditors(): array {
  68. return $this->editors;
  69. }
  70. public function getTemplates(string $editor, string $type): array {
  71. if (!array_key_exists($editor, $this->editors)) {
  72. throw new \RuntimeException('No matching editor found');
  73. }
  74. $templates = [];
  75. foreach ($this->editors[$editor]->getCreators() as $creator) {
  76. if ($creator instanceof ACreateFromTemplate && $creator->getId() === $type) {
  77. $templates = $creator->getTemplates();
  78. }
  79. }
  80. return $templates;
  81. }
  82. public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
  83. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  84. $file = $userFolder->newFile($path);
  85. $editor = $this->getEditor($editorId);
  86. $creators = $editor->getCreators();
  87. foreach ($creators as $creator) {
  88. if ($creator->getId() === $creatorId) {
  89. $creator->create($file, $creatorId, $templateId);
  90. return $this->createToken($editorId, $file);
  91. }
  92. }
  93. throw new \RuntimeException('No creator found');
  94. }
  95. public function open(int $fileId, string $editorId = null): string {
  96. $file = $this->rootFolder->getUserFolder($this->userId)->getById($fileId);
  97. if (count($file) === 0 || !($file[0] instanceof File) || $file === null) {
  98. throw new NotFoundException();
  99. }
  100. /** @var File $file */
  101. $file = $file[0];
  102. if ($editorId === null) {
  103. $editorId = $this->findEditorForFile($file);
  104. }
  105. return $this->createToken($editorId, $file);
  106. }
  107. private function findEditorForFile(File $file) {
  108. foreach ($this->editors as $editor) {
  109. if (in_array($file->getMimeType(), $editor->getMimetypes())) {
  110. return $editor->getId();
  111. }
  112. }
  113. throw new \RuntimeException('No default editor found for files mimetype');
  114. }
  115. public function edit(string $token): Response {
  116. try {
  117. /** @var IEditor $editor */
  118. $tokenObject = $this->getToken($token);
  119. if ($tokenObject->hasBeenAccessed()) {
  120. throw new \RuntimeException('Token has already been used and can only be used for followup requests');
  121. }
  122. $editor = $this->getEditor($tokenObject->getEditor());
  123. $this->accessToken($token);
  124. } catch (\Throwable $throwable) {
  125. $this->invalidateToken($token);
  126. return new NotFoundResponse();
  127. }
  128. return $editor->open($tokenObject);
  129. }
  130. public function editSecure(File $file, string $editorId): TemplateResponse {
  131. // TODO: Implementation in follow up
  132. }
  133. private function getEditor($editorId): IEditor {
  134. if (!array_key_exists($editorId, $this->editors)) {
  135. throw new \RuntimeException('No editor found');
  136. }
  137. return $this->editors[$editorId];
  138. }
  139. public function getToken(string $token): IToken {
  140. $query = $this->connection->getQueryBuilder();
  141. $query->select('*')->from(self::TABLE_TOKENS)
  142. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  143. $result = $query->execute();
  144. if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
  145. return new Token($this, $tokenRow);
  146. }
  147. throw new \RuntimeException('Failed to validate the token');
  148. }
  149. public function cleanup(): int {
  150. $query = $this->connection->getQueryBuilder();
  151. $query->delete(self::TABLE_TOKENS)
  152. ->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
  153. return $query->execute();
  154. }
  155. public function refreshToken(string $token): bool {
  156. $query = $this->connection->getQueryBuilder();
  157. $query->update(self::TABLE_TOKENS)
  158. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  159. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  160. $result = $query->execute();
  161. return $result !== 0;
  162. }
  163. public function invalidateToken(string $token): bool {
  164. $query = $this->connection->getQueryBuilder();
  165. $query->delete(self::TABLE_TOKENS)
  166. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  167. $result = $query->execute();
  168. return $result !== 0;
  169. }
  170. public function accessToken(string $token): bool {
  171. $query = $this->connection->getQueryBuilder();
  172. $query->update(self::TABLE_TOKENS)
  173. ->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
  174. ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  175. ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
  176. $result = $query->execute();
  177. return $result !== 0;
  178. }
  179. public function invokeTokenScope($userId): void {
  180. \OC_User::setIncognitoMode(true);
  181. \OC_User::setUserId($userId);
  182. }
  183. public function createToken($editorId, File $file, IShare $share = null): string {
  184. $token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
  185. $query = $this->connection->getQueryBuilder();
  186. $query->insert(self::TABLE_TOKENS)
  187. ->values([
  188. 'token' => $query->createNamedParameter($token),
  189. 'editor_id' => $query->createNamedParameter($editorId),
  190. 'file_id' => $query->createNamedParameter($file->getId()),
  191. 'user_id' => $query->createNamedParameter($this->userId),
  192. 'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
  193. 'timestamp' => $query->createNamedParameter(time())
  194. ]);
  195. $query->execute();
  196. return $token;
  197. }
  198. public function getFileForToken($userId, $fileId) {
  199. $userFolder = $this->rootFolder->getUserFolder($userId);
  200. return $userFolder->getById($fileId)[0];
  201. }
  202. }