diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index 8049957c11..d5f90c237e 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -181,7 +181,8 @@ public function createFromTemplate(int $templateId, string $fileName, string $di $template = $this->templateManager->get($templateId); $urlSrc = $this->tokenManager->getUrlSrc($file); - $wopi = $this->tokenManager->generateWopiTokenForTemplate($template, $this->userId, $file->getId()); + $isGuest = $this->userId === null; + $wopi = $this->tokenManager->generateWopiTokenForTemplate($template, $file->getId(), $this->userId, $isGuest); $params = [ 'permissions' => $template->getPermissions(), @@ -394,7 +395,8 @@ public function token(int $fileId, ?string $shareToken = null, ?string $path = n ]); } - $wopi = $this->getToken($file, $share); + $isGuest = $guestName || !$this->userId; + $wopi = $this->getToken($file, $share, null, $isGuest); $this->tokenManager->setGuestName($wopi, $guestName); @@ -477,11 +479,20 @@ private function getFileForShare(IShare $share, ?int $fileId, ?string $path = nu throw new NotFoundException(); } - private function getToken(File $file, ?IShare $share = null, ?int $version = null): Wopi { + private function getToken(File $file, ?IShare $share = null, ?int $version = null, bool $isGuest = false): Wopi { // Pass through $version $templateFile = $this->templateManager->getTemplateSource($file->getId()); if ($templateFile) { - return $this->tokenManager->generateWopiTokenForTemplate($templateFile, $share?->getShareOwner() ?? $this->userId, $file->getId()); + $owneruid = $share?->getShareOwner() ?? $file->getOwner()->getUID(); + + return $this->tokenManager->generateWopiTokenForTemplate( + $templateFile, + $file->getId(), + $owneruid, + $isGuest, + false, + $share?->getPermissions() + ); } return $this->tokenManager->generateWopiToken($this->getWopiFileId($file->getId(), $version), $share?->getToken(), $this->userId); diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 69b3a4081c..4d0824cd9e 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -560,7 +560,8 @@ public function postFile(string $fileId, string $access_token): JSONResponse { // Unless the editor is empty (public link) we modify the files as the current editor $editor = $wopi->getEditorUid(); - if ($editor === null && !$wopi->isRemoteToken()) { + $isPublic = $editor === null && !$wopi->isRemoteToken(); + if ($isPublic) { $editor = $wopi->getOwnerUid(); } @@ -573,16 +574,10 @@ public function postFile(string $fileId, string $access_token): JSONResponse { $file = $this->getFileForWopiToken($wopi); $suggested = $this->request->getHeader('X-WOPI-RequestedName'); - $suggested = mb_convert_encoding($suggested, 'utf-8', 'utf-7') . '.' . $file->getExtension(); - if (strpos($suggested, '.') === 0) { - $path = dirname($file->getPath()) . '/New File' . $suggested; - } elseif (strpos($suggested, '/') !== 0) { - $path = dirname($file->getPath()) . '/' . $suggested; - } else { - $path = $userFolder->getPath() . $suggested; - } + $parent = $isPublic ? dirname($file->getPath()) : $userFolder->getPath(); + $path = $this->normalizePath($suggested, $parent); if ($path === '') { return new JSONResponse([ @@ -611,20 +606,8 @@ public function postFile(string $fileId, string $access_token): JSONResponse { $suggested = $this->request->getHeader('X-WOPI-SuggestedTarget'); $suggested = mb_convert_encoding($suggested, 'utf-8', 'utf-7'); - if ($suggested[0] === '.') { - $path = dirname($file->getPath()) . '/New File' . $suggested; - } elseif ($suggested[0] !== '/') { - $path = dirname($file->getPath()) . '/' . $suggested; - } else { - $path = $userFolder->getPath() . $suggested; - } - - if ($path === '') { - return new JSONResponse([ - 'status' => 'error', - 'message' => 'Cannot create the file' - ]); - } + $parent = $isPublic ? dirname($file->getPath()) : $userFolder->getPath(); + $path = $this->normalizePath($suggested, $parent); // create the folder first if (!$this->rootFolder->nodeExists(dirname($path))) { @@ -638,8 +621,8 @@ public function postFile(string $fileId, string $access_token): JSONResponse { $content = fopen('php://input', 'rb'); // Set the user to register the change under his name - $this->userScopeService->setUserScope($wopi->getEditorUid()); - $this->userScopeService->setFilesystemScope($wopi->getEditorUid()); + $this->userScopeService->setUserScope($editor); + $this->userScopeService->setFilesystemScope($editor); try { $this->wrappedFilesystemOperation($wopi, function () use ($file, $content) { @@ -668,6 +651,13 @@ public function postFile(string $fileId, string $access_token): JSONResponse { } } + private function normalizePath(string $path, ?string $parent = null): string { + $path = str_starts_with($path, '/') ? $path : '/' . $path; + $parent = is_null($parent) ? '' : rtrim($parent, '/'); + + return $parent . $path; + } + private function lock(Wopi $wopi, string $lock): JSONResponse { try { $lock = $this->lockManager->lock(new LockContext( diff --git a/lib/TokenManager.php b/lib/TokenManager.php index 674fd563e7..c4a8636354 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -26,6 +26,7 @@ use OCP\Share\IManager; use OCP\Share\IShare; use OCP\Util; +use Psr\Log\LoggerInterface; class TokenManager { public function __construct( @@ -39,6 +40,7 @@ public function __construct( private Helper $helper, private PermissionManager $permissionManager, private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger, ) { } @@ -105,7 +107,7 @@ public function generateWopiToken(string $fileId, ?string $shareToken = null, ?s // no active user login while generating the token // this is required during WopiPutRelativeFile if (is_null($editoruid)) { - \OC::$server->getLogger()->warning('Generating token for SaveAs without editoruid'); + $this->logger->warning('Generating token for SaveAs without editoruid'); $updatable = true; } else { // Make sure we use the user folder if available since fetching all files by id from the root might be expensive @@ -188,10 +190,17 @@ public function upgradeFromDirectInitiator(Direct $direct, Wopi $wopi) { return $wopi; } - public function generateWopiTokenForTemplate(File $templateFile, ?string $userId, int $targetFileId, bool $direct = false): Wopi { - $owneruid = $userId; - $editoruid = $userId; - $rootFolder = $this->rootFolder->getUserFolder($editoruid); + public function generateWopiTokenForTemplate( + File $templateFile, + int $targetFileId, + string $owneruid, + bool $isGuest, + bool $direct = false, + ?int $sharePermissions = null, + ): Wopi { + $editoruid = $isGuest ? null : $owneruid; + + $rootFolder = $this->rootFolder->getUserFolder($owneruid); $targetFile = $rootFolder->getFirstNodeById($targetFileId); if (!$targetFile instanceof File) { throw new NotFoundException(); @@ -202,12 +211,26 @@ public function generateWopiTokenForTemplate(File $templateFile, ?string $userId throw new NotPermittedException(); } - $updatable = $targetFile->isUpdateable() && $this->permissionManager->userCanEdit($editoruid); + $updatable = $targetFile->isUpdateable(); + if (!is_null($sharePermissions)) { + $shareUpdatable = (bool)($sharePermissions & \OCP\Constants::PERMISSION_UPDATE); + $updatable = $updatable && $shareUpdatable; + } $serverHost = $this->urlGenerator->getAbsoluteURL('/'); - return $this->wopiMapper->generateFileToken($targetFile->getId(), $owneruid, $editoruid, 0, $updatable, $serverHost, null, - false, $direct, $templateFile->getId()); + return $this->wopiMapper->generateFileToken( + $targetFile->getId(), + $owneruid, + $editoruid, + 0, + $updatable, + $serverHost, + $isGuest ? '' : null, + false, + $direct, + $templateFile->getId() + ); } public function newInitiatorToken($sourceServer, ?Node $node = null, $shareToken = null, bool $direct = false, $userId = null): Wopi { diff --git a/tests/features/bootstrap/WopiContext.php b/tests/features/bootstrap/WopiContext.php index 4749563882..9b5a2f3a1e 100644 --- a/tests/features/bootstrap/WopiContext.php +++ b/tests/features/bootstrap/WopiContext.php @@ -13,6 +13,7 @@ use GuzzleHttp\Psr7\Utils; use JuliusHaertl\NextcloudBehat\Context\FilesContext; use JuliusHaertl\NextcloudBehat\Context\ServerContext; +use JuliusHaertl\NextcloudBehat\Context\SharingContext; use PHPUnit\Framework\Assert; class WopiContext implements Context { @@ -20,6 +21,8 @@ class WopiContext implements Context { private $serverContext; /** @var FilesContext */ private $filesContext; + /** @var SharingContext */ + private $sharingContext; private $downloadedFile; private $response; @@ -38,6 +41,7 @@ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); $this->serverContext = $environment->getContext(ServerContext::class); $this->filesContext = $environment->getContext(FilesContext::class); + $this->sharingContext = $environment->getContext(SharingContext::class); } public function getWopiEndpointBaseUrl() { @@ -87,6 +91,32 @@ public function collaboraPuts($source) { } } + /** + * @Then /^Create new document as guest with file name "([^"]*)"$/ + */ + public function createDocumentAsGuest(string $name) { + $client = new Client(); + $options = [ + 'body' => json_encode([ + 'directoryPath' => '/', + 'fileName' => $name, + 'mimeType' => 'application/vnd.oasis.opendocument.text', + 'shareToken' => $this->sharingContext->getLastShareData()['token'], + 'templateId' => 0, + ]), + 'headers' => [ + 'Content-Type' => 'application/json', + 'OCS-ApiRequest' => 'true' + ], + ]; + + try { + $this->response = $client->post($this->getWopiEndpointBaseUrl() . 'ocs/v2.php/apps/richdocuments/api/v1/file', $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + /** * @Then /^the WOPI HTTP status code should be "([^"]*)"$/ * @param int $statusCode diff --git a/tests/features/wopi.feature b/tests/features/wopi.feature index 16d23a288a..522e1d2fd7 100644 --- a/tests/features/wopi.feature +++ b/tests/features/wopi.feature @@ -342,3 +342,19 @@ Feature: WOPI | UserFriendlyName | user2-displayname | And Collabora downloads the file And Collabora downloads the file and it is equal to "./../emptyTemplates/template.ods" + + Scenario: Save as guest user to owner root + Given as user "user1" + And User "user1" creates a folder "SharedFolder" + And as "user1" create a share with + | path | /SharedFolder | + | shareType | 3 | + And Updating last share with + | permissions | 31 | + And Create new document as guest with file name "some-guest-document.odt" + And as "user1" the file "/SharedFolder/some-guest-document.odt" exists + And a guest opens the file "some-guest-document.odt" of the shared link + And Collabora fetches checkFileInfo + And Collabora saves the content of "./../emptyTemplates/template.ods" as "/saved-as-guest-document.odt" + And as "user1" the file "/SharedFolder/saved-as-guest-document.odt" exists + And as "user1" the file "/saved-as-guest-document.odt" does not exist