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

WIP: Admin setting iframe #4373

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
7 changes: 7 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@
['name' => 'settings#checkSettings', 'url' => 'settings/check', 'verb' => 'GET'],
['name' => 'settings#demoServers', 'url' => 'settings/demo', 'verb' => 'GET'],
['name' => 'settings#getFontNames', 'url' => 'settings/fonts', 'verb' => 'GET'],
// We want to create new routes like this to store files...
['name' => 'settings#getJsonFontList', 'url' => 'settings/fonts.json', 'verb' => 'GET'],
['name' => 'settings#getFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'GET'],
['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#uploadSystemFile', 'url' => 'settings/system-files', 'verb' => 'POST'],
['name' => 'settings#getSystemFileList', 'url' => 'settings/system-files.json', 'verb' => 'GET' ],
['name' => 'settings#getSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'GET'],
['name' => 'settings#uploadUserFile', 'url' => 'settings/user-files', 'verb' => 'POST'],
['name' => 'settings#getUserFileList', 'url' => 'settings/user-files.json', 'verb' => 'GET'],
['name' => 'settings#downloadUserFile', 'url' => 'settings/user-files/{fileName}', 'verb' => 'GET'],

// Direct Editing: Webview
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
'OCA\\Richdocuments\\Service\\FederationService' => $baseDir . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => $baseDir . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php',
'OCA\\Richdocuments\\Service\\SettingsService' => $baseDir . '/../lib/Service/SettingsService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => $baseDir . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\PdfService' => $baseDir . '/../lib/Service/PdfService.php',
'OCA\\Richdocuments\\Service\\RemoteOptionsService' => $baseDir . '/../lib/Service/RemoteOptionsService.php',
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Service\\FederationService' => __DIR__ . '/..' . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => __DIR__ . '/..' . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php',
'OCA\\Richdocuments\\Service\\SettingsService' => __DIR__ . '/..' . '/../lib/Service/SettingsService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => __DIR__ . '/..' . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\PdfService' => __DIR__ . '/..' . '/../lib/Service/PdfService.php',
'OCA\\Richdocuments\\Service\\RemoteOptionsService' => __DIR__ . '/..' . '/../lib/Service/RemoteOptionsService.php',
Expand Down
19 changes: 19 additions & 0 deletions lib/Controller/DocumentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,25 @@ public function editOnlineTarget(int $fileId, ?string $target = null): RedirectR
#[PublicPage]
public function token(int $fileId, ?string $shareToken = null, ?string $path = null, ?string $guestName = null): DataResponse {
try {
if ($fileId === -1 && $path !== null && str_starts_with($path, 'adminIntegratorSettings/')) {
$parts = explode('/', $path);
$adminUserId = $parts[1] ?? $this->userId; // fallback if needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems dangerous, we should always use $this->userId and not let the user id be passed as request data.

Additionally we need to check if the user id is an admin (Can be done through https://github.com/nextcloud/server/blob/dff881544920f426b984f91b7bc8dece1f351342/lib/public/IGroupManager.php#L115


$docKey = $fileId . '_' . $this->config->getSystemValue('instanceid');

$wopi = $this->tokenManager->generateWopiToken($fileId, null, $adminUserId);

$coolBaseUrl = $this->appConfig->getCollaboraUrlPublic();
$adminSettingsWopiSrc = $coolBaseUrl . '/browser/adminIntegratorSettings.html?';

return new DataResponse([
'urlSrc' => $adminSettingsWopiSrc,
'token' => $wopi->getToken(),
'token_ttl' => $wopi->getExpiry(),
]);
}

// Normal file handling (unchanged)
$share = $shareToken ? $this->shareManager->getShareByToken($shareToken) : null;
$file = $shareToken ? $this->getFileForShare($share, $fileId, $path) : $this->getFileForUser($fileId, $path);

Expand Down
168 changes: 167 additions & 1 deletion lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
use OCA\Richdocuments\Service\SettingsService;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\ConnectivityService;
use OCA\Richdocuments\Service\DemoService;
Expand Down Expand Up @@ -54,6 +55,7 @@ public function __construct(
private CapabilitiesService $capabilitiesService,
private DemoService $demoService,
private FontService $fontService,
private SettingsService $settingsService,
private LoggerInterface $logger,
private ?string $userId,
) {
Expand Down Expand Up @@ -96,7 +98,7 @@ public function demoServers(): DataResponse {
public function getSettings(): JSONResponse {
return new JSONResponse($this->getSettingsData());
}

// TODO : Provide Auth tokens here :)
private function getSettingsData(): array {
return [
'wopi_url' => $this->appConfig->getCollaboraUrlInternal(),
Expand All @@ -110,6 +112,7 @@ private function getSettingsData(): array {
'product_name' => $this->capabilitiesService->getServerProductName(),
'product_version' => $this->capabilitiesService->getProductVersion(),
'product_hash' => $this->capabilitiesService->getProductHash(),
'userId' => $this->userId
];
}

Expand Down Expand Up @@ -432,6 +435,169 @@ public function uploadFontFile(): JSONResponse {
}
}

/**
* @return JSONResponse
* @throws UploadException
* @throws NotPermittedException
* @throws Exception
*/
public function uploadSystemFile(): JSONResponse {
try {
$file = $this->getUploadedFile('systemfile');
if (!isset($file['tmp_name'], $file['name'])) {
return new JSONResponse(['error' => 'No uploaded file'], 400);
}

$newFileResource = fopen($file['tmp_name'], 'rb');
if ($newFileResource === false) {
throw new UploadException('Could not open file resource');
}

$result = $this->settingsService->uploadSystemFile($file['name'], $newFileResource);
return new JSONResponse($result);
} catch (NotPermittedException $e) {
$this->logger->error('Not permitted', ['exception' => $e]);
return new JSONResponse(['error' => 'Not permitted'], 403);
} catch (UploadException $e) {
$this->logger->error('UploadException', ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], 400);
} catch (\Exception $e) {
$this->logger->error('General error', ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], 500);
}
}

/**
* @return JSONResponse
*/
public function getSystemFileList(): JSONResponse {
try {
$fileNames = $this->settingsService->getSystemFileNames();
return new JSONResponse($fileNames);
} catch (NotPermittedException $e) {
return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
}
}

/**
* @param string $fileName
* @return DataResponse
*
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
*/
public function getSystemFile(string $fileName) {
try {
$systemFile = $this->settingsService->getSystemFile($fileName);
$mimeType = $systemFile->getMimeType() ?: 'application/octet-stream';
$fileContents = $systemFile->getContent();

return new DataDisplayResponse(
$fileContents,
Http::STATUS_OK,
[
'Content-Type' => $mimeType,
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
]
);
} 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);
}
}

/**
* @return JSONResponse
*/
public function uploadUserFile(): JSONResponse {
// Make sure we know who is uploading
if ($this->userId === null) {
return new JSONResponse(['error' => 'User not logged in'], 401);
}

try {
// The key "userfile" must match the FormData append() key in Vue
$file = $this->getUploadedFile('userfile');
if (!isset($file['tmp_name'], $file['name'])) {
return new JSONResponse(['error' => 'No uploaded file'], 400);
}

$newFileResource = fopen($file['tmp_name'], 'rb');
if ($newFileResource === false) {
throw new UploadException('Could not open file resource');
}

$result = $this->settingsService->uploadUserFile($this->userId, $file['name'], $newFileResource);
return new JSONResponse($result); // e.g. { "size": 1234 }
} catch (NotPermittedException $e) {
$this->logger->error('Not permitted', ['exception' => $e]);
return new JSONResponse(['error' => 'Not permitted'], 403);
} catch (UploadException $e) {
$this->logger->error('UploadException', ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], 400);
} catch (\Exception $e) {
$this->logger->error('General error', ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], 500);
}
}

/**
* @return JSONResponse
*/
public function getUserFileList(): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
}

try {
$fileNames = $this->settingsService->getUserFileNames($this->userId);
return new JSONResponse($fileNames);
} catch (NotPermittedException $e) {
return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
} catch (\Exception $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param string $fileName
*
* @NoAdminRequired
* @NoCSRFRequired
*/
public function downloadUserFile(string $fileName) {
if ($this->userId === null) {
return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
}

try {
$userFile = $this->settingsService->getUserFile($this->userId, $fileName);
$mimeType = $userFile->getMimeType() ?: 'application/octet-stream';
$content = $userFile->getContent(); // get file bytes

// Return as DataDisplayResponse
$response = new DataDisplayResponse(
$content,
Http::STATUS_OK,
[
'Content-Type' => $mimeType,
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
]
);

return $response;
} 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) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param string $key
* @return array
Expand Down
56 changes: 55 additions & 1 deletion lib/Controller/WopiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use OCA\Richdocuments\Service\SettingsService;

#[RestrictToWopiServer]
class WopiController extends Controller {
Expand Down Expand Up @@ -84,6 +85,7 @@ public function __construct(
private IGroupManager $groupManager,
private ILockManager $lockManager,
private IEventDispatcher $eventDispatcher,
private SettingsService $settingsService,
) {
parent::__construct($appName, $request);
}
Expand All @@ -97,9 +99,20 @@ public function __construct(
#[FrontpageRoute(verb: 'GET', url: 'wopi/files/{fileId}')]
public function checkFileInfo(string $fileId, string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);

// TODO: condition for $wopi not found?

if ($fileId == "-1" && $wopi->getTokenType() == WOPI::TOKEN_TYPE_SETTING_AUTH) {
$response = [
"usersettings" => 'DONE',
];

return new JSONResponse($response);
}

[$fileId, , $version] = Helper::parseFileId($fileId);

$wopi = $this->wopiMapper->getWopiForToken($access_token);
$file = $this->getFileForWopiToken($wopi);
if (!($file instanceof File)) {
throw new NotFoundException('No valid file found for ' . $fileId);
Expand Down Expand Up @@ -353,6 +366,47 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
}
}

#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')]
public function handleSettingsFile(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);
}

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

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


$newFileName = 'settings-' . uniqid() . '.json';

$result = $this->settingsService->uploadSystemFile($newFileName, $fileContent);

return new JSONResponse([
'status' => 'success',
'filename' => $newFileName,
'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);
}
}


/**
* Given an access token and a fileId, replaces the files with the request body.
* Expects a valid token in access_token parameter.
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/Wopi.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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
28 changes: 28 additions & 0 deletions lib/Db/WopiMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ public function generateFileToken($fileId, $owner, $editor, $version, $updatable
return $wopi;
}

public function generateUserSettingsToken($fileId, $owner, $editor, $version, $updatable, $serverHost, ?string $guestDisplayname = null, $hideDownload = false, $direct = false, $templateId = 0, $share = null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably can simplify the signature of this method a lot. Most of it is passed in as dummy/default values

$token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);

$wopi = Wopi::fromParams([
'fileid' => $fileId,
'ownerUid' => $owner,
'editorUid' => $editor,
'version' => $version,
'canwrite' => $updatable,
'serverHost' => $serverHost,
'token' => $token,
'expiry' => $this->calculateNewTokenExpiry(),
'guestDisplayname' => $guestDisplayname,
'hideDownload' => $hideDownload,
'direct' => $direct,
'templateId' => $templateId,
'remoteServer' => '',
'remoteServerToken' => '',
'share' => $share,
'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
Loading