Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

private/juliushaertl/cool settings iframe 2 #4446

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
376c8eb
Initial cool admin setting iframe setup
codewithvk Dec 3, 2024
7cdc547
WIP auth
codewithvk Dec 18, 2024
29eb8a1
Temp: Auth handling stuff
codewithvk Jan 7, 2025
ee53126
WIP: wopi setting upload
codewithvk Jan 7, 2025
550f6bc
Created an AppData-based directory for system settings and user settings
codewithvk Jan 8, 2025
f0f2871
Create a Settings controller API endpoint for handling AppData-based …
codewithvk Jan 8, 2025
6e60009
Temporary Commit: Created a temporary UI to validate the functionalit…
codewithvk Jan 8, 2025
7d93ddf
Change the admin settings iframe URL to adminIntegratorSettings.
codewithvk Jan 9, 2025
64422c1
WOPI: Update the wopi setting upload route to accept a file and store…
codewithvk Jan 9, 2025
e9549b7
add delete button and create wopi/setting route to handle wopi file r…
codewithvk Jan 11, 2025
346d2c0
Manage setting configs files with dynamic routes
codewithvk Jan 11, 2025
d7639c5
Code cleanup: Remove POC helper functions
codewithvk Jan 11, 2025
a126ae0
Send WOPI setting base URL to integrator
codewithvk Jan 13, 2025
c1caefe
fix: fetch config url
codewithvk Jan 15, 2025
6d82912
wopi: add delete setting file route
codewithvk Jan 16, 2025
b94a1bf
refactor: token generation for iframe
codewithvk Jan 19, 2025
0a17cf2
feat(user-settings): introduce iframe for user settings
codewithvk Jan 19, 2025
97d83b1
fix: linting issue for CI
codewithvk Jan 23, 2025
2aa7eef
fix: generate token for user shared config url
codewithvk Jan 23, 2025
478c976
fix: accept document and setting url token for file upload
codewithvk Jan 23, 2025
a6f49bb
fix: set proper WOPI response and remove unnecessary WOPI callback fr…
codewithvk Jan 23, 2025
0ba4a68
fix(settings): remove iframe title and section
codewithvk Jan 24, 2025
41cfbf7
fix(settings): fetch directory from root folder
codewithvk Jan 27, 2025
4326959
fix(wopi): share SharedSettings to wopi checkfileInfo
codewithvk Jan 28, 2025
d87ff89
fix(wopi): Generating setting token for guest users
codewithvk Jan 28, 2025
452f7c1
fix(settings): generate user config per userId & handle guest users
codewithvk Jan 29, 2025
b2c5bad
fix: composer psalm error via pointing correct folder
codewithvk Jan 29, 2025
d589c57
fix: code cleanups
codewithvk Jan 29, 2025
e786703
fix: move admin setting iframe below server-config
codewithvk Feb 2, 2025
66c1253
refactor: some code cleanups
codewithvk Feb 4, 2025
2bbca24
config: support hasSettingIframeSupport capability
codewithvk Feb 5, 2025
56a8c72
fix: Avoid warning on file id explode
juliusknorr Feb 5, 2025
ebbebd0
ci: Keep separate assets for cypress
juliusknorr Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cypress-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,15 @@ jobs:
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
if: failure()
with:
name: Upload screenshots
name: Upload screenshots ${{ matrix.code-image}}-${{ matrix.containers }}
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
retention-days: 5

- name: Upload nextcloud logs
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
if: failure()
with:
name: Upload nextcloud log
name: Upload nextcloud log ${{ matrix.code-image}}-${{ matrix.containers }}
path: data/nextcloud.log
retention-days: 5

Expand Down
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: 135 additions & 2 deletions lib/Controller/WopiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
use OCA\Richdocuments\Helper;
use OCA\Richdocuments\PermissionManager;
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 +46,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 +89,7 @@ public function __construct(
private ILockManager $lockManager,
private IEventDispatcher $eventDispatcher,
private TaskProcessingManager $taskProcessingManager,
private SettingsService $settingsService,
) {
parent::__construct($appName, $request);
}
Expand All @@ -100,7 +104,7 @@ public function __construct(
public function checkFileInfo(string $fileId, string $access_token): JSONResponse {
try {
[$fileId, , $version] = Helper::parseFileId($fileId);

$wopi = $this->wopiMapper->getWopiForToken($access_token);
$file = $this->getFileForWopiToken($wopi);
if (!($file instanceof File)) {
Expand Down Expand Up @@ -133,11 +137,18 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
} catch (NoLockProviderException|PreConditionNotMetException) {
}

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

if (!$isPublic) {
$userSettings = $this->generateSettings($userId, 'userconfig');
}
$sharedSettings = $this->generateSettings($userId, 'systemconfig');

$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 @@ -165,8 +176,13 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
'EnableRemoteAIContent' => $isTaskProcessingEnabled,
'HasContentRange' => true,
'ServerPrivateInfo' => [],
'SharedSettings' => $sharedSettings,
];

if (!$isPublic) {
$response['UserSettings'] = $userSettings;
}

$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 +397,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();
// TODO: auth - for admin??
$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 +984,16 @@ 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'];
}
// todo extract nextcloud url from everything
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
Loading
Loading