Add API to set the guest name for an existing WOPI token

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl 2021-08-13 09:45:03 +02:00
commit 95985564d0
No known key found for this signature in database
GPG key ID: 4C614C6ED2CDE6DF
14 changed files with 253 additions and 50 deletions

View file

@ -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'],

View file

@ -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.

View file

@ -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);

View file

@ -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([
@ -245,6 +254,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
*

View file

@ -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;

View file

@ -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()

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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,
}

View file

@ -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);
}
}

View file

@ -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 ]);
}
}

View file

@ -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;

View file

@ -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"
@ -171,6 +172,57 @@ Feature: Direct editing
| permissions | 3 |
And as user "user2"
When User "user2" 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 |
| 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 |
@ -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