Skip to content

Commit

Permalink
initial draft
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <[email protected]>
  • Loading branch information
bigcat88 committed Nov 28, 2023
1 parent 480d59c commit 46d095f
Show file tree
Hide file tree
Showing 19 changed files with 1,257 additions and 1 deletion.
20 changes: 19 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,26 @@
// AppAPI admin settings
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],

// Menu Entries
['name' => 'MenuEntry#viewExAppPage', 'url' => '/embedded/{appId}/{name}', 'verb' => 'GET' , 'root' => '/embedded'],
['name' => 'MenuEntry#ExAppIcon', 'url' => '/embedded/{appId}/{name}/icon', 'verb' => 'GET' , 'root' => '/embedded'],

// Proxy
// ['name' => 'MenuEntry#ExAppProxySubLinksGet',
// 'url' => '/proxying/{appId}/{other}', 'verb' => 'GET' , 'root' => '/proxying',
// 'requirements' => array('other' => '.+'), 'defaults' => array('other' => '')],
// ['name' => 'MenuEntry#ExAppProxySubLinksPost',
// 'url' => '/proxying/{appId}/{other}', 'verb' => 'POST' , 'root' => '/proxying',
// 'requirements' => array('other' => '.+'), 'defaults' => array('other' => '')],
// ['name' => 'MenuEntry#ExAppProxySubLinksPut',
// 'url' => '/proxying/{appId}/{other}', 'verb' => 'PUT' , 'root' => '/proxying',
// 'requirements' => array('other' => '.+'), 'defaults' => array('other' => '')],
['name' => 'MenuEntry#ExAppProxySubLinksGet',
'url' => '/proxy/css/{appId}/{other}', 'verb' => 'GET' , 'root' => '',
'requirements' => array('other' => '.+'), 'defaults' => array('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 Down
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\ExAppMenuEntryService;
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(ExAppMenuEntryService::class);
$menuEntryService->registerMenuEntries($container);
}
}
318 changes: 318 additions & 0 deletions lib/Controller/MenuEntryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Controller;

use DOMDocument;
use DOMException;
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\AppAPI\ProxyResponse;
use OCA\AppAPI\Service\AppAPIService;
use OCA\AppAPI\Service\ExAppMenuEntryService;
use OCA\AppAPI\Service\ExAppInitialStateService;
use OCA\AppAPI\Service\ExAppScriptsService;
use OCA\AppAPI\Service\ExAppStylesService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http as HttpAlias;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\DB\Exception;
use OCP\Files\IMimeTypeDetector;
use OCP\Http\Client\IResponse;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Util;

class MenuEntryController extends Controller {

public function __construct(
IRequest $request,
private IInitialState $initialState,
private IURLGenerator $url,
private ExAppMenuEntryService $menuEntryService,
private ExAppInitialStateService $initialStateService,
private ExAppScriptsService $scriptsService,
private ExAppStylesService $stylesService,
private AppAPIService $service,
private IMimeTypeDetector $mimeTypeHelper,
private ContentSecurityPolicyNonceManager $nonceManager,

Check failure on line 48 in lib/Controller/MenuEntryController.php

View workflow job for this annotation

GitHub Actions / php-psalm-analysis (8.1)

UndefinedClass

lib/Controller/MenuEntryController.php:48:3: UndefinedClass: Class, interface or enum named OC\Security\CSP\ContentSecurityPolicyNonceManager does not exist (see https://psalm.dev/019)
private ?string $userId,
) {
parent::__construct(Application::APP_ID, $request);
}

/**
* @NoCSRFRequired
* @NoAdminRequired
* @throws Exception
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function viewExAppPage(string $appId, string $name): TemplateResponse {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}
$menuEntry = $this->menuEntryService->getExAppMenuEntry($appId, $name);
if ($menuEntry === null) {
return new NotFoundResponse();
}
$initialStates = $this->initialStateService->getExAppInitialStates($appId, 'top_menu');
foreach ($initialStates as $key => $value) {
$this->initialState->provideInitialState($key, $value);
}
$this->scriptsService->applyExAppScripts($appId, 'top_menu');
$this->stylesService->applyExAppStyles($appId, 'top_menu');

$response = new TemplateResponse(Application::APP_ID, 'main');
return $response;
}

/**
* @NoCSRFRequired
* @NoAdminRequired
* @throws DOMException
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function ExAppIframeProxy(string $appId, string $name): Response {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}
$menuEntry = $this->menuEntryService->getExAppMenuEntry($appId, $name);
if ($menuEntry === null) {
return new NotFoundResponse();
}
$response = $this->service->aeRequestToExApp(
$exApp, $menuEntry->getRoute(), $this->userId, 'GET', request: $this->request
);
if (is_array($response)) {
$error_response = new Response();
return $error_response->setStatus(500);
}
$reHeaders = [];
foreach ($response->getHeaders() as $k => $values) {
$reHeaders[$k] = count($values) === 1 ? $values[0] : $values;
}
$reHeaders['content-security-policy'] = 'frame-ancestors *;';

$dom = new DOMDocument();
@$dom->loadHTML($response->getBody(), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$base = $dom->createElement('base');
$base->setAttribute(
'href',
$this->url->getAbsoluteURL('index.php/apps/app_api/proxying/'. $exApp->getAppid() . '/')
);
$base->setAttribute('target', '_parent');
$head = $dom->getElementsByTagName('head')->item(0);
if ($head) {
if ($head->hasChildNodes()) {
$head->insertBefore($base, $head->firstChild);
} else {
$head->appendChild($base);
}
}
return new DataDisplayResponse($dom->saveHTML(), $response->getStatusCode(), $reHeaders);
}

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();

Check failure on line 139 in lib/Controller/MenuEntryController.php

View workflow job for this annotation

GitHub Actions / php-psalm-analysis (8.1)

UndefinedClass

lib/Controller/MenuEntryController.php:139:13: UndefinedClass: Class, interface or enum named OC\Security\CSP\ContentSecurityPolicyNonceManager does not exist (see https://psalm.dev/019)
$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 ExAppProxyCss(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, '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 ExAppProxySubLinksGet(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, '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 ExAppIframeProxySubLinksPost(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 ExAppIframeProxySubLinksPut(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',
$this->request->getParams(),
request: $this->request
);
if (is_array($response)) {
$error_response = new Response();
return $error_response->setStatus(500);
}
return $this->createProxyResponse($other, $response);
}

/**
* @NoCSRFRequired
* @NoAdminRequired
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function ExAppIconProxy(string $appId, string $name): Response {
$exApp = $this->service->getExApp($appId);
if ($exApp === null) {
return new NotFoundResponse();
}
if (!$exApp->getEnabled()) {
return new NotFoundResponse();
}
$icon = $this->menuEntryService->loadFileActionIcon($appId, $name, $exApp, $this->request, $this->userId);
if ($icon !== null && isset($icon['body'], $icon['headers'])) {
$response = new DataDisplayResponse(
$icon['body'],
HttpAlias::STATUS_OK,
['Content-Type' => $icon['headers']['Content-Type'][0] ?? 'image/svg+xml']
);
$response->cacheFor(ExAppMenuEntryService::ICON_CACHE_TTL, false, true);
return $response;
}
return new DataDisplayResponse('', 400);
}

/**
* @NoCSRFRequired
* @NoAdminRequired
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[AppAPIAuth]
public function registerExAppMenuEntry(string $name, string $displayName, string $route, string $iconUrl = '', int $adminRequired = 0): DataResponse {
return new DataResponse();
}

/**
* @NoCSRFRequired
* @NoAdminRequired
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[AppAPIAuth]
public function unregisterExAppMenuEntry(string $name): DataResponse {
return new DataResponse();
}
}
Loading

0 comments on commit 46d095f

Please sign in to comment.