diff options
-rw-r--r-- | appinfo/routes.php | 2 | ||||
-rw-r--r-- | docs/mobile_editor.md | 7 | ||||
-rw-r--r-- | lib/Controller/DocumentController.php | 8 | ||||
-rw-r--r-- | lib/Controller/OCSController.php | 47 | ||||
-rw-r--r-- | lib/Controller/WopiController.php | 2 | ||||
-rw-r--r-- | lib/Db/Wopi.php | 2 | ||||
-rw-r--r-- | lib/Helper.php | 2 | ||||
-rw-r--r-- | lib/TokenManager.php | 48 | ||||
-rw-r--r-- | src/document.js | 11 | ||||
-rw-r--r-- | src/helpers/guestName.js | 13 | ||||
-rw-r--r-- | tests/features/bootstrap/DirectContext.php | 60 | ||||
-rw-r--r-- | tests/features/bootstrap/RichDocumentsContext.php | 10 | ||||
-rw-r--r-- | tests/features/bootstrap/WopiContext.php | 4 | ||||
-rw-r--r-- | tests/features/direct.feature | 87 |
14 files changed, 253 insertions, 50 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 02c0baf8..877616dd 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -73,6 +73,8 @@ return [ ['name' => 'OCS#getTemplates', 'url' => '/api/v1/templates/{type}', 'verb' => 'GET'], ['name' => 'OCS#createFromTemplate', 'url' => '/api/v1/templates/new', 'verb' => 'POST'], + ['name' => 'OCS#updateGuestName', 'url' => '/api/v1/wopi/guestname', 'verb' => 'POST'], + ['name' => 'Federation#index', 'url' => '/api/v1/federation', 'verb' => 'GET'], ['name' => 'Federation#remoteWopiToken', 'url' => '/api/v1/federation', 'verb' => 'POST'], ['name' => 'Federation#initiatorUser', 'url' => '/api/v1/federation/user', 'verb' => 'POST'], diff --git a/docs/mobile_editor.md b/docs/mobile_editor.md index 343d4e78..158192f3 100644 --- a/docs/mobile_editor.md +++ b/docs/mobile_editor.md @@ -35,6 +35,7 @@ document: The returned xml or json will have an url to open in a webview. -The user will join the session as a guest but with their user details provided by their own -instance. - +The endpoint can be used with or without user credentials for the Nextcloud server. +For anonymous requests the webview will ask the user for a guest name on writable +files. When requesting a link as an authenticated user, the user will join the +document as a guest but with their user details provided by their own instance. diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index 664d17a3..bca32ffa 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -360,11 +360,9 @@ class DocumentController extends Controller { 'userId' => $this->uid, ]; - if ($this->uid !== null || ($share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE) === 0 || $this->helper->getGuestName() !== null) { - list($urlSrc, $token) = $this->tokenManager->getToken($item->getId(), $shareToken, $this->uid); - $params['token'] = $token; - $params['urlsrc'] = $urlSrc; - } + list($urlSrc, $token) = $this->tokenManager->getToken($item->getId(), $shareToken, $this->uid); + $params['token'] = $token; + $params['urlsrc'] = $urlSrc; $response = new TemplateResponse('richdocuments', 'documents', $params, 'base'); $this->setupPolicy($response); diff --git a/lib/Controller/OCSController.php b/lib/Controller/OCSController.php index d1eb7181..52bcaa88 100644 --- a/lib/Controller/OCSController.php +++ b/lib/Controller/OCSController.php @@ -24,9 +24,12 @@ namespace OCA\Richdocuments\Controller; use OCA\Richdocuments\Db\DirectMapper; +use OCA\Richdocuments\Db\Wopi; +use OCA\Richdocuments\Helper; use OCA\Richdocuments\Service\FederationService; use OCA\Richdocuments\TemplateManager; use OCA\Richdocuments\TokenManager; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; @@ -150,16 +153,22 @@ class OCSController extends \OCP\AppFramework\OCSController { $wopi = $this->tokenManager->newInitiatorToken($host, null, $shareToken, true, $this->userId); $client = \OC::$server->getHTTPClientService()->newClient(); - $response = $client->post(rtrim($host, '/') . '/ocs/v2.php/apps/richdocuments/api/v1/direct/share/initiator?format=json', [ - 'body' => [ - 'initiatorServer' => \OC::$server->getURLGenerator()->getAbsoluteURL(''), - 'initiatorToken' => $wopi->getToken(), - 'shareToken' => $shareToken, - 'path' => $path, - 'password' => $password - ], - 'timeout' => 30 - ]); + try { + $response = $client->post(rtrim($host, '/') . '/ocs/v2.php/apps/richdocuments/api/v1/direct/share/initiator?format=json', [ + 'body' => [ + 'initiatorServer' => \OC::$server->getURLGenerator()->getAbsoluteURL(''), + 'initiatorToken' => $wopi->getToken(), + 'shareToken' => $shareToken, + 'path' => $path, + 'password' => $password + ], + 'timeout' => 30 + ]); + } catch (\Exception $e) { + $response = new DataResponse([], HTTP::STATUS_FORBIDDEN); + $response->throttle(); + return $response; + } $url = \json_decode($response->getBody(), true)['ocs']['data']['url']; return new DataResponse([ @@ -246,6 +255,24 @@ class OCSController extends \OCP\AppFramework\OCSController { } /** + * Generate a direct editing link for a file in a public share to open with the current user + * + * @NoAdminRequired + * @BruteForceProtection(action=richdocumentsCreatePublic) + * @PublicPage + */ + public function updateGuestName(string $access_token, string $guestName): DataResponse { + try { + $this->tokenManager->updateGuestName($access_token, $guestName); + return new DataResponse([], Http::STATUS_OK); + } catch (DoesNotExistException $e) { + $response = new DataResponse([], Http::STATUS_FORBIDDEN); + $response->throttle(); + return $response; + } + } + + /** * @NoAdminRequired * * @param string $type The template type diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 20cec6d9..d01db98f 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -271,7 +271,7 @@ class WopiController extends Controller { return $response; } - $response['UserFriendlyName'] = $initiator->getGuestDisplayname() . ' (Guest)'; + $response['UserFriendlyName'] = $this->tokenManager->prepareGuestName($initiator->getGuestDisplayname()); if ($initiator->hasTemplateId()) { $templateUrl = $wopi->getRemoteServer() . '/index.php/apps/richdocuments/wopi/template/' . $initiator->getTemplateId() . '?access_token=' . $initiator->getToken(); $response['TemplateSource'] = $templateUrl; diff --git a/lib/Db/Wopi.php b/lib/Db/Wopi.php index 11d7d4ed..31250603 100644 --- a/lib/Db/Wopi.php +++ b/lib/Db/Wopi.php @@ -51,7 +51,7 @@ use OCP\AppFramework\Db\Entity; * @method string getRemoteServerToken() * @method void setExpiry(int $expiry) * @method int getExpiry() - * @method void setGuestDisplayname(string $token) + * @method void setGuestDisplayname(string $guestDisplayName) * @method string getGuestDisplayname() * @method void setTemplateDestination(int $fileId) * @method int getTemplateDestination() diff --git a/lib/Helper.php b/lib/Helper.php index aa48dc35..6b96d3a7 100644 --- a/lib/Helper.php +++ b/lib/Helper.php @@ -82,7 +82,7 @@ class Helper { return $filename; } - public function getGuestName() { + public function getGuestNameFromCookie() { if ($this->userId !== null || !isset($_COOKIE['guestUser']) || $_COOKIE['guestUser'] === '') { return null; } diff --git a/lib/TokenManager.php b/lib/TokenManager.php index 8c563f73..1ad20db8 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -21,11 +21,14 @@ namespace OCA\Richdocuments; +use InvalidArgumentException; use OCA\Richdocuments\Db\Direct; use OCA\Richdocuments\Db\WopiMapper; use OCA\Richdocuments\Db\Wopi; use OCA\Richdocuments\Service\CapabilitiesService; use OCA\Richdocuments\WOPI\Parser; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\DataResponse; use OCP\Constants; use OCP\Files\File; use OCP\Files\ForbiddenException; @@ -192,22 +195,7 @@ class TokenManager { fclose($fp); $serverHost = $this->urlGenerator->getAbsoluteURL('/'); - - $guestName = null; - if ($this->userId === null) { - if ($guestName = $this->helper->getGuestName()) { - $guestName = $this->trans->t('%s (Guest)', Util::sanitizeHTML($guestName)); - $cut = 56; - while (mb_strlen($guestName) >= 64) { - $guestName = $this->trans->t('%s (Guest)', Util::sanitizeHTML( - mb_substr($guestName, 0, $cut) - )); - $cut -= 5; - } - } else { - $guestName = $this->trans->t('Anonymous guest'); - } - } + $guestName = $this->userId === null ? $this->prepareGuestName($this->helper->getGuestNameFromCookie()) : null; $wopi = $this->wopiMapper->generateFileToken($fileId, $owneruid, $editoruid, $version, $updatable, $serverHost, $guestName, 0, $hideDownload, $direct, 0, $shareToken); return [ @@ -314,4 +302,32 @@ class TokenManager { return $wopi; } + public function prepareGuestName(string $guestName = null) { + if (empty($guestName)) { + return $this->trans->t('Anonymous guest'); + } + + $guestName = $this->trans->t('%s (Guest)', Util::sanitizeHTML($guestName)); + $cut = 56; + while (mb_strlen($guestName) >= 64) { + $guestName = $this->trans->t('%s (Guest)', Util::sanitizeHTML( + mb_substr($guestName, 0, $cut) + )); + $cut -= 5; + } + + return $guestName; + } + + /** + * @param string $accessToken + * @param string $guestName + * @throws DoesNotExistException + */ + public function updateGuestName(string $accessToken, string $guestName) { + $wopi = $this->wopiMapper->getWopiForToken($accessToken); + $wopi->setGuestDisplayname($this->prepareGuestName($guestName)); + $this->wopiMapper->update($wopi); + } + } diff --git a/src/document.js b/src/document.js index 63e93a96..53c89be7 100644 --- a/src/document.js +++ b/src/document.js @@ -2,7 +2,7 @@ import { emit } from '@nextcloud/event-bus' import { getRootUrl } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' import Config from './services/config.tsx' -import { setGuestNameCookie, shouldAskForGuestName } from './helpers/guestName' +import { setGuestName, shouldAskForGuestName } from './helpers/guestName' import PostMessageService from './services/postMessage.tsx' import { @@ -139,8 +139,13 @@ $.widget('oc.guestNamePicker', { $('#documents-content').prepend(text) const setGuestNameSubmit = () => { const username = $('#nickname').val() - setGuestNameCookie(username) - window.location.reload(true) + div.remove() + text.innerText = '' + text.classList.add('icon-loading') + setGuestName(username).then(() => { + $('#documents-content').remove() + documentsMain.initSession() + }) } $('#nickname').keyup(function(event) { diff --git a/src/helpers/guestName.js b/src/helpers/guestName.js index b47a8e3c..d32a94e0 100644 --- a/src/helpers/guestName.js +++ b/src/helpers/guestName.js @@ -22,6 +22,8 @@ import Config from './../services/config.tsx' import { getCurrentUser } from '@nextcloud/auth' +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' import mobile from './mobile' let guestName = '' @@ -44,11 +46,16 @@ const getGuestNameCookie = function() { return guestName } -const setGuestNameCookie = function(username) { +const setGuestName = function(username) { if (username !== '') { - document.cookie = 'guestUser=' + encodeURIComponent(username) + '; path=/' + // document.cookie = 'guestUser=' + encodeURIComponent(username) + '; path=/' guestName = username } + const accessToken = encodeURIComponent(Config.get('token')) + return axios.post(generateOcsUrl('apps/richdocuments/api/v1/wopi', 2) + 'guestname', { + access_token: accessToken, + guestName, + }) } const shouldAskForGuestName = () => { @@ -61,6 +68,6 @@ const shouldAskForGuestName = () => { export { getGuestNameCookie, - setGuestNameCookie, + setGuestName, shouldAskForGuestName, } diff --git a/tests/features/bootstrap/DirectContext.php b/tests/features/bootstrap/DirectContext.php index 4c17a0f4..4662d35d 100644 --- a/tests/features/bootstrap/DirectContext.php +++ b/tests/features/bootstrap/DirectContext.php @@ -83,6 +83,46 @@ class DirectContext implements Context { $this->handleDirectEditingLink(); } + /** + * @When /^User "([^"]*)" opens the file "([^"]*)" in the last share link through direct editing from server "([^"]*)"$/ + */ + public function userOpensTheFileInTheLastShareLinkThroughDirectEditingFromServer($user, $path, $host) { + $shareToken = $this->sharingContext->getLastShareData()['token']; + $this->serverContext->usingWebAsUser($user); + $this->requestPublicDirectEditingLink($user, $shareToken, $path, null, $this->serverContext->getServer($host)); + $this->handleDirectEditingLink(); + } + + /** + * @When /^User "([^"]*)" opens the file "([^"]*)" in the last share link through direct editing from server "([^"]*)" with password "([^"]*)"$/ + */ + public function userOpensTheFileInTheLastShareLinkThroughDirectEditingFromServerWithPassword($user, $path, $host, $password) { + $shareToken = $this->sharingContext->getLastShareData()['token']; + $this->serverContext->usingWebAsUser($user); + $this->requestPublicDirectEditingLink($user, $shareToken, $path, $password, $this->serverContext->getServer($host)); + $this->handleDirectEditingLink(); + } + + /** + * @When /^User "([^"]*)" cannot open the file "([^"]*)" in the last share link through direct editing from server "([^"]*)" with password "([^"]*)"$/ + */ + public function userCannotOpenTheFileInTheLastShareLinkThroughDirectEditingFromServerWithPassword($user, $path, $host, $password) { + $shareToken = $this->sharingContext->getLastShareData()['token']; + $this->serverContext->usingWebAsUser($user); + $this->requestPublicDirectEditingLink($user, $shareToken, $path, $password, $this->serverContext->getServer($host)); + $this->serverContext->assertHttpStatusCode(403); + } + + /** + * @When /^A guest opens the file "([^"]*)" in the last share link through direct editing$/ + */ + public function aGuestOpensTheFileInTheLastShareLinkThroughDirectEditing($path) { + $shareToken = $this->sharingContext->getLastShareData()['token']; + $this->serverContext->usingWebasGuest(); + $this->requestPublicDirectEditingLink(null, $shareToken, $path); + $this->handleDirectEditingLink(); + } + private function handleDirectEditingLink() { $this->serverContext->assertHttpStatusCode(200); $data = $this->serverContext->getOCSResponseData(); @@ -152,13 +192,25 @@ class DirectContext implements Context { $this->serverContext->sendOCSRequest('POST', 'apps/richdocuments/api/v1/document', [ 'fileId' => $fileId ]); } - private function requestPublicDirectEditingLink($user, $token, $filePath = null, $password = null) { - $this->serverContext->sendOCSRequest('POST', 'apps/richdocuments/api/v1/share', [ - 'host' => $this->serverContext->getBaseUrl(), + /** + * @param $user + * @param $token + * @param null $filePath + * @param null $password + * @param null $host + */ + private function requestPublicDirectEditingLink($user, $token, $filePath = null, $password = null, $host = null) { + // ServerContext currently does not support sending anonymous ocs requests + $options = $user ? [] : [ 'auth' => null ]; + $data = [ 'shareToken' => $token, 'path' => $filePath, 'password' => $password - ]); + ]; + if ($host) { + $data['host'] = $host; + } + $this->serverContext->sendOCSRequest('POST', 'apps/richdocuments/api/v1/share', $data, $options); } } diff --git a/tests/features/bootstrap/RichDocumentsContext.php b/tests/features/bootstrap/RichDocumentsContext.php index cea95325..20cf8bf4 100644 --- a/tests/features/bootstrap/RichDocumentsContext.php +++ b/tests/features/bootstrap/RichDocumentsContext.php @@ -215,4 +215,14 @@ class RichDocumentsContext implements Context $this->wopiToken = $result['token']; $this->wopiContext->setWopiParameters($this->currentServer, $this->fileId, $this->wopiToken); } + + /** + * @When /^the guest updates the display name to "([^"]*)"$/ + */ + public function updateTheGuestDisplayName($displayName) { + $this->serverContext->sendOCSRequest('POST', 'apps/richdocuments/api/v1/wopi/guestname', [ + 'access_token' => $this->wopiContext->getWopiToken(), + 'guestName' => $displayName, + ], [ 'auth' => null ]); + } } diff --git a/tests/features/bootstrap/WopiContext.php b/tests/features/bootstrap/WopiContext.php index 03ddda51..d115b884 100644 --- a/tests/features/bootstrap/WopiContext.php +++ b/tests/features/bootstrap/WopiContext.php @@ -66,6 +66,10 @@ class WopiContext implements Context { return $this->serverContext->getBaseUrl(); } + public function getWopiToken() { + return $this->wopiToken; + } + public function setWopiParameters($server, $fileId, $accessToken) { $this->currentServer = $server; $this->fileId = $fileId; diff --git a/tests/features/direct.feature b/tests/features/direct.feature index 8359b020..b09103d1 100644 --- a/tests/features/direct.feature +++ b/tests/features/direct.feature @@ -129,7 +129,8 @@ Feature: Direct editing And Collabora fetches and receives the following in the checkFileInfo response | BaseFileName | document-share-link.odt | | OwnerId | user1 | - | UserFriendlyName | user2-displayname (Guest) | + | UserId | user2 | + | UserFriendlyName | user2-displayname | And checkFileInfo "UserCanWrite" is true And both Collabora files used the same file id And Collabora can save the file with the content of "./../assets/template.ods" @@ -151,8 +152,8 @@ Feature: Direct editing And Collabora fetches and receives the following in the checkFileInfo response | BaseFileName | document-share-link.odt | | OwnerId | user1 | - | UserFriendlyName | user2-displayname (Guest) | - And checkFileInfo "UserId" matches "/Guest-/" + | UserId | user2 | + | UserFriendlyName | user2-displayname | And both Collabora files used the same file id And Collabora can not save the file with the content of "./../assets/template.ods" Then Collabora downoads the file and it is equal to "./../assets/template.odt" @@ -174,6 +175,57 @@ Feature: Direct editing And Collabora fetches and receives the following in the checkFileInfo response | BaseFileName | document-share-link.odt | | OwnerId | user1 | + | UserId | user2 | + | UserFriendlyName | user2-displayname | + And checkFileInfo "UserCanWrite" is true + And both Collabora files used the same file id + And Collabora can save the file with the content of "./../assets/template.ods" + Then Collabora downoads the file and it is equal to "./../assets/template.ods" + + Scenario: Open a file in a shared folder of a share link with direct editing as writable as a guest + Given on instance "serverA" + And as user "user1" + And User "user1" creates a folder "Folder" + And User "user1" uploads file "./../assets/template.odt" to "/Folder/document-share-link.odt" + When User "user1" opens "/Folder/document-share-link.odt" through direct editing + And Collabora fetches checkFileInfo + And as "user1" create a share with + | path | /Folder/ | + | shareType | 3 | + And Updating last share with + | permissions | 3 | + When A guest opens the file "/document-share-link.odt" in the last share link through direct editing + And Collabora fetches and receives the following in the checkFileInfo response + | BaseFileName | document-share-link.odt | + | OwnerId | user1 | + | UserFriendlyName | Anonymous guest | + When the guest updates the display name to "Random name" + And Collabora fetches checkFileInfo + And checkFileInfo "UserFriendlyName" is "Random name (Guest)" + And checkFileInfo "UserId" matches "/Guest-/" + And checkFileInfo "UserCanWrite" is true + And both Collabora files used the same file id + And Collabora can save the file with the content of "./../assets/template.ods" + Then Collabora downoads the file and it is equal to "./../assets/template.ods" + + Scenario: Open a file in a shared folder of a share link with direct editing as writable as a remote user + Given on instance "serverA" + And as user "user1" + And User "user1" creates a folder "Folder" + And User "user1" uploads file "./../assets/template.odt" to "/Folder/document-share-link.odt" + When User "user1" opens "/Folder/document-share-link.odt" through direct editing + And Collabora fetches checkFileInfo + And as "user1" create a share with + | path | /Folder/ | + | shareType | 3 | + And Updating last share with + | permissions | 3 | + Given on instance "serverB" + And as user "user2" + When User "user2" opens the file "/document-share-link.odt" in the last share link through direct editing from server "serverA" + And Collabora fetches and receives the following in the checkFileInfo response + | BaseFileName | document-share-link.odt | + | OwnerId | user1 | | UserFriendlyName | user2-displayname (Guest) | And checkFileInfo "UserId" matches "/Guest-/" And checkFileInfo "UserCanWrite" is true @@ -181,6 +233,35 @@ Feature: Direct editing And Collabora can save the file with the content of "./../assets/template.ods" Then Collabora downoads the file and it is equal to "./../assets/template.ods" + Scenario: Open a file in a shared folder of a share link with direct editing as writable as a remote user with password + Given on instance "serverA" + And as user "user1" + And User "user1" creates a folder "Folder" + And User "user1" uploads file "./../assets/template.odt" to "/Folder/document-share-link.odt" + When User "user1" opens "/Folder/document-share-link.odt" through direct editing + And Collabora fetches checkFileInfo + And as "user1" create a share with + | path | /Folder/ | + | shareType | 3 | + | password | mysecret | + And Updating last share with + | permissions | 3 | + Given on instance "serverB" + And as user "user2" + When User "user2" opens the file "/document-share-link.odt" in the last share link through direct editing from server "serverA" with password "mysecret" + And Collabora fetches and receives the following in the checkFileInfo response + | BaseFileName | document-share-link.odt | + | OwnerId | user1 | + | UserFriendlyName | user2-displayname (Guest) | + And checkFileInfo "UserId" matches "/Guest-/" + And checkFileInfo "UserCanWrite" is true + And both Collabora files used the same file id + And Collabora can save the file with the content of "./../assets/template.ods" + Then Collabora downoads the file and it is equal to "./../assets/template.ods" + + And as user "user2" + When User "user2" cannot open the file "/document-share-link.odt" in the last share link through direct editing from server "serverA" with password "wrongpassword" + @federation @known-failure-ci Scenario: Open a link that originates on a federated share through direct editing Given user "user3" exists |