Skip to content

Commit 452fb40

Browse files
authored
Merge pull request #4373 from codewithvk/private/codewithvk/cool_setting_iframe
2 parents 0ab7a84 + fdb0200 commit 452fb40

File tree

15 files changed

+865
-6
lines changed

15 files changed

+865
-6
lines changed

appinfo/routes.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@
3636
['name' => 'settings#getFontFileOverview', 'url' => 'settings/fonts/{name}/overview', 'verb' => 'GET'],
3737
['name' => 'settings#deleteFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'DELETE'],
3838
['name' => 'settings#uploadFontFile', 'url' => 'settings/fonts', 'verb' => 'POST'],
39+
[
40+
'name' => 'settings#getSettingsFile',
41+
'url' => 'settings/{type}/{token}/{category}/{name}',
42+
'verb' => 'GET',
43+
'requirements' => [
44+
'type' => '[a-zA-Z0-9_\-]+',
45+
'category' => '[a-zA-Z0-9_\-]+',
46+
'name' => '.+',
47+
],
48+
],
49+
['name' => 'settings#generateIframeToken', 'url' => 'settings/generateToken/{type}', 'verb' => 'GET'],
3950

4051
// Direct Editing: Webview
4152
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],

lib/Controller/SettingsController.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
use OCA\Richdocuments\AppConfig;
99
use OCA\Richdocuments\Capabilities;
10+
use OCA\Richdocuments\Db\WopiMapper;
1011
use OCA\Richdocuments\Service\CapabilitiesService;
1112
use OCA\Richdocuments\Service\ConnectivityService;
1213
use OCA\Richdocuments\Service\DemoService;
1314
use OCA\Richdocuments\Service\DiscoveryService;
1415
use OCA\Richdocuments\Service\FontService;
16+
use OCA\Richdocuments\Service\SettingsService;
1517
use OCA\Richdocuments\UploadException;
1618
use OCP\App\IAppManager;
1719
use OCP\AppFramework\Controller;
@@ -54,7 +56,10 @@ public function __construct(
5456
private CapabilitiesService $capabilitiesService,
5557
private DemoService $demoService,
5658
private FontService $fontService,
59+
private SettingsService $settingsService,
5760
private LoggerInterface $logger,
61+
private IURLGenerator $urlGenerator,
62+
private WopiMapper $wopiMapper,
5863
private ?string $userId,
5964
) {
6065
parent::__construct($appName, $request);
@@ -96,7 +101,6 @@ public function demoServers(): DataResponse {
96101
public function getSettings(): JSONResponse {
97102
return new JSONResponse($this->getSettingsData());
98103
}
99-
100104
private function getSettingsData(): array {
101105
return [
102106
'wopi_url' => $this->appConfig->getCollaboraUrlInternal(),
@@ -113,6 +117,7 @@ private function getSettingsData(): array {
113117
'esignature_base_url' => $this->appConfig->getAppValue('esignature_base_url'),
114118
'esignature_client_id' => $this->appConfig->getAppValue('esignature_client_id'),
115119
'esignature_secret' => $this->appConfig->getAppValue('esignature_secret'),
120+
'userId' => $this->userId
116121
];
117122
}
118123

@@ -407,6 +412,23 @@ public function getFontFileOverview(string $name): DataDisplayResponse {
407412
}
408413
}
409414

415+
/**
416+
* @NoAdminRequired
417+
*
418+
* @param string $type - Type is 'admin' or 'user'
419+
* @return DataResponse
420+
*/
421+
public function generateIframeToken(string $type): DataResponse {
422+
try {
423+
$response = $this->settingsService->generateIframeToken($type, $this->userId);
424+
return new DataResponse($response);
425+
} catch (\Exception $e) {
426+
return new DataResponse([
427+
'message' => 'Settings token not generated.'
428+
], Http::STATUS_INTERNAL_SERVER_ERROR);
429+
}
430+
}
431+
410432
/**
411433
* @param string $name
412434
* @return DataResponse
@@ -450,6 +472,40 @@ public function uploadFontFile(): JSONResponse {
450472
}
451473
}
452474

475+
/**
476+
* @param string $type
477+
* @param string $category
478+
* @param string $name
479+
*
480+
* @return DataDisplayResponse
481+
*
482+
* @NoAdminRequired
483+
* @PublicPage
484+
* @NoCSRFRequired
485+
**/
486+
public function getSettingsFile(string $type, string $token, string $category, string $name) {
487+
try {
488+
$wopi = $this->wopiMapper->getWopiForToken($token);
489+
if ($type === 'userconfig') {
490+
$userId = $wopi->getEditorUid() ?: $wopi->getOwnerUid();
491+
$type = $type . '/' . $userId;
492+
}
493+
$systemFile = $this->settingsService->getSettingsFile($type, $category, $name);
494+
return new DataDisplayResponse(
495+
$systemFile->getContent(),
496+
200,
497+
[
498+
'Content-Type' => $systemFile->getMimeType() ?: 'application/octet-stream'
499+
]
500+
);
501+
} catch (NotFoundException $e) {
502+
return new DataDisplayResponse('File not found.', 404);
503+
} catch (\Exception $e) {
504+
return new DataDisplayResponse('Something went wrong', 500);
505+
}
506+
}
507+
508+
453509
/**
454510
* @param string $key
455511
* @return array

lib/Controller/WopiController.php

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
use OCA\Richdocuments\Exceptions\UnknownTokenException;
1717
use OCA\Richdocuments\Helper;
1818
use OCA\Richdocuments\PermissionManager;
19+
use OCA\Richdocuments\Service\CapabilitiesService;
1920
use OCA\Richdocuments\Service\FederationService;
21+
use OCA\Richdocuments\Service\SettingsService;
2022
use OCA\Richdocuments\Service\UserScopeService;
2123
use OCA\Richdocuments\TaskProcessingManager;
2224
use OCA\Richdocuments\TemplateManager;
2325
use OCA\Richdocuments\TokenManager;
26+
use OCA\Richdocuments\WOPI\SettingsUrl;
2427
use OCP\AppFramework\Controller;
2528
use OCP\AppFramework\Http;
2629
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
@@ -44,6 +47,7 @@
4447
use OCP\Files\Lock\OwnerLockedException;
4548
use OCP\Files\Node;
4649
use OCP\Files\NotFoundException;
50+
use OCP\Files\NotPermittedException;
4751
use OCP\IConfig;
4852
use OCP\IGroupManager;
4953
use OCP\IRequest;
@@ -86,6 +90,8 @@ public function __construct(
8690
private ILockManager $lockManager,
8791
private IEventDispatcher $eventDispatcher,
8892
private TaskProcessingManager $taskProcessingManager,
93+
private SettingsService $settingsService,
94+
private CapabilitiesService $capabilitiesService,
8995
) {
9096
parent::__construct($appName, $request);
9197
}
@@ -133,11 +139,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
133139
} catch (NoLockProviderException|PreConditionNotMetException) {
134140
}
135141

142+
$userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId;
143+
144+
136145
$response = [
137146
'BaseFileName' => $file->getName(),
138147
'Size' => $file->getSize(),
139148
'Version' => $version,
140-
'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId,
149+
'UserId' => $userId,
141150
'OwnerId' => $wopi->getOwnerUid(),
142151
'UserFriendlyName' => $userDisplayName,
143152
'UserExtraInfo' => [],
@@ -167,6 +176,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
167176
'ServerPrivateInfo' => [],
168177
];
169178

179+
if ($this->capabilitiesService->hasSettingIframeSupport()) {
180+
181+
if (!$isPublic) {
182+
$response['UserSettings'] = $this->generateSettings($userId, 'userconfig');
183+
}
184+
$response['SharedSettings'] = $this->generateSettings($userId, 'systemconfig');
185+
}
186+
170187
$enableZotero = $this->config->getAppValue(Application::APPNAME, 'zoteroEnabled', 'yes') === 'yes';
171188
if (!$isPublic && $enableZotero) {
172189
$zoteroAPIKey = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'zoteroAPIKey', '');
@@ -381,6 +398,111 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
381398
}
382399
}
383400

401+
#[NoAdminRequired]
402+
#[NoCSRFRequired]
403+
#[PublicPage]
404+
#[FrontpageRoute(verb: 'GET', url: 'wopi/settings')]
405+
public function getSettings(string $type, string $access_token): JSONResponse {
406+
if (empty($type)) {
407+
return new JSONResponse(['error' => 'Invalid type parameter'], Http::STATUS_BAD_REQUEST);
408+
}
409+
410+
try {
411+
$wopi = $this->wopiMapper->getWopiForToken($access_token);
412+
if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) {
413+
return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_BAD_REQUEST);
414+
}
415+
416+
$isPublic = empty($wopi->getEditorUid());
417+
$guestUserId = 'Guest-' . \OC::$server->getSecureRandom()->generate(8);
418+
$userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId;
419+
420+
$userConfig = $this->settingsService->generateSettingsConfig($type, $userId);
421+
return new JSONResponse($userConfig, Http::STATUS_OK);
422+
} catch (UnknownTokenException|ExpiredTokenException $e) {
423+
$this->logger->debug($e->getMessage(), ['exception' => $e]);
424+
return new JSONResponse(['error' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
425+
} catch (\Exception $e) {
426+
$this->logger->error($e->getMessage(), ['exception' => $e]);
427+
return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR);
428+
}
429+
}
430+
431+
#[NoAdminRequired]
432+
#[NoCSRFRequired]
433+
#[PublicPage]
434+
#[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')]
435+
public function uploadSettingsFile(string $fileId, string $access_token): JSONResponse {
436+
try {
437+
$wopi = $this->wopiMapper->getWopiForToken($access_token);
438+
439+
$userId = $wopi->getEditorUid();
440+
441+
$content = fopen('php://input', 'rb');
442+
if (!$content) {
443+
throw new \Exception('Failed to read input stream.');
444+
}
445+
446+
$fileContent = stream_get_contents($content);
447+
fclose($content);
448+
449+
// Use the fileId as a file path URL (e.g., "/settings/systemconfig/wordbook/en_US%20(1).dic")
450+
$settingsUrl = new SettingsUrl($fileId);
451+
$result = $this->settingsService->uploadFile($settingsUrl, $fileContent, $userId);
452+
453+
return new JSONResponse([
454+
'status' => 'success',
455+
'filename' => $settingsUrl->getFileName(),
456+
'details' => $result,
457+
], Http::STATUS_OK);
458+
459+
} catch (UnknownTokenException $e) {
460+
$this->logger->debug($e->getMessage(), ['exception' => $e]);
461+
return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN);
462+
} catch (\Exception $e) {
463+
$this->logger->error($e->getMessage(), ['exception' => $e]);
464+
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
465+
}
466+
}
467+
468+
#[NoAdminRequired]
469+
#[NoCSRFRequired]
470+
#[PublicPage]
471+
#[FrontpageRoute(verb: 'DELETE', url: 'wopi/settings')]
472+
public function deleteSettingsFile(string $fileId, string $access_token): JSONResponse {
473+
try {
474+
$wopi = $this->wopiMapper->getWopiForToken($access_token);
475+
if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) {
476+
return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_FORBIDDEN);
477+
}
478+
479+
// Parse the dynamic file path from `fileId`, e.g. "/settings/systemconfig/wordbook/en_US (1).dic"
480+
$settingsUrl = new SettingsUrl($fileId);
481+
$type = $settingsUrl->getType();
482+
$category = $settingsUrl->getCategory();
483+
$fileName = $settingsUrl->getFileName();
484+
$userId = $wopi->getEditorUid();
485+
486+
$this->settingsService->deleteSettingsFile($type, $category, $fileName, $userId);
487+
488+
return new JSONResponse([
489+
'status' => 'success',
490+
'message' => "File '$fileName' deleted from '$category' of type '$type'."
491+
], Http::STATUS_OK);
492+
} catch (UnknownTokenException $e) {
493+
$this->logger->debug($e->getMessage(), ['exception' => $e]);
494+
return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN);
495+
} catch (NotFoundException $e) {
496+
return new JSONResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND);
497+
} catch (NotPermittedException $e) {
498+
return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
499+
} catch (\Exception $e) {
500+
$this->logger->error($e->getMessage(), ['exception' => $e]);
501+
return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR);
502+
}
503+
}
504+
505+
384506
/**
385507
* Given an access token and a fileId, replaces the files with the request body.
386508
* Expects a valid token in access_token parameter.
@@ -863,4 +985,17 @@ private function getWopiUrlForTemplate(Wopi $wopi): string {
863985
$nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
864986
return $nextcloudUrl . '/index.php/apps/richdocuments/wopi/template/' . $wopi->getTemplateId() . '?access_token=' . $wopi->getToken();
865987
}
988+
989+
private function generateSettingToken(string $userId): string {
990+
return $this->settingsService->generateIframeToken('user', $userId)['token'];
991+
}
992+
993+
private function generateSettings(string $userId, string $type): array {
994+
$nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
995+
$uri = $nextcloudUrl . '/index.php/apps/richdocuments/wopi/settings' . '?type=' . $type . '&access_token=' . $this->generateSettingToken($userId) . '&fileId=' . '-1';
996+
return [
997+
'uri' => $uri,
998+
'stamp' => time()
999+
];
1000+
}
8661001
}

lib/Db/Wopi.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ class Wopi extends Entity implements \JsonSerializable {
6868
*/
6969
public const TOKEN_TYPE_INITIATOR = 4;
7070

71+
/*
72+
* Temporary token that is used for authentication while communication between cool iframe and user/admin settings
73+
*/
74+
public const TOKEN_TYPE_SETTING_AUTH = 5;
75+
7176
/** @var string */
7277
protected $ownerUid;
7378

lib/Db/WopiMapper.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ public function generateFileToken($fileId, $owner, $editor, $version, $updatable
6666
return $wopi;
6767
}
6868

69+
public function generateUserSettingsToken($fileId, $userId, $version, $serverHost) {
70+
$token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);
71+
72+
$wopi = Wopi::fromParams([
73+
'fileid' => $fileId,
74+
'ownerUid' => $userId,
75+
'editorUid' => $userId,
76+
'version' => $version,
77+
'canwrite' => true,
78+
'serverHost' => $serverHost,
79+
'token' => $token,
80+
'expiry' => $this->calculateNewTokenExpiry(),
81+
'templateId' => '0',
82+
'tokenType' => Wopi::TOKEN_TYPE_SETTING_AUTH,
83+
]);
84+
85+
/** @var Wopi $wopi */
86+
$wopi = $this->insert($wopi);
87+
88+
return $wopi;
89+
}
90+
6991
public function generateInitiatorToken($uid, $remoteServer) {
7092
$token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);
7193

lib/Helper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static function parseFileId(string $fileId) {
4040
}
4141

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

4646
return [

lib/Service/CapabilitiesService.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public function hasZoteroSupport(): bool {
8181
return $this->getCapabilities()['hasZoteroSupport'] ?? false;
8282
}
8383

84+
public function hasSettingIframeSupport(): bool {
85+
return $this->getCapabilities()['hasSettingIframeSupport'] ?? false;
86+
}
87+
8488
public function hasWASMSupport(): bool {
8589
return $this->getCapabilities()['hasWASMSupport'] ?? false;
8690
}

0 commit comments

Comments
 (0)