diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index da85631f4b9..dcff792ea42 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -38,6 +38,7 @@ 'OCA\\Text\\Listeners\\BeforeAssistantNotificationListener' => $baseDir . '/../lib/Listeners/BeforeAssistantNotificationListener.php', 'OCA\\Text\\Listeners\\BeforeNodeDeletedListener' => $baseDir . '/../lib/Listeners/BeforeNodeDeletedListener.php', 'OCA\\Text\\Listeners\\BeforeNodeRenamedListener' => $baseDir . '/../lib/Listeners/BeforeNodeRenamedListener.php', + 'OCA\\Text\\Listeners\\BeforeNodeWrittenListener' => $baseDir . '/../lib/Listeners/BeforeNodeWrittenListener.php', 'OCA\\Text\\Listeners\\FilesLoadAdditionalScriptsListener' => $baseDir . '/../lib/Listeners/FilesLoadAdditionalScriptsListener.php', 'OCA\\Text\\Listeners\\FilesSharingLoadAdditionalScriptsListener' => $baseDir . '/../lib/Listeners/FilesSharingLoadAdditionalScriptsListener.php', 'OCA\\Text\\Listeners\\LoadEditorListener' => $baseDir . '/../lib/Listeners/LoadEditorListener.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index 8732ef4a1dc..a172981b0ef 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -53,6 +53,7 @@ class ComposerStaticInitText 'OCA\\Text\\Listeners\\BeforeAssistantNotificationListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeAssistantNotificationListener.php', 'OCA\\Text\\Listeners\\BeforeNodeDeletedListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeNodeDeletedListener.php', 'OCA\\Text\\Listeners\\BeforeNodeRenamedListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeNodeRenamedListener.php', + 'OCA\\Text\\Listeners\\BeforeNodeWrittenListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeNodeWrittenListener.php', 'OCA\\Text\\Listeners\\FilesLoadAdditionalScriptsListener' => __DIR__ . '/..' . '/../lib/Listeners/FilesLoadAdditionalScriptsListener.php', 'OCA\\Text\\Listeners\\FilesSharingLoadAdditionalScriptsListener' => __DIR__ . '/..' . '/../lib/Listeners/FilesSharingLoadAdditionalScriptsListener.php', 'OCA\\Text\\Listeners\\LoadEditorListener' => __DIR__ . '/..' . '/../lib/Listeners/LoadEditorListener.php', diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 41f60adb579..07e37032863 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -32,6 +32,7 @@ use OCA\Text\Listeners\BeforeAssistantNotificationListener; use OCA\Text\Listeners\BeforeNodeDeletedListener; use OCA\Text\Listeners\BeforeNodeRenamedListener; +use OCA\Text\Listeners\BeforeNodeWrittenListener; use OCA\Text\Listeners\FilesLoadAdditionalScriptsListener; use OCA\Text\Listeners\FilesSharingLoadAdditionalScriptsListener; use OCA\Text\Listeners\LoadEditorListener; @@ -51,6 +52,7 @@ use OCP\DirectEditing\RegisterDirectEditorEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\BeforeNodeWrittenEvent; use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Template\ITemplateManager; use OCP\Files\Template\TemplateFileCreator; @@ -71,6 +73,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadEditor::class, LoadEditorListener::class); // for attachments $context->registerEventListener(NodeCopiedEvent::class, NodeCopiedListener::class); + $context->registerEventListener(BeforeNodeWrittenEvent::class, BeforeNodeWrittenListener::class); $context->registerEventListener(BeforeNodeRenamedEvent::class, BeforeNodeRenamedListener::class); $context->registerEventListener(BeforeNodeDeletedEvent::class, BeforeNodeDeletedListener::class); $context->registerEventListener(AddMissingIndicesEvent::class, AddMissingIndicesListener::class); diff --git a/lib/Command/ResetDocument.php b/lib/Command/ResetDocument.php index 65a614e2f1a..0864ccb68ec 100644 --- a/lib/Command/ResetDocument.php +++ b/lib/Command/ResetDocument.php @@ -23,9 +23,7 @@ namespace OCA\Text\Command; -use OCA\Text\Db\DocumentMapper; -use OCA\Text\Db\SessionMapper; -use OCA\Text\Db\StepMapper; +use OCA\Text\Exception\DocumentHasUnsavedChangesException; use OCA\Text\Service\DocumentService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -34,33 +32,26 @@ class ResetDocument extends Command { protected DocumentService $documentService; - protected DocumentMapper $documentMapper; - protected StepMapper $stepMapper; - protected SessionMapper $sessionMapper; - public function __construct(DocumentService $documentService, DocumentMapper $documentMapper, StepMapper $stepMapper, SessionMapper $sessionMapper) { + public function __construct(DocumentService $documentService) { parent::__construct(); - $this->documentService = $documentService; - $this->documentMapper = $documentMapper; - $this->stepMapper = $stepMapper; - $this->sessionMapper = $sessionMapper; } protected function configure(): void { $this ->setName('text:reset') - ->setDescription('Reset a text document') + ->setDescription('Reset a text document session to the current file content') ->addArgument( 'file-id', InputArgument::REQUIRED, - 'File id of the document to rest' + 'File id of the document to reset' ) ->addOption( - 'full', + 'force', 'f', null, - 'Drop all existing steps and use the currently saved version' + 'Reset the document session even with unsaved changes' ) ; } @@ -72,27 +63,23 @@ protected function configure(): void { */ protected function execute(InputInterface $input, OutputInterface $output): int { $fileId = $input->getArgument('file-id'); - $fullReset = $input->getOption('full'); + $fullReset = $input->getOption('force'); if ($fullReset) { - $output->writeln('Full document reset'); + $output->writeln('Force-reset the document session for file ' . $fileId); $this->documentService->resetDocument($fileId, true); return 0; - } else { - $output->writeln('Trying to restore to last saved version'); - $document = $this->documentMapper->find($fileId); - $deleted = $this->stepMapper->deleteAfterVersion($fileId, $document->getLastSavedVersion()); - if ($deleted > 0) { - $this->sessionMapper->deleteByDocumentId($fileId); - $output->writeln('Reverted document to the last saved version'); - - return 0; - } else { - $output->writeln('Failed revert changes that are newer than the last saved version'); - } + } + $output->writeln('Reset the document session for file ' . $fileId); + try { + $this->documentService->resetDocument($fileId); + } catch (DocumentHasUnsavedChangesException) { + $output->writeln('Not resetting due to unsaved changes'); return 1; } + + return 0; } } diff --git a/lib/Cron/Cleanup.php b/lib/Cron/Cleanup.php index 19933cfc17c..e374e742f10 100644 --- a/lib/Cron/Cleanup.php +++ b/lib/Cron/Cleanup.php @@ -28,7 +28,7 @@ namespace OCA\Text\Cron; -use OCA\Text\Service\AttachmentService; +use OCA\Text\Exception\DocumentHasUnsavedChangesException; use OCA\Text\Service\DocumentService; use OCA\Text\Service\SessionService; use OCP\AppFramework\Utility\ITimeFactory; @@ -39,26 +39,22 @@ class Cleanup extends TimedJob { private SessionService $sessionService; private DocumentService $documentService; private LoggerInterface $logger; - private AttachmentService $attachmentService; public function __construct(ITimeFactory $time, SessionService $sessionService, DocumentService $documentService, - AttachmentService $attachmentService, LoggerInterface $logger) { parent::__construct($time); $this->sessionService = $sessionService; $this->documentService = $documentService; - $this->attachmentService = $attachmentService; $this->logger = $logger; $this->setInterval(SessionService::SESSION_VALID_TIME); } /** * @param array $argument - * @return void */ - protected function run($argument) { + protected function run($argument): void { $this->logger->debug('Run cleanup job for text documents'); $documents = $this->documentService->getAll(); foreach ($documents as $document) { @@ -69,11 +65,10 @@ protected function run($argument) { continue; } - if ($this->documentService->hasUnsavedChanges($document)) { - continue; + try { + $this->documentService->resetDocument($document->getId()); + } catch (DocumentHasUnsavedChangesException) { } - - $this->documentService->resetDocument($document->getId()); } $this->logger->debug('Run cleanup job for text sessions'); diff --git a/lib/Listeners/BeforeNodeDeletedListener.php b/lib/Listeners/BeforeNodeDeletedListener.php index 2f1143b4e0d..a1e0cc0864b 100644 --- a/lib/Listeners/BeforeNodeDeletedListener.php +++ b/lib/Listeners/BeforeNodeDeletedListener.php @@ -26,6 +26,7 @@ namespace OCA\Text\Listeners; use OCA\Text\Service\AttachmentService; +use OCA\Text\Service\DocumentService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; @@ -35,10 +36,13 @@ * @template-implements IEventListener */ class BeforeNodeDeletedListener implements IEventListener { - private $attachmentService; + private AttachmentService $attachmentService; + private DocumentService $documentService; - public function __construct(AttachmentService $attachmentService) { + public function __construct(AttachmentService $attachmentService, + DocumentService $documentService) { $this->attachmentService = $attachmentService; + $this->documentService = $documentService; } public function handle(Event $event): void { @@ -48,6 +52,7 @@ public function handle(Event $event): void { $node = $event->getNode(); if ($node instanceof File && $node->getMimeType() === 'text/markdown') { $this->attachmentService->deleteAttachments($node); + $this->documentService->resetDocument($node->getId(), true); } } } diff --git a/lib/Listeners/BeforeNodeWrittenListener.php b/lib/Listeners/BeforeNodeWrittenListener.php new file mode 100644 index 00000000000..ec6f28e48e5 --- /dev/null +++ b/lib/Listeners/BeforeNodeWrittenListener.php @@ -0,0 +1,56 @@ + + * + * @author Jonas + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Text\Listeners; + +use OCA\Text\Service\DocumentService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\BeforeNodeWrittenEvent; +use OCP\Files\File; + +/** + * @template-implements IEventListener + */ +class BeforeNodeWrittenListener implements IEventListener { + private DocumentService $documentService; + + public function __construct(DocumentService $documentService) { + $this->documentService = $documentService; + } + + public function handle(Event $event): void { + if (!$event instanceof BeforeNodeWrittenEvent) { + return; + } + $node = $event->getNode(); + if ($node instanceof File && $node->getMimeType() === 'text/markdown') { + if (!$this->documentService->isSaveFromText()) { + // Reset document session to avoid manual conflict resolution if there's no unsaved steps + $this->documentService->resetDocument($node->getId()); + } + } + } +} diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 5b1cb09d251..39944996a00 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -74,6 +74,8 @@ class DocumentService { */ public const AUTOSAVE_MINIMUM_DELAY = 10; + private bool $saveFromText = false; + private ?string $userId; private DocumentMapper $documentMapper; private SessionMapper $sessionMapper; @@ -118,6 +120,10 @@ public function getDocument(int $id): ?Document { } } + public function isSaveFromText(): bool { + return $this->saveFromText; + } + /** * @throws NotFoundException * @throws InvalidPathException @@ -389,6 +395,7 @@ public function autosave(Document $document, ?File $file, int $version, ?string ILock::TYPE_APP, Application::APP_NAME ), function () use ($file, $autoSaveDocument, $documentState) { + $this->saveFromText = true; $file->putContent($autoSaveDocument); if ($documentState) { $this->writeDocumentState($file->getId(), $documentState); @@ -425,10 +432,8 @@ public function resetDocument(int $documentId, bool $force = false): void { $this->stepMapper->deleteAll($documentId); $this->sessionMapper->deleteByDocumentId($documentId); $this->documentMapper->delete($document); + $this->getStateFile($documentId)->delete(); - if ($force) { - $this->getStateFile($documentId)->delete(); - } $this->logger->debug('document reset for ' . $documentId); } catch (DoesNotExistException|NotFoundException $e) { // Ignore if document not found or state file not found