%PDF- %PDF-
Direktori : /proc/thread-self/root/www/varak.net/nextcloud.varak.net/apps/text/lib/Service/ |
Current File : //proc/thread-self/root/www/varak.net/nextcloud.varak.net/apps/text/lib/Service/ApiService.php |
<?php declare(strict_types=1); /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Text\Service; use Exception; use InvalidArgumentException; use OCA\Files_Sharing\SharedStorage; use OCA\NotifyPush\Queue\IQueue; use OCA\Text\AppInfo\Application; use OCA\Text\Db\Document; use OCA\Text\Db\Session; use OCA\Text\Exception\DocumentSaveConflictException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Constants; use OCP\Files\AlreadyExistsException; use OCP\Files\InvalidPathException; use OCP\Files\Lock\ILock; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IL10N; use OCP\IRequest; use OCP\Lock\LockedException; use OCP\Share\IShare; use Psr\Log\LoggerInterface; class ApiService { public function __construct( private IRequest $request, private ConfigService $configService, private SessionService $sessionService, private DocumentService $documentService, private EncodingService $encodingService, private LoggerInterface $logger, private IL10N $l10n, private ?string $userId, private ?IQueue $queue, ) { } public function create(?int $fileId = null, ?string $filePath = null, ?string $baseVersionEtag = null, ?string $token = null, ?string $guestName = null): DataResponse { try { if ($token !== null) { $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); /* * Check if we have proper read access (files drop) * If not then well 404 it is. */ try { $this->documentService->checkSharePermissions($token, Constants::PERMISSION_READ); } catch (NotFoundException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (NotPermittedException $e) { return new DataResponse(['error' => $this->l10n->t('This file cannot be displayed as download is disabled by the share')], Http::STATUS_NOT_FOUND); } } elseif ($fileId !== null) { try { $file = $this->documentService->getFileById($fileId, $this->userId); } catch (NotFoundException|NotPermittedException $e) { $this->logger->error('No permission to access this file', [ 'exception' => $e ]); return new DataResponse([ 'error' => $this->l10n->t('File not found') ], Http::STATUS_NOT_FOUND); } } else { return new DataResponse(['error' => 'No valid file argument provided'], Http::STATUS_PRECONDITION_FAILED); } $storage = $file->getStorage(); // Block using text for disabled download internal shares if ($storage->instanceOfStorage(SharedStorage::class)) { /** @var IShare $share */ $share = $storage->getShare(); $shareAttribtues = $share->getAttributes(); if ($shareAttribtues !== null && $shareAttribtues->getAttribute('permissions', 'download') === false) { return new DataResponse(['error' => $this->l10n->t('This file cannot be displayed as download is disabled by the share')], Http::STATUS_FORBIDDEN); } } $readOnly = $this->documentService->isReadOnly($file, $token); $this->sessionService->removeInactiveSessionsWithoutSteps($file->getId()); $document = $this->documentService->getDocument($file->getId()); $freshSession = $document === null; if ($baseVersionEtag !== null && $baseVersionEtag !== $document?->getBaseVersionEtag()) { return new DataResponse(['error' => $this->l10n->t('Editing session has expired. Please reload the page.')], Http::STATUS_PRECONDITION_FAILED); } if ($freshSession) { $this->logger->info('Create new document of ' . $file->getId()); try { $document = $this->documentService->createDocument($file); } catch (AlreadyExistsException) { $freshSession = false; $document = $this->documentService->getDocument($file->getId()); } } else { $this->logger->info('Keep previous document of ' . $file->getId()); } } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return new DataResponse(['error' => 'Failed to create the document session'], Http::STATUS_INTERNAL_SERVER_ERROR); } /** @var Document $document */ $session = $this->sessionService->initSession($document->getId(), $guestName); $documentState = null; $content = null; if ($freshSession) { $this->logger->debug('Starting a fresh editing session for ' . $file->getId()); $content = $this->loadContent($file); } else { $this->logger->debug('Loading existing session for ' . $file->getId()); try { $stateFile = $this->documentService->getStateFile($document->getId()); $documentState = $stateFile->getContent(); $this->logger->debug('Existing document, state file loaded ' . $file->getId()); } catch (NotFoundException $e) { $this->logger->debug('Existing document, but state file not found for ' . $file->getId()); // If we have no state file we need to load the content from the file // On the client side we use this to initialize a idempotent initial y.js document $content = $this->loadContent($file); } } $lockInfo = $this->documentService->getLockInfo($file); if ($lockInfo && $lockInfo->getType() === ILock::TYPE_APP && $lockInfo->getOwner() === Application::APP_NAME) { $lockInfo = null; } if (!$readOnly) { $isLocked = $this->documentService->lock($file->getId()); if (!$isLocked) { $readOnly = true; } } return new DataResponse([ 'document' => $document, 'session' => array_merge($session->jsonSerialize(), ['displayName' => $this->sessionService->getNameForSession($session)]), 'readOnly' => $readOnly, 'content' => $content, 'documentState' => $documentState, 'lock' => $lockInfo, ]); } public function close(int $documentId, int $sessionId, string $sessionToken): DataResponse { $this->sessionService->closeSession($documentId, $sessionId, $sessionToken); $this->sessionService->removeInactiveSessionsWithoutSteps($documentId); $activeSessions = $this->sessionService->getActiveSessions($documentId); if (count($activeSessions) === 0) { $this->documentService->unlock($documentId); } return new DataResponse([]); } /** * @throws NotFoundException */ public function push(Session $session, Document $document, int $version, array $steps, string $awareness, ?string $token = null): DataResponse { try { $session = $this->sessionService->updateSessionAwareness($session, $awareness); } catch (DoesNotExistException $e) { // Session was removed in the meantime. #3875 return new DataResponse(['error' => $this->l10n->t('Editing session has expired. Please reload the page.')], Http::STATUS_PRECONDITION_FAILED); } if (empty($steps)) { return new DataResponse([]); } try { $result = $this->documentService->addStep($document, $session, $steps, $version, $token); $this->addToPushQueue($document, [$awareness, ...array_values($steps)]); } catch (InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_UNPROCESSABLE_ENTITY); } catch (DoesNotExistException|NotPermittedException) { // Either no write access or session was removed in the meantime (#3875). return new DataResponse(['error' => $this->l10n->t('Editing session has expired. Please reload the page.')], Http::STATUS_PRECONDITION_FAILED); } return new DataResponse($result); } private function addToPushQueue(Document $document, array $steps): void { if ($this->queue === null || !$this->configService->isNotifyPushSyncEnabled()) { return; } $sessions = $this->sessionService->getActiveSessions($document->getId()); $userIds = array_values(array_filter(array_unique( array_map(fn ($session): ?string => $session['userId'], $sessions) ))); foreach ($userIds as $userId) { $this->queue->push('notify_custom', [ 'user' => $userId, 'message' => 'text_steps', 'body' => [ 'documentId' => $document->getId(), 'steps' => $steps, ], ]); } } public function sync(Session $session, Document $document, int $version = 0, ?string $shareToken = null): DataResponse { $documentId = $session->getDocumentId(); $result = []; try { $result = [ 'steps' => $this->documentService->getSteps($documentId, $version), 'sessions' => $this->sessionService->getAllSessions($documentId), 'document' => $document, ]; // ensure file is still present and accessible $file = $this->documentService->getFileForSession($session, $shareToken); $this->documentService->assertNoOutsideConflict($document, $file); } catch (NotPermittedException|NotFoundException|InvalidPathException $e) { $this->logger->info($e->getMessage(), ['exception' => $e]); return new DataResponse([ 'message' => 'File not found' ], Http::STATUS_NOT_FOUND); } catch (DoesNotExistException $e) { $this->logger->info($e->getMessage(), ['exception' => $e]); return new DataResponse([ 'message' => 'Document no longer exists' ], Http::STATUS_NOT_FOUND); } catch (DocumentSaveConflictException) { try { /** @psalm-suppress PossiblyUndefinedVariable */ $result['outsideChange'] = $file->getContent(); } catch (LockedException) { // Ignore locked exception since it might happen due to an autosave action happening at the same time } } return new DataResponse($result, isset($result['outsideChange']) ? Http::STATUS_CONFLICT : Http::STATUS_OK); } public function save(Session $session, Document $document, int $version = 0, ?string $autosaveContent = null, ?string $documentState = null, bool $force = false, bool $manualSave = false, ?string $shareToken = null): DataResponse { try { $file = $this->documentService->getFileForSession($session, $shareToken); } catch (NotPermittedException|NotFoundException $e) { $this->logger->info($e->getMessage(), ['exception' => $e]); return new DataResponse([ 'message' => 'File not found' ], Http::STATUS_NOT_FOUND); } catch (DoesNotExistException $e) { $this->logger->info($e->getMessage(), ['exception' => $e]); return new DataResponse([ 'message' => 'Document no longer exists' ], Http::STATUS_NOT_FOUND); } $result = []; try { $result['document'] = $this->documentService->autosave($document, $file, $version, $autosaveContent, $documentState, $force, $manualSave, $shareToken); } catch (DocumentSaveConflictException) { try { $result['outsideChange'] = $file->getContent(); } catch (LockedException) { // Ignore locked exception since it might happen due to an autosave action happening at the same time } } catch (NotFoundException) { return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return new DataResponse([ 'message' => 'Failed to autosave document' ], Http::STATUS_INTERNAL_SERVER_ERROR); } return new DataResponse($result, isset($result['outsideChange']) ? Http::STATUS_CONFLICT : Http::STATUS_OK); } public function updateSession(Session $session, string $guestName): DataResponse { return new DataResponse($this->sessionService->updateSession($session, $guestName)); } private function loadContent(\OCP\Files\File $file): ?string { try { $content = $file->getContent(); $content = $this->encodingService->encodeToUtf8($content); if ($content === null) { $this->logger->warning('Failed to encode file to UTF8. File ID: ' . $file->getId()); } } catch (NotFoundException $e) { $this->logger->warning($e->getMessage(), ['exception' => $e]); $content = null; } return $content; } }