Skip to content

Commit

Permalink
ExApp UI Implementation (Part1) (#135)
Browse files Browse the repository at this point in the history
This PR adds ability for ExApp to have entry in TopMenu and provide it's
own page when clicking on it.

* OCS API for specifying InitialStates, Scripts, Styles, TopMenu
* Small corrections and refactoring of code that relates on UI
* Small bug fixes to other parts of AppAPI, mostly for FileActions Menu.

PR is ready, after merging this, one additional PR will be created that
relates to this one.
That PR will cover missing parts:
* Docs, changelog update
* FileActions Menu rework(icon specifying)
* Fixes of stuff that will be found if any, related to UI
* CI Actions and Makefile adjusting, to keep `js/proxy_js` folder
* Fix of Proxying stuff

Merging this PR will allow nc_py_api's PR to be finished on this theme
and finish first example for testing.

---------

Signed-off-by: Alexander Piskun <[email protected]>
Signed-off-by: Andrey Borysenko <[email protected]>
Co-authored-by: Andrey Borysenko <[email protected]>
  • Loading branch information
bigcat88 and andrey18106 authored Dec 3, 2023
1 parent 947c0ca commit 5027722
Show file tree
Hide file tree
Showing 39 changed files with 2,158 additions and 222 deletions.
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

0 comments on commit 5027722

Please sign in to comment.