Skip to content

Commit

Permalink
Merge pull request #4373 from codewithvk/private/codewithvk/cool_sett…
Browse files Browse the repository at this point in the history
…ing_iframe
  • Loading branch information
juliusknorr authored Feb 5, 2025
2 parents 0ab7a84 + fdb0200 commit 452fb40
Show file tree
Hide file tree
Showing 15 changed files with 865 additions and 6 deletions.
11 changes: 11 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@
['name' => 'settings#getFontFileOverview', 'url' => 'settings/fonts/{name}/overview', 'verb' => 'GET'],
['name' => 'settings#deleteFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'DELETE'],
['name' => 'settings#uploadFontFile', 'url' => 'settings/fonts', 'verb' => 'POST'],
[
'name' => 'settings#getSettingsFile',
'url' => 'settings/{type}/{token}/{category}/{name}',
'verb' => 'GET',
'requirements' => [
'type' => '[a-zA-Z0-9_\-]+',
'category' => '[a-zA-Z0-9_\-]+',
'name' => '.+',
],
],
['name' => 'settings#generateIframeToken', 'url' => 'settings/generateToken/{type}', 'verb' => 'GET'],

// Direct Editing: Webview
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],
Expand Down
58 changes: 57 additions & 1 deletion lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
use OCA\Richdocuments\Db\WopiMapper;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\ConnectivityService;
use OCA\Richdocuments\Service\DemoService;
use OCA\Richdocuments\Service\DiscoveryService;
use OCA\Richdocuments\Service\FontService;
use OCA\Richdocuments\Service\SettingsService;
use OCA\Richdocuments\UploadException;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
Expand Down Expand Up @@ -54,7 +56,10 @@ public function __construct(
private CapabilitiesService $capabilitiesService,
private DemoService $demoService,
private FontService $fontService,
private SettingsService $settingsService,
private LoggerInterface $logger,
private IURLGenerator $urlGenerator,
private WopiMapper $wopiMapper,
private ?string $userId,
) {
parent::__construct($appName, $request);
Expand Down Expand Up @@ -96,7 +101,6 @@ public function demoServers(): DataResponse {
public function getSettings(): JSONResponse {
return new JSONResponse($this->getSettingsData());
}

private function getSettingsData(): array {
return [
'wopi_url' => $this->appConfig->getCollaboraUrlInternal(),
Expand All @@ -113,6 +117,7 @@ private function getSettingsData(): array {
'esignature_base_url' => $this->appConfig->getAppValue('esignature_base_url'),
'esignature_client_id' => $this->appConfig->getAppValue('esignature_client_id'),
'esignature_secret' => $this->appConfig->getAppValue('esignature_secret'),
'userId' => $this->userId
];
}

Expand Down Expand Up @@ -407,6 +412,23 @@ public function getFontFileOverview(string $name): DataDisplayResponse {
}
}

/**
* @NoAdminRequired
*
* @param string $type - Type is 'admin' or 'user'
* @return DataResponse
*/
public function generateIframeToken(string $type): DataResponse {
try {
$response = $this->settingsService->generateIframeToken($type, $this->userId);
return new DataResponse($response);
} catch (\Exception $e) {
return new DataResponse([
'message' => 'Settings token not generated.'
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param string $name
* @return DataResponse
Expand Down Expand Up @@ -450,6 +472,40 @@ public function uploadFontFile(): JSONResponse {
}
}

/**
* @param string $type
* @param string $category
* @param string $name
*
* @return DataDisplayResponse
*
* @NoAdminRequired
* @PublicPage
* @NoCSRFRequired
**/
public function getSettingsFile(string $type, string $token, string $category, string $name) {
try {
$wopi = $this->wopiMapper->getWopiForToken($token);
if ($type === 'userconfig') {
$userId = $wopi->getEditorUid() ?: $wopi->getOwnerUid();
$type = $type . '/' . $userId;
}
$systemFile = $this->settingsService->getSettingsFile($type, $category, $name);
return new DataDisplayResponse(
$systemFile->getContent(),
200,
[
'Content-Type' => $systemFile->getMimeType() ?: 'application/octet-stream'
]
);
} catch (NotFoundException $e) {
return new DataDisplayResponse('File not found.', 404);
} catch (\Exception $e) {
return new DataDisplayResponse('Something went wrong', 500);
}
}


/**
* @param string $key
* @return array
Expand Down
137 changes: 136 additions & 1 deletion lib/Controller/WopiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
use OCA\Richdocuments\Exceptions\UnknownTokenException;
use OCA\Richdocuments\Helper;
use OCA\Richdocuments\PermissionManager;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\FederationService;
use OCA\Richdocuments\Service\SettingsService;
use OCA\Richdocuments\Service\UserScopeService;
use OCA\Richdocuments\TaskProcessingManager;
use OCA\Richdocuments\TemplateManager;
use OCA\Richdocuments\TokenManager;
use OCA\Richdocuments\WOPI\SettingsUrl;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
Expand All @@ -44,6 +47,7 @@
use OCP\Files\Lock\OwnerLockedException;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IRequest;
Expand Down Expand Up @@ -86,6 +90,8 @@ public function __construct(
private ILockManager $lockManager,
private IEventDispatcher $eventDispatcher,
private TaskProcessingManager $taskProcessingManager,
private SettingsService $settingsService,
private CapabilitiesService $capabilitiesService,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -133,11 +139,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
} catch (NoLockProviderException|PreConditionNotMetException) {
}

$userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId;


$response = [
'BaseFileName' => $file->getName(),
'Size' => $file->getSize(),
'Version' => $version,
'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId,
'UserId' => $userId,
'OwnerId' => $wopi->getOwnerUid(),
'UserFriendlyName' => $userDisplayName,
'UserExtraInfo' => [],
Expand Down Expand Up @@ -167,6 +176,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
'ServerPrivateInfo' => [],
];

if ($this->capabilitiesService->hasSettingIframeSupport()) {

if (!$isPublic) {
$response['UserSettings'] = $this->generateSettings($userId, 'userconfig');
}
$response['SharedSettings'] = $this->generateSettings($userId, 'systemconfig');
}

$enableZotero = $this->config->getAppValue(Application::APPNAME, 'zoteroEnabled', 'yes') === 'yes';
if (!$isPublic && $enableZotero) {
$zoteroAPIKey = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'zoteroAPIKey', '');
Expand Down Expand Up @@ -381,6 +398,111 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
}
}

#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'GET', url: 'wopi/settings')]
public function getSettings(string $type, string $access_token): JSONResponse {
if (empty($type)) {
return new JSONResponse(['error' => 'Invalid type parameter'], Http::STATUS_BAD_REQUEST);
}

try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);
if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) {
return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_BAD_REQUEST);
}

$isPublic = empty($wopi->getEditorUid());
$guestUserId = 'Guest-' . \OC::$server->getSecureRandom()->generate(8);
$userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId;

$userConfig = $this->settingsService->generateSettingsConfig($type, $userId);
return new JSONResponse($userConfig, Http::STATUS_OK);
} catch (UnknownTokenException|ExpiredTokenException $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')]
public function uploadSettingsFile(string $fileId, string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);

$userId = $wopi->getEditorUid();

$content = fopen('php://input', 'rb');
if (!$content) {
throw new \Exception('Failed to read input stream.');
}

$fileContent = stream_get_contents($content);
fclose($content);

// Use the fileId as a file path URL (e.g., "/settings/systemconfig/wordbook/en_US%20(1).dic")
$settingsUrl = new SettingsUrl($fileId);
$result = $this->settingsService->uploadFile($settingsUrl, $fileContent, $userId);

return new JSONResponse([
'status' => 'success',
'filename' => $settingsUrl->getFileName(),
'details' => $result,
], Http::STATUS_OK);

} catch (UnknownTokenException $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'DELETE', url: 'wopi/settings')]
public function deleteSettingsFile(string $fileId, string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);
if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) {
return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_FORBIDDEN);
}

// Parse the dynamic file path from `fileId`, e.g. "/settings/systemconfig/wordbook/en_US (1).dic"
$settingsUrl = new SettingsUrl($fileId);
$type = $settingsUrl->getType();
$category = $settingsUrl->getCategory();
$fileName = $settingsUrl->getFileName();
$userId = $wopi->getEditorUid();

$this->settingsService->deleteSettingsFile($type, $category, $fileName, $userId);

return new JSONResponse([
'status' => 'success',
'message' => "File '$fileName' deleted from '$category' of type '$type'."
], Http::STATUS_OK);
} catch (UnknownTokenException $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN);
} catch (NotFoundException $e) {
return new JSONResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND);
} catch (NotPermittedException $e) {
return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}


/**
* Given an access token and a fileId, replaces the files with the request body.
* Expects a valid token in access_token parameter.
Expand Down Expand Up @@ -863,4 +985,17 @@ private function getWopiUrlForTemplate(Wopi $wopi): string {
$nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
return $nextcloudUrl . '/index.php/apps/richdocuments/wopi/template/' . $wopi->getTemplateId() . '?access_token=' . $wopi->getToken();
}

private function generateSettingToken(string $userId): string {
return $this->settingsService->generateIframeToken('user', $userId)['token'];
}

private function generateSettings(string $userId, string $type): array {
$nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
$uri = $nextcloudUrl . '/index.php/apps/richdocuments/wopi/settings' . '?type=' . $type . '&access_token=' . $this->generateSettingToken($userId) . '&fileId=' . '-1';
return [
'uri' => $uri,
'stamp' => time()
];
}
}
5 changes: 5 additions & 0 deletions lib/Db/Wopi.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class Wopi extends Entity implements \JsonSerializable {
*/
public const TOKEN_TYPE_INITIATOR = 4;

/*
* Temporary token that is used for authentication while communication between cool iframe and user/admin settings
*/
public const TOKEN_TYPE_SETTING_AUTH = 5;

/** @var string */
protected $ownerUid;

Expand Down
22 changes: 22 additions & 0 deletions lib/Db/WopiMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ public function generateFileToken($fileId, $owner, $editor, $version, $updatable
return $wopi;
}

public function generateUserSettingsToken($fileId, $userId, $version, $serverHost) {
$token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);

$wopi = Wopi::fromParams([
'fileid' => $fileId,
'ownerUid' => $userId,
'editorUid' => $userId,
'version' => $version,
'canwrite' => true,
'serverHost' => $serverHost,
'token' => $token,
'expiry' => $this->calculateNewTokenExpiry(),
'templateId' => '0',
'tokenType' => Wopi::TOKEN_TYPE_SETTING_AUTH,
]);

/** @var Wopi $wopi */
$wopi = $this->insert($wopi);

return $wopi;
}

public function generateInitiatorToken($uid, $remoteServer) {
$token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);

Expand Down
2 changes: 1 addition & 1 deletion lib/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static function parseFileId(string $fileId) {
}

if (str_contains($fileId, '-')) {
[$fileId, $templateId] = explode('/', $fileId);
[$fileId, $templateId] = array_pad(explode('/', $fileId), 2, null);
}

return [
Expand Down
4 changes: 4 additions & 0 deletions lib/Service/CapabilitiesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public function hasZoteroSupport(): bool {
return $this->getCapabilities()['hasZoteroSupport'] ?? false;
}

public function hasSettingIframeSupport(): bool {
return $this->getCapabilities()['hasSettingIframeSupport'] ?? false;
}

public function hasWASMSupport(): bool {
return $this->getCapabilities()['hasWASMSupport'] ?? false;
}
Expand Down
Loading

0 comments on commit 452fb40

Please sign in to comment.