From 86ae7439c1984ae73712a716bf2b31946e257b0f Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:45:38 +0300 Subject: [PATCH] UI: FileActions rework (#141) * changed type of `order` to int in model --------- Signed-off-by: Andrey Borysenko Co-authored-by: Andrey Borysenko --- .github/workflows/tests-deploy.yml | 10 +- appinfo/routes.php | 9 +- css/filesactions.css | 5 + lib/AppInfo/Application.php | 4 +- lib/Controller/OCSUiController.php | 151 ++++------------ lib/Controller/TopMenuController.php | 26 +-- lib/Db/UI/FilesActionsMenu.php | 14 +- lib/Db/UI/FilesActionsMenuMapper.php | 1 - lib/Listener/LoadFilesPluginListener.php | 12 +- lib/Migration/Version1003Date202311061844.php | 5 + lib/Service/AppAPIService.php | 52 +++--- lib/Service/ExAppApiScopeService.php | 2 +- .../FilesActionsMenuService.php} | 83 +++------ .../InitialStateService.php} | 4 +- .../ScriptsService.php} | 16 +- .../StylesService.php} | 17 +- lib/Service/{ => UI}/TopMenuService.php | 16 +- src/filesplugin.js | 168 +++++++++++------- 18 files changed, 254 insertions(+), 341 deletions(-) rename lib/Service/{ExFilesActionsMenuService.php => UI/FilesActionsMenuService.php} (61%) rename lib/Service/{ExAppInitialStateService.php => UI/InitialStateService.php} (97%) rename lib/Service/{ExAppScriptsService.php => UI/ScriptsService.php} (91%) rename lib/Service/{ExAppStylesService.php => UI/StylesService.php} (85%) rename lib/Service/{ => UI}/TopMenuService.php (92%) diff --git a/.github/workflows/tests-deploy.yml b/.github/workflows/tests-deploy.yml index e04c341f..12f6ce05 100644 --- a/.github/workflows/tests-deploy.yml +++ b/.github/workflows/tests-deploy.yml @@ -148,7 +148,7 @@ jobs: runs-on: ubuntu-22.04 name: NC In Julius Docker • 🐘8.1 env: - docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:latest + docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:20231202-1 steps: - name: Set app env @@ -224,7 +224,7 @@ jobs: runs-on: ubuntu-22.04 name: NC In Julius Docker(Docker by port) • 🐘8.1 env: - docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:latest + docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:20231202-1 steps: - name: Set app env @@ -238,7 +238,7 @@ jobs: docker run -d -p 8443:443 -v /var/run/docker.sock:/var/run/docker.sock:ro \ --env CREATE_CERTS_WITH_PW=supersecret --env CERT_HOSTNAME=host.docker.internal \ -v `pwd`/certs:/data/certs kekru/docker-remote-api-tls:master - sleep 30s + sleep 60s - name: Install AppAPI run: | @@ -305,7 +305,7 @@ jobs: runs-on: ubuntu-22.04 name: NC In Julius Docker(APP by hostname) • 🐘8.1 env: - docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:latest + docker-image: ghcr.io/juliushaertl/nextcloud-dev-php81:20231202-1 steps: - name: Set app env @@ -318,7 +318,7 @@ jobs: docker run -d -p 8443:443 -v /var/run/docker.sock:/var/run/docker.sock:ro \ --env CREATE_CERTS_WITH_PW=supersecret --env CERT_HOSTNAME=host.docker.internal \ -v `pwd`/certs:/data/certs kekru/docker-remote-api-tls:master - sleep 30s + sleep 60s - name: Install AppAPI run: | diff --git a/appinfo/routes.php b/appinfo/routes.php index 9962eb5b..8c221373 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -76,12 +76,11 @@ ['name' => 'TalkBot#registerExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'POST'], ['name' => 'TalkBot#unregisterExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'DELETE'], + // --- UI --- // 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'], + ['name' => 'OCSUi#registerFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'POST'], + ['name' => 'OCSUi#unregisterFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'DELETE'], + ['name' => 'OCSUi#getFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'GET'], // Top Menu ['name' => 'OCSUi#registerExAppMenuEntry', 'url' => '/api/v1/ui/top-menu', 'verb' => 'POST'], diff --git a/css/filesactions.css b/css/filesactions.css index 67b9de28..69b4df02 100644 --- a/css/filesactions.css +++ b/css/filesactions.css @@ -2,3 +2,8 @@ background-image: url('../img/app-dark.svg'); filter: var(--background-invert-if-dark); } + +/* For Nextcloud 27 */ +.menuitem.action > img.icon { + filter: var(--background-invert-if-dark); +} diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f72fb4bb..5d12ce06 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,8 +15,7 @@ use OCA\AppAPI\Notifications\ExAppNotifier; use OCA\AppAPI\Profiler\AppAPIDataCollector; use OCA\AppAPI\PublicCapabilities; - -use OCA\AppAPI\Service\TopMenuService; +use OCA\AppAPI\Service\UI\TopMenuService; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCP\AppFramework\App; @@ -32,7 +31,6 @@ use OCP\IUserSession; use OCP\Profiler\IProfiler; use OCP\SabrePluginEvent; - use OCP\User\Events\UserDeletedEvent; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; diff --git a/lib/Controller/OCSUiController.php b/lib/Controller/OCSUiController.php index aa4ba4f8..a2711c17 100644 --- a/lib/Controller/OCSUiController.php +++ b/lib/Controller/OCSUiController.php @@ -6,41 +6,30 @@ use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Attribute\AppAPIAuth; -use OCA\AppAPI\Service\AppAPIService; -use OCA\AppAPI\Service\ExAppInitialStateService; -use OCA\AppAPI\Service\ExAppScriptsService; -use OCA\AppAPI\Service\ExAppStylesService; -use OCA\AppAPI\Service\ExFilesActionsMenuService; - -use OCA\AppAPI\Service\TopMenuService; +use OCA\AppAPI\Service\UI\FilesActionsMenuService; +use OCA\AppAPI\Service\UI\InitialStateService; +use OCA\AppAPI\Service\UI\ScriptsService; +use OCA\AppAPI\Service\UI\StylesService; +use OCA\AppAPI\Service\UI\TopMenuService; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\PublicPage; -use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; -use OCP\Http\Client\IResponse; -use OCP\IConfig; use OCP\IRequest; -use Psr\Log\LoggerInterface; class OCSUiController extends OCSController { protected $request; public function __construct( - IRequest $request, - private readonly ?string $userId, - private readonly ExFilesActionsMenuService $exFilesActionsMenuService, - private readonly TopMenuService $menuEntryService, - private readonly ExAppInitialStateService $initialStateService, - private readonly ExAppScriptsService $scriptsService, - private readonly ExAppStylesService $stylesService, - private readonly AppAPIService $appAPIService, - private readonly IConfig $config, - private readonly LoggerInterface $logger, + IRequest $request, + private readonly FilesActionsMenuService $filesActionsMenuService, + private readonly TopMenuService $menuEntryService, + private readonly InitialStateService $initialStateService, + private readonly ScriptsService $scriptsService, + private readonly StylesService $stylesService, ) { parent::__construct(Application::APP_ID, $request); @@ -51,27 +40,35 @@ public function __construct( * @PublicPage * @NoCSRFRequired * - * @param array $fileActionMenuParams [name, display_name, mime, permissions, order, icon, icon_class, action_handler] - * + * @param string $name + * @param string $displayName + * @param string $actionHandler + * @param string $icon + * @param string $mime + * @param int $permissions + * @param int $order * @return DataResponse + * @throws OCSBadRequestException */ #[AppAPIAuth] #[PublicPage] #[NoCSRFRequired] - public function registerFileActionMenu(array $fileActionMenuParams): DataResponse { - $registeredFileActionMenu = $this->exFilesActionsMenuService->registerFileActionMenu( - $this->request->getHeader('EX-APP-ID'), $fileActionMenuParams); - return new DataResponse([ - 'success' => $registeredFileActionMenu !== null, - 'registeredFileActionMenu' => $registeredFileActionMenu, - ], Http::STATUS_OK); + public function registerFileActionMenu(string $name, string $displayName, string $actionHandler, + string $icon = "", string $mime = "file", int $permissions = 31, + int $order = 0): DataResponse { + $result = $this->filesActionsMenuService->registerFileActionMenu( + $this->request->getHeader('EX-APP-ID'), $name, $displayName, $actionHandler, $icon, $mime, $permissions, $order); + if (!$result) { + throw new OCSBadRequestException("File Action Menu entry could not be registered"); + } + return new DataResponse(); } /** * @PublicPage * @NoCSRFRequired * - * @param string $fileActionMenuName + * @param string $name * * @throws OCSNotFoundException * @return DataResponse @@ -79,9 +76,9 @@ public function registerFileActionMenu(array $fileActionMenuParams): DataRespons #[AppAPIAuth] #[PublicPage] #[NoCSRFRequired] - public function unregisterFileActionMenu(string $fileActionMenuName): DataResponse { - $unregisteredFileActionMenu = $this->exFilesActionsMenuService->unregisterFileActionMenu( - $this->request->getHeader('EX-APP-ID'), $fileActionMenuName); + public function unregisterFileActionMenu(string $name): DataResponse { + $unregisteredFileActionMenu = $this->filesActionsMenuService->unregisterFileActionMenu( + $this->request->getHeader('EX-APP-ID'), $name); if ($unregisteredFileActionMenu === null) { throw new OCSNotFoundException('FileActionMenu not found'); } @@ -97,7 +94,7 @@ public function unregisterFileActionMenu(string $fileActionMenuName): DataRespon #[PublicPage] #[NoCSRFRequired] public function getFileActionMenu(string $name): DataResponse { - $result = $this->exFilesActionsMenuService->getExAppFileAction( + $result = $this->filesActionsMenuService->getExAppFileAction( $this->request->getHeader('EX-APP-ID'), $name); if (!$result) { throw new OCSNotFoundException('FileActionMenu not found'); @@ -310,86 +307,4 @@ public function getExAppStyle(string $type, string $name, string $path): DataRes } return new DataResponse($result, Http::STATUS_OK); } - - /** - * @NoCSRFRequired - * @NoAdminRequired - * - * @param string $appId - * @param string $actionName - * @param array $actionFile - * @param string $actionHandler - * - * @return DataResponse - */ - #[NoAdminRequired] - #[NoCSRFRequired] - public function handleFileAction(string $appId, string $actionName, array $actionFile, string $actionHandler): DataResponse { - $result = false; - $exFileAction = $this->exFilesActionsMenuService->getExAppFileAction($appId, $actionName); - if ($exFileAction !== null) { - $handler = $exFileAction->getActionHandler(); // route on ex app - $params = [ - 'actionName' => $actionName, - 'actionHandler' => $actionHandler, - 'actionFile' => [ - 'fileId' => $actionFile['fileId'], - 'name' => $actionFile['name'], - 'directory' => $actionFile['directory'], - 'etag' => $actionFile['etag'], - 'mime' => $actionFile['mime'], - 'fileType' => $actionFile['fileType'], - 'mtime' => $actionFile['mtime'] / 1000, // convert ms to s - 'size' => intval($actionFile['size']), - 'favorite' => $actionFile['favorite'] ?? "false", - 'permissions' => $actionFile['permissions'], - 'shareOwner' => $actionFile['shareOwner'] ?? null, - 'shareOwnerId' => $actionFile['shareOwnerId'] ?? null, - 'shareTypes' => $actionFile['shareTypes'] ?? null, - 'shareAttributes' => $actionFile['shareAttributes'] ?? null, - 'sharePermissions' => $actionFile['sharePermissions'] ?? null, - 'userId' => $this->userId, - 'instanceId' => $this->config->getSystemValue('instanceid', null), - ], - ]; - $exApp = $this->appAPIService->getExApp($appId); - if ($exApp !== null) { - $result = $this->appAPIService->aeRequestToExApp($exApp, $handler, $this->userId, 'POST', $params, [], $this->request); - if ($result instanceof IResponse) { - $result = $result->getStatusCode() === 200; - } elseif (isset($result['error'])) { - $this->logger->error(sprintf('Failed to handle ExApp %s FileAction %s. Error: %s', $appId, $actionName, $result['error'])); - } - } - } - return new DataResponse([ - 'success' => $result, - 'handleFileActionSent' => $result, - ], Http::STATUS_OK); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $appId - * @param string $exFileActionName - * - * @return DataDisplayResponse - */ - #[NoAdminRequired] - #[NoCSRFRequired] - public function loadFileActionIcon(string $appId, string $exFileActionName): DataDisplayResponse { - $icon = $this->exFilesActionsMenuService->loadFileActionIcon($appId, $exFileActionName); - if ($icon !== null && isset($icon['body'], $icon['headers'])) { - $response = new DataDisplayResponse( - $icon['body'], - Http::STATUS_OK, - ['Content-Type' => $icon['headers']['Content-Type'][0] ?? 'image/svg+xml'] - ); - $response->cacheFor(ExFilesActionsMenuService::ICON_CACHE_TTL, false, true); - return $response; - } - return new DataDisplayResponse('', 400); - } } diff --git a/lib/Controller/TopMenuController.php b/lib/Controller/TopMenuController.php index 937e8e02..ed04e508 100644 --- a/lib/Controller/TopMenuController.php +++ b/lib/Controller/TopMenuController.php @@ -6,11 +6,11 @@ use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Service\AppAPIService; -use OCA\AppAPI\Service\ExAppInitialStateService; -use OCA\AppAPI\Service\ExAppScriptsService; -use OCA\AppAPI\Service\ExAppStylesService; use OCA\AppAPI\Service\ExAppUsersService; -use OCA\AppAPI\Service\TopMenuService; +use OCA\AppAPI\Service\UI\InitialStateService; +use OCA\AppAPI\Service\UI\ScriptsService; +use OCA\AppAPI\Service\UI\StylesService; +use OCA\AppAPI\Service\UI\TopMenuService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; @@ -26,15 +26,15 @@ class TopMenuController extends Controller { public array $jsProxyMap = []; public function __construct( - IRequest $request, - private IInitialState $initialState, - private TopMenuService $menuEntryService, - private ExAppInitialStateService $initialStateService, - private ExAppScriptsService $scriptsService, - private ExAppStylesService $stylesService, - private ExAppUsersService $exAppUsersService, - private AppAPIService $service, - private ?string $userId, + IRequest $request, + private IInitialState $initialState, + private TopMenuService $menuEntryService, + private InitialStateService $initialStateService, + private ScriptsService $scriptsService, + private StylesService $stylesService, + private ExAppUsersService $exAppUsersService, + private AppAPIService $service, + private ?string $userId, ) { parent::__construct(Application::APP_ID, $request); } diff --git a/lib/Db/UI/FilesActionsMenu.php b/lib/Db/UI/FilesActionsMenu.php index 825301d3..17d058aa 100644 --- a/lib/Db/UI/FilesActionsMenu.php +++ b/lib/Db/UI/FilesActionsMenu.php @@ -17,18 +17,16 @@ * @method string getDisplayName() * @method string getMime() * @method string getPermissions() - * @method string getOrder() + * @method int getOrder() * @method string getIcon() - * @method string getIconClass() * @method string getActionHandler() * @method void setAppid(string $appid) * @method void setName(string $name) * @method void setDisplayName(string $displayName) * @method void setMime(string $mime) * @method void setPermissions(string $permissions) - * @method void setOrder(string $order) + * @method void setOrder(int $order) * @method void setIcon(string $icon) - * @method void setIconClass(string $iconClass) * @method void setActionHandler(string $actionHandler) */ class FilesActionsMenu extends Entity implements JsonSerializable { @@ -39,7 +37,6 @@ class FilesActionsMenu extends Entity implements JsonSerializable { protected $permissions; protected $order; protected $icon; - protected $iconClass; protected $actionHandler; /** @@ -51,9 +48,8 @@ public function __construct(array $params = []) { $this->addType('displayName', 'string'); $this->addType('mime', 'string'); $this->addType('permissions', 'string'); - $this->addType('order', 'string'); + $this->addType('order', 'int'); $this->addType('icon', 'string'); - $this->addType('iconClass', 'string'); $this->addType('actionHandler', 'string'); if (isset($params['id'])) { @@ -80,9 +76,6 @@ public function __construct(array $params = []) { if (isset($params['icon'])) { $this->setIcon($params['icon']); } - if (isset($params['icon_class'])) { - $this->setIconClass($params['icon_class']); - } if (isset($params['action_handler'])) { $this->setActionHandler($params['action_handler']); } @@ -98,7 +91,6 @@ public function jsonSerialize(): array { 'permissions' => $this->getPermissions(), 'order' => $this->getOrder(), 'icon' => $this->getIcon(), - 'icon_class' => $this->getIconClass(), 'action_handler' => $this->getActionHandler(), ]; } diff --git a/lib/Db/UI/FilesActionsMenuMapper.php b/lib/Db/UI/FilesActionsMenuMapper.php index 35c26be5..e5800f67 100644 --- a/lib/Db/UI/FilesActionsMenuMapper.php +++ b/lib/Db/UI/FilesActionsMenuMapper.php @@ -32,7 +32,6 @@ public function findAllEnabled(): array { 'ex_files_actions_menu.permissions', 'ex_files_actions_menu.order', 'ex_files_actions_menu.icon', - 'ex_files_actions_menu.icon_class', 'ex_files_actions_menu.action_handler', ) ->from($this->tableName, 'ex_files_actions_menu') diff --git a/lib/Listener/LoadFilesPluginListener.php b/lib/Listener/LoadFilesPluginListener.php index 75d8c694..2969c939 100644 --- a/lib/Listener/LoadFilesPluginListener.php +++ b/lib/Listener/LoadFilesPluginListener.php @@ -5,12 +5,12 @@ namespace OCA\AppAPI\Listener; use OCA\AppAPI\AppInfo\Application; -use OCA\AppAPI\Service\ExFilesActionsMenuService; - +use OCA\AppAPI\Service\UI\FilesActionsMenuService; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCP\AppFramework\Services\IInitialState; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\IConfig; use OCP\Util; /** @@ -20,7 +20,8 @@ class LoadFilesPluginListener implements IEventListener { public function __construct( private IInitialState $initialState, - private ExFilesActionsMenuService $service + private FilesActionsMenuService $service, + private IConfig $config, ) { } @@ -31,7 +32,10 @@ public function handle(Event $event): void { $exFilesActions = $this->service->getRegisteredFileActions(); if (!empty($exFilesActions)) { - $this->initialState->provideInitialState('ex_files_actions_menu', ['fileActions' => $exFilesActions]); + $this->initialState->provideInitialState('ex_files_actions_menu', [ + 'fileActions' => $exFilesActions, + 'instanceId' => $this->config->getSystemValue('instanceid'), + ]); Util::addScript(Application::APP_ID, Application::APP_ID . '-filesplugin'); Util::addStyle(Application::APP_ID, 'filesactions'); } diff --git a/lib/Migration/Version1003Date202311061844.php b/lib/Migration/Version1003Date202311061844.php index dd92e170..7ea28453 100644 --- a/lib/Migration/Version1003Date202311061844.php +++ b/lib/Migration/Version1003Date202311061844.php @@ -146,6 +146,11 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addUniqueIndex(['appid', 'type', 'name', 'path'], 'ui_style__idx'); } + if ($schema->hasTable('ex_files_actions_menu')) { + $table = $schema->getTable('ex_files_actions_menu'); + $table->dropColumn('icon_class'); + } + return $schema; } } diff --git a/lib/Service/AppAPIService.php b/lib/Service/AppAPIService.php index 91030d09..b12819e0 100644 --- a/lib/Service/AppAPIService.php +++ b/lib/Service/AppAPIService.php @@ -7,10 +7,14 @@ use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\ExApp; use OCA\AppAPI\Db\ExAppMapper; - use OCA\AppAPI\Fetcher\ExAppArchiveFetcher; use OCA\AppAPI\Fetcher\ExAppFetcher; use OCA\AppAPI\Notifications\ExNotificationsManager; +use OCA\AppAPI\Service\UI\FilesActionsMenuService; +use OCA\AppAPI\Service\UI\InitialStateService; +use OCA\AppAPI\Service\UI\ScriptsService; +use OCA\AppAPI\Service\UI\StylesService; +use OCA\AppAPI\Service\UI\TopMenuService; use OCP\App\IAppManager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -43,29 +47,29 @@ class AppAPIService { public function __construct( private readonly LoggerInterface $logger, private readonly ILogFactory $logFactory, - ICacheFactory $cacheFactory, - private readonly IThrottler $throttler, - private readonly IConfig $config, - IClientService $clientService, - private readonly ExAppMapper $exAppMapper, - private readonly IAppManager $appManager, - private readonly ExAppUsersService $exAppUsersService, - private readonly ExAppApiScopeService $exAppApiScopeService, - private readonly ExAppScopesService $exAppScopesService, - private readonly TopMenuService $topMenuService, - private readonly ExAppInitialStateService $initialStateService, - private readonly ExAppScriptsService $scriptsService, - private readonly ExAppStylesService $stylesService, - private readonly ExFilesActionsMenuService $filesActionsMenuService, - private readonly ISecureRandom $random, - private readonly IUserSession $userSession, - private readonly ISession $session, - private readonly IUserManager $userManager, - private readonly ExAppConfigService $exAppConfigService, - private readonly ExNotificationsManager $exNotificationsManager, - private readonly TalkBotsService $talkBotsService, - private readonly ExAppFetcher $exAppFetcher, - private readonly ExAppArchiveFetcher $exAppArchiveFetcher, + ICacheFactory $cacheFactory, + private readonly IThrottler $throttler, + private readonly IConfig $config, + IClientService $clientService, + private readonly ExAppMapper $exAppMapper, + private readonly IAppManager $appManager, + private readonly ExAppUsersService $exAppUsersService, + private readonly ExAppApiScopeService $exAppApiScopeService, + private readonly ExAppScopesService $exAppScopesService, + private readonly TopMenuService $topMenuService, + private readonly InitialStateService $initialStateService, + private readonly ScriptsService $scriptsService, + private readonly StylesService $stylesService, + private readonly FilesActionsMenuService $filesActionsMenuService, + private readonly ISecureRandom $random, + private readonly IUserSession $userSession, + private readonly ISession $session, + private readonly IUserManager $userManager, + private readonly ExAppConfigService $exAppConfigService, + private readonly ExNotificationsManager $exNotificationsManager, + private readonly TalkBotsService $talkBotsService, + private readonly ExAppFetcher $exAppFetcher, + private readonly ExAppArchiveFetcher $exAppArchiveFetcher, ) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/service'); $this->client = $clientService->newClient(); diff --git a/lib/Service/ExAppApiScopeService.php b/lib/Service/ExAppApiScopeService.php index f5b0d2c6..bfc71b9b 100644 --- a/lib/Service/ExAppApiScopeService.php +++ b/lib/Service/ExAppApiScopeService.php @@ -86,7 +86,7 @@ public function registerInitScopes(): bool { $initApiScopes = [ // AppAPI scopes - ['api_route' => $aeApiV1Prefix . '/files/actions/menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], + ['api_route' => $aeApiV1Prefix . '/ui/files-actions-menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], ['api_route' => $aeApiV1Prefix . '/ui/top-menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], ['api_route' => $aeApiV1Prefix . '/ui/initial-state', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], ['api_route' => $aeApiV1Prefix . '/ui/script', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], diff --git a/lib/Service/ExFilesActionsMenuService.php b/lib/Service/UI/FilesActionsMenuService.php similarity index 61% rename from lib/Service/ExFilesActionsMenuService.php rename to lib/Service/UI/FilesActionsMenuService.php index 1616406d..e5965d36 100644 --- a/lib/Service/ExFilesActionsMenuService.php +++ b/lib/Service/UI/FilesActionsMenuService.php @@ -2,87 +2,88 @@ declare(strict_types=1); -namespace OCA\AppAPI\Service; +namespace OCA\AppAPI\Service\UI; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\UI\FilesActionsMenu; use OCA\AppAPI\Db\UI\FilesActionsMenuMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\AppFramework\Http; use OCP\DB\Exception; -use OCP\Http\Client\IClient; -use OCP\Http\Client\IClientService; use OCP\ICache; use OCP\ICacheFactory; use Psr\Log\LoggerInterface; -class ExFilesActionsMenuService { +class FilesActionsMenuService { public const ICON_CACHE_TTL = 60 * 60 * 24; // 1 day private ICache $cache; - private IClient $client; public function __construct( ICacheFactory $cacheFactory, private readonly FilesActionsMenuMapper $mapper, private readonly LoggerInterface $logger, - IClientService $clientService, ) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/ex_files_actions_menu'); - $this->client = $clientService->newClient(); } /** * Register file action menu from ExApp * * @param string $appId - * @param array $params - * + * @param string $name + * @param string $displayName + * @param string $actionHandler + * @param string $icon + * @param string $mime + * @param int $permissions + * @param int $order * @return FilesActionsMenu|null */ - public function registerFileActionMenu(string $appId, array $params): ?FilesActionsMenu { + public function registerFileActionMenu(string $appId, string $name, string $displayName, string $actionHandler, + string $icon, string $mime, int $permissions, int $order): ?FilesActionsMenu { try { - $fileActionMenu = $this->mapper->findByName($params['name']); + $fileActionMenu = $this->mapper->findByAppidName($appId, $name); } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception) { $fileActionMenu = null; } try { $newFileActionMenu = new FilesActionsMenu([ 'appid' => $appId, - 'name' => $params['name'], - 'display_name' => $params['display_name'], - 'mime' => $params['mime'], - 'permissions' => $params['permissions'] ?? 31, - 'order' => $params['order'] ?? 0, - 'icon' => $params['icon'] ?? null, - 'icon_class' => $params['icon_class'] ?? 'icon-app-api', - 'action_handler' => $params['action_handler'], + 'name' => $name, + 'display_name' => $displayName, + 'action_handler' => ltrim($actionHandler, '/'), + 'icon' => ltrim($icon, '/'), + 'mime' => $mime, + 'permissions' => $permissions, + 'order' => $order, ]); if ($fileActionMenu !== null) { $newFileActionMenu->setId($fileActionMenu->getId()); } $fileActionMenu = $this->mapper->insertOrUpdate($newFileActionMenu); - $this->cache->set('/ex_files_actions_menu_' . $appId . '_' . $params['name'], $fileActionMenu); + $this->cache->set('/ex_files_actions_menu_' . $appId . '_' . $name, $fileActionMenu); $this->resetCacheEnabled(); } catch (Exception $e) { - $this->logger->error(sprintf('Failed to register ExApp %s FileActionMenu %s. Error: %s', $appId, $params['name'], $e->getMessage()), ['exception' => $e]); + $this->logger->error( + sprintf('Failed to register ExApp %s FileActionMenu %s. Error: %s', $appId, $name, $e->getMessage()), ['exception' => $e] + ); return null; } return $fileActionMenu; } - public function unregisterFileActionMenu(string $appId, string $fileActionMenuName): ?FilesActionsMenu { + public function unregisterFileActionMenu(string $appId, string $name): ?FilesActionsMenu { try { - $fileActionMenu = $this->getExAppFileAction($appId, $fileActionMenuName); + $fileActionMenu = $this->getExAppFileAction($appId, $name); if ($fileActionMenu === null) { return null; } $this->mapper->delete($fileActionMenu); - $this->cache->remove('/ex_files_actions_menu_' . $appId . '_' . $fileActionMenuName); + $this->cache->remove('/ex_files_actions_menu_' . $appId . '_' . $name); $this->resetCacheEnabled(); return $fileActionMenu; } catch (Exception $e) { - $this->logger->error(sprintf('Failed to unregister ExApp %s FileActionMenu %s. Error: %s', $appId, $fileActionMenuName, $e->getMessage()), ['exception' => $e]); + $this->logger->error(sprintf('Failed to unregister ExApp %s FileActionMenu %s. Error: %s', $appId, $name, $e->getMessage()), ['exception' => $e]); return null; } } @@ -126,36 +127,6 @@ public function getExAppFileAction(string $appId, string $fileActionName): ?File return $fileAction; } - /** - * @param string $appId - * @param string $exFileActionName - * - * @return array|null - */ - public function loadFileActionIcon(string $appId, string $exFileActionName): ?array { - $exFileAction = $this->getExAppFileAction($appId, $exFileActionName); - if ($exFileAction === null) { - return null; - } - $iconUrl = $exFileAction->getIcon(); - if (!isset($iconUrl) || $iconUrl === '') { - return null; - } - try { - $thumbnailResponse = $this->client->get($iconUrl); - if ($thumbnailResponse->getStatusCode() === Http::STATUS_OK) { - return [ - 'body' => $thumbnailResponse->getBody(), - 'headers' => $thumbnailResponse->getHeaders(), - ]; - } - } catch (\Exception $e) { - $this->logger->error(sprintf('Failed to load ExApp %s FileAction icon %s. Error: %s', $appId, $exFileActionName, $e->getMessage()), ['exception' => $e]); - return null; - } - return null; - } - public function unregisterExAppFileActions(string $appId): int { try { $result = $this->mapper->removeAllByAppId($appId); diff --git a/lib/Service/ExAppInitialStateService.php b/lib/Service/UI/InitialStateService.php similarity index 97% rename from lib/Service/ExAppInitialStateService.php rename to lib/Service/UI/InitialStateService.php index 4ac36866..7265aab0 100644 --- a/lib/Service/ExAppInitialStateService.php +++ b/lib/Service/UI/InitialStateService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace OCA\AppAPI\Service; +namespace OCA\AppAPI\Service\UI; use OCA\AppAPI\Db\UI\InitialState; use OCA\AppAPI\Db\UI\InitialStateMapper; @@ -11,7 +11,7 @@ use OCP\DB\Exception; use Psr\Log\LoggerInterface; -class ExAppInitialStateService { +class InitialStateService { public function __construct( private readonly InitialStateMapper $mapper, diff --git a/lib/Service/ExAppScriptsService.php b/lib/Service/UI/ScriptsService.php similarity index 91% rename from lib/Service/ExAppScriptsService.php rename to lib/Service/UI/ScriptsService.php index 28f41bee..de1008fe 100644 --- a/lib/Service/ExAppScriptsService.php +++ b/lib/Service/UI/ScriptsService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace OCA\AppAPI\Service; +namespace OCA\AppAPI\Service\UI; use LengthException; use OCA\AppAPI\AppInfo\Application; @@ -14,7 +14,7 @@ use OCP\Util; use Psr\Log\LoggerInterface; -class ExAppScriptsService { +class ScriptsService { public const MAX_JS_FILES = 10; //should be equal to number of files in "proxy_js" folder. @@ -31,7 +31,7 @@ public function setExAppScript(string $appId, string $type, string $name, string 'appid' => $appId, 'type' => $type, 'name' => $name, - 'path' => $path, + 'path' => ltrim($path, '/'), 'after_app_id' => $afterAppId, ]); if ($script !== null) { @@ -48,12 +48,12 @@ public function setExAppScript(string $appId, string $type, string $name, string } public function deleteExAppScript(string $appId, string $type, string $name, string $path): bool { - return $this->mapper->removeByNameTypePath($appId, $type, $name, $path); + return $this->mapper->removeByNameTypePath($appId, $type, $name, ltrim($path, '/')); } public function getExAppScript(string $appId, string $type, string $name, string $path): ?Script { try { - return $this->mapper->findByAppIdTypeNamePath($appId, $type, $name, $path); + return $this->mapper->findByAppIdTypeNamePath($appId, $type, $name, ltrim($path, '/')); } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception) { } return null; @@ -95,11 +95,7 @@ public function applyExAppScripts(string $appId, string $type, string $name): ar } else { Util::addScript(Application::APP_ID, $fakeJsPath, $value['after_app_id']); } - if (str_starts_with($value['path'], '/')) { - $mapResult[$i] = $appId . $value['path']; - } else { - $mapResult[$i] = $appId . '/' . $value['path']; - } + $mapResult[$i] = $appId . '/' . $value['path']; $i++; } return $mapResult; diff --git a/lib/Service/ExAppStylesService.php b/lib/Service/UI/StylesService.php similarity index 85% rename from lib/Service/ExAppStylesService.php rename to lib/Service/UI/StylesService.php index a3b33d29..1cec0ddd 100644 --- a/lib/Service/ExAppStylesService.php +++ b/lib/Service/UI/StylesService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace OCA\AppAPI\Service; +namespace OCA\AppAPI\Service\UI; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\UI\Style; @@ -13,7 +13,7 @@ use OCP\Util; use Psr\Log\LoggerInterface; -class ExAppStylesService { +class StylesService { public function __construct( private readonly StyleMapper $mapper, @@ -28,7 +28,7 @@ public function setExAppStyle(string $appId, string $type, string $name, string 'appid' => $appId, 'type' => $type, 'name' => $name, - 'path' => $path, + 'path' => ltrim($path, '/'), ]); if ($style !== null) { $newStyle->setId($style->getId()); @@ -44,12 +44,12 @@ public function setExAppStyle(string $appId, string $type, string $name, string } public function deleteExAppStyle(string $appId, string $type, string $name, string $path): bool { - return $this->mapper->removeByNameTypePath($appId, $type, $name, $path); + return $this->mapper->removeByNameTypePath($appId, $type, $name, ltrim($path, '/')); } public function getExAppStyle(string $appId, string $type, string $name, string $path): ?Style { try { - return $this->mapper->findByAppIdTypeNamePath($appId, $type, $name, $path); + return $this->mapper->findByAppIdTypeNamePath($appId, $type, $name, ltrim($path, '/')); } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { } return null; @@ -79,12 +79,7 @@ public function deleteExAppStyles(string $appId): int { public function applyExAppStyles(string $appId, string $type, string $name): void { $styles = $this->mapper->findByAppIdTypeName($appId, $type, $name); foreach ($styles as $value) { - if (str_starts_with($value['path'], '/')) { - // in the future we should allow offload of styles to the NC instance if they start with '/' - $path = 'proxy/'. $appId . $value['path']; - } else { - $path = 'proxy/'. $appId . '/' . $value['path']; - } + $path = 'proxy/'. $appId . '/' . $value['path']; Util::addStyle(Application::APP_ID, $path); } } diff --git a/lib/Service/TopMenuService.php b/lib/Service/UI/TopMenuService.php similarity index 92% rename from lib/Service/TopMenuService.php rename to lib/Service/UI/TopMenuService.php index 3075dc8b..19024665 100644 --- a/lib/Service/TopMenuService.php +++ b/lib/Service/UI/TopMenuService.php @@ -3,7 +3,7 @@ declare(strict_types=1); -namespace OCA\AppAPI\Service; +namespace OCA\AppAPI\Service\UI; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\UI\TopMenu; @@ -27,12 +27,12 @@ class TopMenuService { private ICache $cache; public function __construct( - private readonly TopMenuMapper $mapper, - private readonly LoggerInterface $logger, - private readonly ExAppInitialStateService $initialStateService, - private readonly ExAppScriptsService $scriptsService, - private readonly ExAppStylesService $stylesService, - ICacheFactory $cacheFactory, + private readonly TopMenuMapper $mapper, + private readonly LoggerInterface $logger, + private readonly InitialStateService $initialStateService, + private readonly ScriptsService $scriptsService, + private readonly StylesService $stylesService, + ICacheFactory $cacheFactory, ) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/ex_top_menus'); } @@ -84,7 +84,7 @@ public function registerExAppMenuEntry(string $appId, string $name, string $disp 'appid' => $appId, 'name' => $name, 'display_name' => $displayName, - 'icon' => $icon, + 'icon' => ltrim($icon, '/'), 'admin_required' => $adminRequired, ]); if ($menuEntry !== null) { diff --git a/src/filesplugin.js b/src/filesplugin.js index 6ddd8e7f..1aad5e37 100644 --- a/src/filesplugin.js +++ b/src/filesplugin.js @@ -1,8 +1,9 @@ import axios from '@nextcloud/axios' -import { generateOcsUrl } from '@nextcloud/router' +import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' import { registerFileAction, FileAction } from '@nextcloud/files' +import { getCurrentUser } from '@nextcloud/auth' const state = loadState('app_api', 'ex_files_actions_menu') @@ -10,6 +11,24 @@ function loadStaticAppAPIInlineSvgIcon() { return '' } +function loadExAppInlineSvgIcon(appId, route) { + const url = generateAppAPIProxyUrl(appId, route) + return axios.get(url).then((response) => { + // Check content type to be svg image + if (response.headers['content-type'] !== 'image/svg+xml') { + return null + } + return response.data + }).catch((error) => { + console.error('error', error) + return null + }) +} + +function generateAppAPIProxyUrl(appId, route) { + return generateUrl(`/apps/app_api/proxy/${appId}/${route}`) +} + if (OCA.Files && OCA.Files.fileActions) { state.fileActions.forEach(fileAction => { const action = { @@ -18,35 +37,34 @@ if (OCA.Files && OCA.Files.fileActions) { mime: fileAction.mime, permissions: Number(fileAction.permissions), order: Number(fileAction.order), - icon: fileAction.icon !== '' ? generateOcsUrl('/apps/app_api/api/v1/files/action/icon?appId=' + fileAction.appid + '&exFileActionName=' + fileAction.name) : null, - iconClass: fileAction.icon_class, + icon: fileAction.icon !== '' ? generateAppAPIProxyUrl(fileAction.appid, fileAction.icon) : null, + iconClass: fileAction.icon === '' ? 'icon-app-api' : '', actionHandler: (fileName, context) => { const file = context.$file[0] - axios.post(generateOcsUrl('/apps/app_api/api/v1/files/action'), { - appId: fileAction.appid, - actionName: fileAction.name, - actionHandler: fileAction.action_handler, - actionFile: { - fileId: Number(file.dataset.id), - name: fileName, - directory: file.dataset.path, - etag: file.dataset.etag, - mime: file.dataset.mime, - favorite: file.dataset?.favorite, - permissions: Number(file.dataset.permissions), - fileType: file.dataset.type, - size: file.dataset.size, - mtime: file.dataset.mtime, - shareTypes: file.dataset?.shareTypes, - shareAttributes: file.dataset?.shareAttributes, - sharePermissions: file.dataset?.sharePermissions, - shareOwner: file.dataset?.shareOwner, - shareOwnerId: file.dataset?.shareOwnerId, - }, + const exAppFileActionHandler = generateAppAPIProxyUrl(fileAction.appid, fileAction.action_handler) + axios.post(exAppFileActionHandler, { + fileId: Number(file.dataset.id), + name: fileName, + directory: file.dataset.path, + etag: file.dataset.etag, + mime: file.dataset.mime, + favorite: file.dataset.favorite || 'false', + permissions: Number(file.dataset.permissions), + fileType: file.dataset.type, + size: Number(file.dataset.size), + mtime: Number(file.dataset.mtime) / 1000, // convert ms to s + shareTypes: file.dataset?.shareTypes || null, + shareAttributes: file.dataset?.shareAttributes || null, + sharePermissions: file.dataset?.sharePermissions || null, + shareOwner: file.dataset?.shareOwner || null, + shareOwnerId: file.dataset?.shareOwnerId || null, + userId: getCurrentUser().uid, + instanceId: state.instanceId, }).then((response) => { - if (response.data.ocs.meta.statuscode === 200) { + if (response.status === 200) { OC.dialogs.info(t('app_api', 'Action request sent to ExApp'), t(fileAction.appid, fileAction.display_name)) } else { + console.debug(response) OC.dialogs.info(t('app_api', 'Error while sending File action request to ExApp'), t(fileAction.appid, fileAction.display_name)) } }).catch((error) => { @@ -59,53 +77,65 @@ if (OCA.Files && OCA.Files.fileActions) { }) } else { state.fileActions.forEach(fileAction => { - const action = new FileAction({ - id: fileAction.name, - displayName: () => fileAction.display_name, - iconSvgInline: () => fileAction.icon_class === 'icon-app-api' ? loadStaticAppAPIInlineSvgIcon() : null, // TODO: Rewrite fileActions to use proxy to get SvgInline from ExApps - order: Number(fileAction.order), - enabled(nodes) { - if (nodes.length !== 1) { - return false + let inlineSvg = loadStaticAppAPIInlineSvgIcon() + if (fileAction.icon !== '') { + loadExAppInlineSvgIcon(fileAction.appid, fileAction.icon).then((svg) => { + if (svg !== null) { + // Set css filter for theming + const parser = new DOMParser() + const icon = parser.parseFromString(svg, 'image/svg+xml') + icon.documentElement.setAttribute('style', 'filter: var(--background-invert-if-dark);') + // Convert back to inline string + inlineSvg = icon.documentElement.outerHTML } + }).finally(() => { + const action = new FileAction({ + id: fileAction.name, + displayName: () => fileAction.display_name, + iconSvgInline: () => inlineSvg, + order: Number(fileAction.order), + enabled(files, view) { + if (files.length !== 1) { + return false + } - return (nodes[0].mime.indexOf(fileAction.mime) !== -1) - }, - async exec(node) { - axios.post(generateOcsUrl('/apps/app_api/api/v1/files/action'), { - appId: fileAction.appid, - actionName: fileAction.name, - actionHandler: fileAction.action_handler, - actionFile: { - fileId: node.fileid, - name: node.basename, - directory: node.dirname, - etag: node.attributes.etag, - mime: node.mime, - favorite: Boolean(node.attributes.favorite).toString(), - permissions: node.permissions, - fileType: node.type, - size: node.size, - mtime: new Date(node.mtime).getTime(), - shareTypes: node.attributes.shareTypes, - shareAttributes: node.attributes.shareAttributes, - sharePermissions: node.attributes.sharePermissions, - shareOwner: node.attributes.ownerDisplayName, - shareOwnerId: node.attributes.ownerId, + return (files[0].mime.indexOf(fileAction.mime) !== -1) + }, + async exec(node) { + const exAppFileActionHandler = generateAppAPIProxyUrl(fileAction.appid, fileAction.action_handler) + axios.post(exAppFileActionHandler, { + fileId: node.fileid, + name: node.basename, + directory: node.dirname, + etag: node.attributes.etag, + mime: node.mime, + favorite: Boolean(node.attributes.favorite).toString(), + permissions: node.permissions, + fileType: node.type, + size: Number(node.size), + mtime: new Date(node.mtime).getTime() / 1000, // convert ms to s + shareTypes: node.attributes.shareTypes || null, + shareAttributes: node.attributes.shareAttributes || null, + sharePermissions: node.attributes.sharePermissions || null, + shareOwner: node.attributes.ownerDisplayName || null, + shareOwnerId: node.attributes.ownerId || null, + userId: getCurrentUser().uid, + instanceId: state.instanceId, + }).then((response) => { + if (response.status === 200) { + OC.dialogs.info(t('app_api', 'Action request sent to ExApp'), t(fileAction.appid, fileAction.display_name)) + } else { + OC.dialogs.info(t('app_api', 'Error while sending File action request to ExApp'), t(fileAction.appid, fileAction.display_name)) + } + }).catch((error) => { + console.error('error', error) + OC.dialogs.info(t('app_api', 'Error while sending File action request to ExApp'), t(fileAction.appid, fileAction.display_name)) + }) + return null }, - }).then((response) => { - if (response.data.ocs.meta.statuscode === 200) { - OC.dialogs.info(t('app_api', 'Action request sent to ExApp'), t(fileAction.appid, fileAction.display_name)) - } else { - OC.dialogs.info(t('app_api', 'Error while sending File action request to ExApp'), t(fileAction.appid, fileAction.display_name)) - } - }).catch((error) => { - console.error('error', error) - OC.dialogs.info(t('app_api', 'Error while sending File action request to ExApp'), t(fileAction.appid, fileAction.display_name)) }) - return null - }, - }) - registerFileAction(action) + registerFileAction(action) + }) + } }) }