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

ExApp UI Implementation (Part1) #135

Merged
merged 34 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
46d095f
initial draft
bigcat88 Nov 28, 2023
935fa0d
part2: proxying JS files from ExApp
bigcat88 Nov 29, 2023
334cb4a
do not ignore js/subfolders
bigcat88 Nov 29, 2023
6028bc0
fixed typo
bigcat88 Nov 29, 2023
996b7a5
db: max `path` length 2000 chars -> 762
bigcat88 Nov 29, 2023
46ed9e2
db: max `path` length 762 chars -> 512
bigcat88 Nov 29, 2023
b8e0aa0
changed table name, refactored Controller
bigcat88 Nov 29, 2023
e315d40
load empty template
andrey18106 Nov 29, 2023
98c30ab
MenuEntry->TopMenu, added `name` to all UI tables
bigcat88 Nov 30, 2023
0821241
Fixed psalm annotations
bigcat88 Nov 30, 2023
aedc157
dev, sync
bigcat88 Nov 30, 2023
048d680
corrected ExApp icon handling
bigcat88 Nov 30, 2023
bfb0cc3
added Top Menu OCS commands, part1
bigcat88 Dec 1, 2023
66586ba
added OCS GET to FileActionsMenu
bigcat88 Dec 1, 2023
a921063
security: do not set user for GET proxy requests for ExApp
bigcat88 Dec 1, 2023
d7a1893
fix: set ExApp menu entry active state manually
andrey18106 Dec 1, 2023
383a4cb
fixed small bug in removeByAppIdName
bigcat88 Dec 1, 2023
6b7a0d0
trigger url if backslash in the end
bigcat88 Dec 1, 2023
9afb99d
allowed subroutes
bigcat88 Dec 1, 2023
b80683e
dev, pre-final part
bigcat88 Dec 2, 2023
5e67fda
remove fileActions during uninstall, corrected cache cleaning during …
bigcat88 Dec 2, 2023
11f81ad
fixed cyclic import
bigcat88 Dec 2, 2023
abcf220
added OCS routes for initialstates, script, styles
bigcat88 Dec 3, 2023
9713763
getAppId->getAppid, fixed typo bug in InitialState.php
bigcat88 Dec 3, 2023
1aa1307
composer cs:fix
bigcat88 Dec 3, 2023
d5d2b50
table ex_apps_ui_state -> ex_apps_ui_states
bigcat88 Dec 3, 2023
83c1cd4
when NotFound, do not spam errors in the log
bigcat88 Dec 3, 2023
e76c80d
added default value for setExAppScript: afterAppId
bigcat88 Dec 3, 2023
7de1f81
Scripts API: finished
bigcat88 Dec 3, 2023
81e2382
Styles API: finished
bigcat88 Dec 3, 2023
33b58cc
Merge branch 'main' into menu-entries-ui
bigcat88 Dec 3, 2023
d7340a3
`array()->`[]`
bigcat88 Dec 3, 2023
8035b7a
all new UI OCS: NoAdminRequired -> PublicPage
bigcat88 Dec 3, 2023
b6eb66c
fixed error: appId is not a valid attribute
bigcat88 Dec 3, 2023
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/js/
/js/*.*
.code-workspace
.DS_Store
.idea/
Expand Down
77 changes: 55 additions & 22 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@
return [
'routes' => [
// AppAPI admin settings
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],
['name' => 'Config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],

// Menu Entries
['name' => 'TopMenu#viewExAppPage',
'url' => '/embedded/{appId}/{name}/{other}', 'verb' => 'GET' , 'root' => '/embedded',
'requirements' => ['other' => '.*'], 'defaults' => ['other' => '']],

// Proxy
['name' => 'ExAppProxy#ExAppGet',
'url' => '/proxy/{appId}/{other}', 'verb' => 'GET' , 'root' => '/proxy',
'requirements' => ['other' => '.+'], 'defaults' => ['other' => '']],
['name' => 'ExAppProxy#ExAppPost',
'url' => '/proxy/{appId}/{other}', 'verb' => 'POST' , 'root' => '/proxy',
'requirements' => ['other' => '.+'], 'defaults' => ['other' => '']],
['name' => 'ExAppProxy#ExAppPut',
'url' => '/proxy/{appId}/{other}', 'verb' => 'PUT' , 'root' => '/proxy',
'requirements' => ['other' => '.+'], 'defaults' => ['other' => '']],

// ExApps actions
['name' => 'ExAppsPage#viewApps', 'url' => '/apps', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#viewApps', 'url' => '/apps', 'verb' => 'GET' , 'root' => '/apps'],
['name' => 'ExAppsPage#listCategories', 'url' => '/apps/categories', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#listApps', 'url' => '/apps/list', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#enableApp', 'url' => '/apps/enable/{appId}', 'verb' => 'GET' , 'root' => ''],
Expand All @@ -24,11 +40,11 @@
['name' => 'ExAppsPage#force', 'url' => '/apps/force', 'verb' => 'POST' , 'root' => ''],

// DaemonConfig actions
['name' => 'daemonConfig#getAllDaemonConfigs', 'url' => '/daemons', 'verb' => 'GET'],
['name' => 'daemonConfig#registerDaemonConfig', 'url' => '/daemons', 'verb' => 'POST'],
['name' => 'daemonConfig#unregisterDaemonConfig', 'url' => '/daemons/{name}', 'verb' => 'DELETE'],
['name' => 'daemonConfig#verifyDaemonConnection', 'url' => '/daemons/{name}/check', 'verb' => 'POST'],
['name' => 'daemonConfig#updateDaemonConfig', 'url' => '/daemons', 'verb' => 'PUT'],
['name' => 'DaemonConfig#getAllDaemonConfigs', 'url' => '/daemons', 'verb' => 'GET'],
['name' => 'DaemonConfig#registerDaemonConfig', 'url' => '/daemons', 'verb' => 'POST'],
['name' => 'DaemonConfig#unregisterDaemonConfig', 'url' => '/daemons/{name}', 'verb' => 'DELETE'],
['name' => 'DaemonConfig#verifyDaemonConnection', 'url' => '/daemons/{name}/check', 'verb' => 'POST'],
['name' => 'DaemonConfig#updateDaemonConfig', 'url' => '/daemons', 'verb' => 'PUT'],
],
'ocs' => [
// Logging
Expand All @@ -43,27 +59,44 @@
// ExApps actions
['name' => 'OCSExApp#setExAppEnabled', 'url' => '/api/v1/ex-app/{appId}/enabled', 'verb' => 'PUT'],

// File Actions Menu
['name' => 'ExFileActionsMenu#registerFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'POST'],
['name' => 'ExFileActionsMenu#unregisterFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'DELETE'],
['name' => 'ExFileActionsMenu#handleFileAction', 'url' => '/api/v1/files/action', 'verb' => 'POST'],
['name' => 'ExFileActionsMenu#loadFileActionIcon', 'url' => '/api/v1/files/action/icon', 'verb' => 'GET'],

// appconfig_ex (app configuration)
['name' => 'appConfig#setAppConfigValue', 'url' => '/api/v1/ex-app/config', 'verb' => 'POST'],
['name' => 'appConfig#getAppConfigValues', 'url' => '/api/v1/ex-app/config/get-values', 'verb' => 'POST'],
['name' => 'appConfig#deleteAppConfigValues', 'url' => '/api/v1/ex-app/config', 'verb' => 'DELETE'],
['name' => 'AppConfig#setAppConfigValue', 'url' => '/api/v1/ex-app/config', 'verb' => 'POST'],
['name' => 'AppConfig#getAppConfigValues', 'url' => '/api/v1/ex-app/config/get-values', 'verb' => 'POST'],
['name' => 'AppConfig#deleteAppConfigValues', 'url' => '/api/v1/ex-app/config', 'verb' => 'DELETE'],

// preferences_ex (user-specific configuration)
['name' => 'preferences#setUserConfigValue', 'url' => '/api/v1/ex-app/preference', 'verb' => 'POST'],
['name' => 'preferences#getUserConfigValues', 'url' => '/api/v1/ex-app/preference/get-values', 'verb' => 'POST'],
['name' => 'preferences#deleteUserConfigValues', 'url' => '/api/v1/ex-app/preference', 'verb' => 'DELETE'],
['name' => 'Preferences#setUserConfigValue', 'url' => '/api/v1/ex-app/preference', 'verb' => 'POST'],
['name' => 'Preferences#getUserConfigValues', 'url' => '/api/v1/ex-app/preference/get-values', 'verb' => 'POST'],
['name' => 'Preferences#deleteUserConfigValues', 'url' => '/api/v1/ex-app/preference', 'verb' => 'DELETE'],

// Notifications
['name' => 'notifications#sendNotification', 'url' => '/api/v1/notification', 'verb' => 'POST'],
['name' => 'Notifications#sendNotification', 'url' => '/api/v1/notification', 'verb' => 'POST'],

// Talk bots
['name' => 'talkBot#registerExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'POST'],
['name' => 'talkBot#unregisterExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'DELETE'],
['name' => 'TalkBot#registerExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'POST'],
['name' => 'TalkBot#unregisterExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'DELETE'],

// File Actions Menu
['name' => 'OCSUi#registerFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'POST'],
['name' => 'OCSUi#unregisterFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'DELETE'],
['name' => 'OCSUi#getFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'GET'],
['name' => 'OCSUi#handleFileAction', 'url' => '/api/v1/files/action', 'verb' => 'POST'],
['name' => 'OCSUi#loadFileActionIcon', 'url' => '/api/v1/files/action/icon', 'verb' => 'GET'],

// Top Menu
['name' => 'OCSUi#registerExAppMenuEntry', 'url' => '/api/v1/ui/top-menu', 'verb' => 'POST'],
['name' => 'OCSUi#unregisterExAppMenuEntry', 'url' => '/api/v1/ui/top-menu', 'verb' => 'DELETE'],
['name' => 'OCSUi#getExAppMenuEntry', 'url' => '/api/v1/ui/top-menu', 'verb' => 'GET'],

//Common UI
['name' => 'OCSUi#setExAppInitialState', 'url' => '/api/v1/ui/initial-state', 'verb' => 'POST'],
['name' => 'OCSUi#deleteExAppInitialState', 'url' => '/api/v1/ui/initial-state', 'verb' => 'DELETE'],
['name' => 'OCSUi#getExAppInitialState', 'url' => '/api/v1/ui/initial-state', 'verb' => 'GET'],
['name' => 'OCSUi#setExAppScript', 'url' => '/api/v1/ui/script', 'verb' => 'POST'],
['name' => 'OCSUi#deleteExAppScript', 'url' => '/api/v1/ui/script', 'verb' => 'DELETE'],
['name' => 'OCSUi#getExAppScript', 'url' => '/api/v1/ui/script', 'verb' => 'GET'],
['name' => 'OCSUi#setExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'POST'],
['name' => 'OCSUi#deleteExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'DELETE'],
['name' => 'OCSUi#getExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'GET'],
],
];
Empty file added js/proxy_js/0.js
Empty file.
Empty file added js/proxy_js/1.js
Empty file.
Empty file added js/proxy_js/2.js
Empty file.
Empty file added js/proxy_js/3.js
Empty file.
Empty file added js/proxy_js/4.js
Empty file.
Empty file added js/proxy_js/5.js
Empty file.
Empty file added js/proxy_js/6.js
Empty file.
Empty file added js/proxy_js/7.js
Empty file.
Empty file added js/proxy_js/8.js
Empty file.
Empty file added js/proxy_js/9.js
Empty file.
10 changes: 10 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
use OCA\AppAPI\Listener\SabrePluginAuthInitListener;
use OCA\AppAPI\Listener\UserDeletedListener;
use OCA\AppAPI\Middleware\AppAPIAuthMiddleware;
use OCA\AppAPI\Middleware\ExAppUiMiddleware;
use OCA\AppAPI\Notifications\ExAppAdminNotifier;
use OCA\AppAPI\Notifications\ExAppNotifier;
use OCA\AppAPI\Profiler\AppAPIDataCollector;
use OCA\AppAPI\PublicCapabilities;

use OCA\AppAPI\Service\TopMenuService;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\AppFramework\App;
Expand Down Expand Up @@ -53,6 +55,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(Capabilities::class);
$context->registerCapability(PublicCapabilities::class);
$context->registerMiddleware(AppAPIAuthMiddleware::class);
$context->registerMiddleware(ExAppUiMiddleware::class);
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerNotifierService(ExAppNotifier::class);
Expand All @@ -67,6 +70,7 @@ public function boot(IBootContext $context): void {
$profiler->add(new AppAPIDataCollector());
}
$context->injectFn($this->registerExAppsManagementNavigation(...));
$context->injectFn($this->registerExAppsMenuEntries(...));
} catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable) {
}
}
Expand Down Expand Up @@ -111,4 +115,10 @@ private function registerExAppsManagementNavigation(IUserSession $userSession):
});
}
}

private function registerExAppsMenuEntries(): void {
$container = $this->getContainer();
$menuEntryService = $container->get(TopMenuService::class);
$menuEntryService->registerMenuEntries($container);
}
}
2 changes: 1 addition & 1 deletion lib/BackgroundJob/ExAppInitStatusCheckJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected function run($argument): void {
}
if (($status['init_start_time'] + $initTimeoutMinutes * 60) > time()) {
$this->service->setAppInitProgress(
$exApp->getAppId(), 0, sprintf('ExApp %s initialization timed out (%sm)', $exApp->getAppid(), $initTimeoutMinutes * 60)
$exApp->getAppid(), 0, sprintf('ExApp %s initialization timed out (%sm)', $exApp->getAppid(), $initTimeoutMinutes * 60)
);
}
}
Expand Down
146 changes: 146 additions & 0 deletions lib/Controller/ExAppProxyController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Controller;

use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\ProxyResponse;
use OCA\AppAPI\Service\AppAPIService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\Files\IMimeTypeDetector;
use OCP\Http\Client\IResponse;
use OCP\IRequest;

class ExAppProxyController extends Controller {

public function __construct(
IRequest $request,
private AppAPIService $service,
private IMimeTypeDetector $mimeTypeHelper,
private ContentSecurityPolicyNonceManager $nonceManager,
private ?string $userId,
) {
parent::__construct(Application::APP_ID, $request);
}

private function createProxyResponse(string $path, IResponse $response, $cache = true): ProxyResponse {
$content = $response->getBody();
$isHTML = pathinfo($path, PATHINFO_EXTENSION) === 'html';
if ($isHTML) {
$nonce = $this->nonceManager->getNonce();
$content = str_replace(
'<script',
"<script nonce=\"$nonce\"",
$content
);
}

$mime = $response->getHeader('content-type');
if (empty($mime)) {
$mime = $this->mimeTypeHelper->detectPath($path);
if (pathinfo($path, PATHINFO_EXTENSION) === 'wasm') {
$mime = 'application/wasm';
}
}

$proxyResponse = new ProxyResponse(
data: $content,
length: strlen($content),
mimeType: $mime,
);

$headersToCopy = ['Content-Disposition', 'Last-Modified', 'Etag'];
foreach ($headersToCopy as $element) {
$headerValue = $response->getHeader($element);
if (empty($headerValue)) {
$proxyResponse->addHeader($element, $headerValue);
}
}

if ($cache && !$isHTML) {
$proxyResponse->cacheFor(3600);
}

$csp = new ContentSecurityPolicy();
$csp->addAllowedScriptDomain($this->request->getServerHost());
$csp->addAllowedScriptDomain('\'unsafe-eval\'');
$csp->addAllowedScriptDomain('\'unsafe-inline\'');
$csp->addAllowedFrameDomain($this->request->getServerHost());
$proxyResponse->setContentSecurityPolicy($csp);
return $proxyResponse;
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function ExAppGet(string $appId, string $other): Response {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}

$response = $this->service->requestToExApp(
$exApp, '/' . $other, $this->userId, 'GET', request: $this->request
);
if (is_array($response)) {
$error_response = new Response();
return $error_response->setStatus(500);
}
return $this->createProxyResponse($other, $response);
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function ExAppPost(string $appId, string $other): Response {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}

$response = $this->service->aeRequestToExApp(
$exApp, '/' . $other, $this->userId,
params: $this->request->getParams(),
request: $this->request
);
if (is_array($response)) {
$error_response = new Response();
return $error_response->setStatus(500);
}
return $this->createProxyResponse($other, $response);
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function ExAppPut(string $appId, string $other): Response {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}

$response = $this->service->aeRequestToExApp(
$exApp, '/' . $other, $this->userId, 'PUT',
params: $this->request->getParams(),
request: $this->request
);
if (is_array($response)) {
$error_response = new Response();
return $error_response->setStatus(500);
}
return $this->createProxyResponse($other, $response);
}
}
Loading