diff --git a/config/opendxp/routing.yaml b/config/opendxp/routing.yaml index 37684d99..cb2ae9ab 100644 --- a/config/opendxp/routing.yaml +++ b/config/opendxp/routing.yaml @@ -19,4 +19,4 @@ opendxp_admin_page_display_preview_image: # we need to have this outside of /admin scope, to be reachable publicly opendxp_admin_document_document_diff_versions_html: path: /__admin/document/diff-versions-html - defaults: { _controller: OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentController::diffVersionsHtmlAction } + defaults: { _controller: OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentVersionController::diffVersionsHtmlAction } diff --git a/config/services.yaml b/config/services.yaml index d852378a..88cc6be5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -18,11 +18,12 @@ services: public: true tags: ['controller.service_arguments'] - OpenDxp\Bundle\AdminBundle\Controller\Admin\IndexController: - public: true + OpenDxp\Maintenance\ExecutorInterface: + alias: OpenDxp\Maintenance\Executor + + OpenDxp\Bundle\AdminBundle\Handler\Admin\Statistics\StatisticsHandler: arguments: $httpClient: '@opendxp.http_client' - tags: [ 'controller.service_arguments' ] # # COMMANDS @@ -91,6 +92,7 @@ services: # OpenDxp\Bundle\AdminBundle\Service\Workflow\ActionsButtonService: ~ + OpenDxp\Bundle\AdminBundle\Service\Workflow\WorkflowElementResolver: ~ # # Admin Config Services @@ -102,3 +104,79 @@ services: # OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface: class: OpenDxp\Bundle\AdminBundle\Service\ElementService + + OpenDxp\Bundle\AdminBundle\Service\CustomLoginUrlGenerator: ~ + + OpenDxp\Bundle\AdminBundle\Service\AdminUserContextInterface: + class: OpenDxp\Bundle\AdminBundle\Service\AdminUserContext + + OpenDxp\Bundle\AdminBundle\Service\Element\SessionService: ~ + OpenDxp\Bundle\AdminBundle\Service\Element\EditLockService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Document\DocumentPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Service\Document\DocumentPersistenceCoordinator: ~ + + OpenDxp\Bundle\AdminBundle\Service\DataObject\DataObjectPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Service\DataObject\DataObjectPersistenceCoordinator: ~ + OpenDxp\Bundle\AdminBundle\Service\DataObject\DataObjectGridService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetPersistenceCoordinator: ~ + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetUploadService: ~ + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetGridService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Grid\GridColumnConfigService: ~ + OpenDxp\Bundle\AdminBundle\Service\Grid\AssetGridColumnConfigResolver: ~ + OpenDxp\Bundle\AdminBundle\Service\Grid\DataObjectGridColumnConfigResolver: ~ + OpenDxp\Bundle\AdminBundle\Service\Grid\GridExportService: ~ + OpenDxp\Bundle\AdminBundle\Service\Grid\GridBatchService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Cache\OpenDxpCacheClearingService: ~ + OpenDxp\Bundle\AdminBundle\Service\Cache\SymfonyCacheClearingService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Translation\AdminSearchTermResolver: ~ + + # + # Factories + # + OpenDxp\Bundle\AdminBundle\Factory\: + resource: '../src/Factory' + + # + # Builder + # + OpenDxp\Bundle\AdminBundle\Builder\: + resource: '../src/Builder' + + # + # HTTP / Value Resolvers + # + OpenDxp\Bundle\AdminBundle\Http\: + resource: '../src/Http' + + # + # Enrichers + # + OpenDxp\Bundle\AdminBundle\Enricher\: + resource: '../src/Enricher' + + # + # Handlers + # + OpenDxp\Bundle\AdminBundle\Handler\: + resource: '../src/Handler' + bind: + $documentNoteTypes: '%opendxp_admin.dataObjects.notes_events.types%' + $assetNoteTypes: '%opendxp_admin.assets.notes_events.types%' + $objectNoteTypes: '%opendxp_admin.documents.notes_events.types%' + + OpenDxp\Bundle\AdminBundle\Handler\Document\AddDocument\AddDocumentHandler: + arguments: + $documentClassResolver: '@opendxp.class.resolver.document' + $defaultDocumentController: '%opendxp.documents.default_controller%' + + # + # Security Voters + # + OpenDxp\Bundle\AdminBundle\Security\Voter\: + resource: '../src/Security/Voter' diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e85a0d1c..44a96e3f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18,8 +18,3 @@ parameters: count: 1 path: src/DependencyInjection/OpenDxpAdminExtension.php - - - message: '#^Trait OpenDxp\\Bundle\\AdminBundle\\EventListener\\Traits\\ControllerTypeTrait is used zero times and is not analysed\.$#' - identifier: trait.unused - count: 1 - path: src/EventListener/Traits/ControllerTypeTrait.php diff --git a/public/js/opendxp/object/helpers/customLayoutEditor.js b/public/js/opendxp/object/helpers/customLayoutEditor.js index a483815c..2087bd38 100644 --- a/public/js/opendxp/object/helpers/customLayoutEditor.js +++ b/public/js/opendxp/object/helpers/customLayoutEditor.js @@ -828,7 +828,7 @@ opendxp.object.helpers.customLayoutEditor = Class.create({ this.layoutComboStore.reload(); this.data = res.data; } else { - Ext.Msg.alert(t('error'), t(res.msg)); + Ext.Msg.alert(t('error'), t(res.message)); } } catch (e) { this.saveOnError(); diff --git a/src/Builder/AdminSettingsAssembler.php b/src/Builder/AdminSettingsAssembler.php new file mode 100644 index 00000000..2805bc9e --- /dev/null +++ b/src/Builder/AdminSettingsAssembler.php @@ -0,0 +1,244 @@ +config; + $systemSettings = SystemSettingsConfig::get(); + $adminSettings = AdminConfig::get(); + + $runtimePerspective = PerspectiveConfig::getRuntimePerspective($user); + $dashboard = $this->dashboardFactory->create(); + + try { + $adminEntrypointUrl = $this->urlGenerator->generate( + $this->customAdminRouteName, + [], + UrlGeneratorInterface::ABSOLUTE_URL + ); + } catch (\Exception) { + $adminEntrypointUrl = null; + } + + $requiredLanguages = $systemSettings['general']['valid_languages']; + if (array_key_exists('required_languages', $systemSettings['general'])) { + $requiredLanguages = $systemSettings['general']['required_languages']; + } + + $maxUpload = OpenDxp\Helper\FileSystemHelper::filesizeToBytes(ini_get('upload_max_filesize') . 'B'); + $maxPost = OpenDxp\Helper\FileSystemHelper::filesizeToBytes(ini_get('post_max_size') . 'B'); + $uploadBytes = min($maxUpload, $maxPost) ?: $maxUpload; + + $sessionGcMaxlifetime = (int) ini_get('session.gc_maxlifetime') ?: 120; + + $maintenanceActive = false; + if (($lastExecution = $this->maintenanceExecutor->getLastExecution()) && time() - $lastExecution < 3660) { + $maintenanceActive = true; + } + + $mailIncomplete = false; + if (isset($config['email']) && $systemSettings['email']) { + if (OpenDxp::inDebugMode() && empty($systemSettings['email']['debug']['email_addresses'])) { + $mailIncomplete = true; + } + if (empty($config['email']['sender']['email'])) { + $mailIncomplete = true; + } + } + + $notificationsEnabled = (bool) $config['notifications']['enabled']; + + return new AdminSettingsDto( + instanceId: $this->buildInstanceId(), + version: Version::getVersion(), + build: Version::getRevision(), + debug: OpenDxp::inDebugMode(), + devMode: OpenDxp::inDevMode(), + disableMinifyJs: OpenDxp::disableMinifyJs(), + environment: $this->kernel->getEnvironment(), + sessionId: htmlentities($payload->sessionId, ENT_QUOTES, 'UTF-8'), + + language: $payload->locale, + websiteLanguages: Admin::reorderWebsiteLanguages($user, $systemSettings['general']['valid_languages'], true), + requiredLanguages: $requiredLanguages, + + chromiumAvailable: HtmlToImage::isSupported(), + videoConverterAvailable: Video::isAvailable(), + + debugAdminTranslations: (bool) $systemSettings['general']['debug_admin_translations'], + generateDocumentPreviews: (bool) $config['documents']['generate_preview'], + disableAssetTreePreview: (bool) $adminSettings['assets']['disable_tree_preview'], + hideEditImage: (bool) $adminSettings['assets']['hide_edit_image'], + dependencyEnabled: $config['dependency']['enabled'], + + mainDomain: $systemSettings['general']['domain'], + customAdminEntrypointUrl: $adminEntrypointUrl, + timezone: $config['general']['timezone'] ?: date_default_timezone_get(), + tileLayerUrlTemplate: $config['maps']['tile_layer_url_template'], + geocodingUrlTemplate: $config['maps']['geocoding_url_template'], + reverseGeocodingUrlTemplate: $config['maps']['reverse_geocoding_url_template'], + hostname: htmlentities(Tool::getHostname(), ENT_QUOTES, 'UTF-8'), + assetDefaultUploadPath: $config['assets']['default_upload_path'], + + assetTreePagingLimit: $config['assets']['tree_paging_limit'], + documentTreePagingLimit: $config['documents']['tree_paging_limit'], + objectTreePagingLimit: $config['objects']['tree_paging_limit'], + + documentAutoSaveInterval: $config['documents']['auto_save_interval'], + objectAutoSaveInterval: $config['objects']['auto_save_interval'], + + perspective: $runtimePerspective, + availablePerspectives: PerspectiveConfig::getAvailablePerspectives($user), + disabledPortlets: $dashboard->getDisabledPortlets(), + + imageThumbnailsWriteable: (new Asset\Image\Thumbnail\Config())->isWriteable(), + videoThumbnailsWriteable: (new Asset\Video\Thumbnail\Config())->isWriteable(), + documentTypesWriteable: (new DocType())->isWriteable(), + predefinedPropertiesWriteable: (new Predefined())->isWriteable(), + predefinedAssetMetadataWriteable: (new \OpenDxp\Model\Metadata\Predefined())->isWriteable(), + perspectivesWriteable: PerspectiveConfig::isWriteable(), + customViewsWriteable: \OpenDxp\Bundle\AdminBundle\CustomView\Config::isWriteable(), + classDefinitionWriteable: !isset($_SERVER['OPENDXP_CLASS_DEFINITION_WRITABLE']) || (bool) $_SERVER['OPENDXP_CLASS_DEFINITION_WRITABLE'], + objectCustomLayoutWriteable: (new CustomLayout())->isWriteable(), + selectOptionsWriteable: (new \OpenDxp\Model\DataObject\SelectOptions\Config())->isWriteable(), + + assetSearchTypes: Asset::getTypes(), + documentTypesConfiguration: Document::getTypesConfiguration(), + documentSearchTypes: Document::getTypes(), + documentValidTypes: array_values(array_filter(Document::getTypes(), fn ($t) => $t !== 'folder')), + documentEmailSearchTypes: $config['documents']['email_search'], + selectOptionsProviderClass: SelectOptionsOptionsProvider::class, + + uploadMaxFilesize: (int) $uploadBytes, + sessionGcMaxlifetime: $sessionGcMaxlifetime, + + maintenanceActive: $maintenanceActive, + maintenanceMode: $this->maintenanceModeHelper->isActive(), + + mailConfigured: !$mailIncomplete, + mailDefaultAddress: $config['email']['sender']['email'] ?? null, + + customViews: $this->buildCustomViews(), + + notificationsEnabled: $notificationsEnabled, + checkNewNotificationEnabled: $notificationsEnabled && (bool) $config['notifications']['check_new_notification']['enabled'], + checkNewNotificationInterval: $config['notifications']['check_new_notification']['interval'] * 1000, + + csrfToken: $this->csrfProtection->getCsrfToken($this->requestStack->getSession()), + ); + } + + public function createStatistics(): StatisticsDto + { + try { + $dbVersion = $this->db->fetchOne('SELECT VERSION()'); + } catch (\Throwable) { + $dbVersion = null; + } + + return new StatisticsDto( + instanceId: $this->buildInstanceId(), + revision: Version::getRevision(), + version: Version::getVersion(), + majorVersion: Version::getMajorVersion(), + phpVersion: PHP_VERSION, + dbVersion: is_string($dbVersion) ? $dbVersion : null, + bundles: array_keys($this->kernel->getBundles()), + ); + } + + private function buildInstanceId(): string + { + try { + return sha1(substr($this->secret, 3, -3)); + } catch (\Exception) { + return 'not-set'; + } + } + + private function buildCustomViews(): array + { + $cvData = []; + foreach (\OpenDxp\Bundle\AdminBundle\CustomView\Config::get() as $node) { + $tmpData = $node; + $treeType = $tmpData['treetype'] ?: 'object'; + $rootNode = Service::getElementByPath($treeType, $tmpData['rootfolder']); + + if ($rootNode) { + $tmpData['rootId'] = $rootNode->getId(); + $tmpData['allowedClasses'] = $tmpData['classes'] ?? null; + $tmpData['showroot'] = (bool) $tmpData['showroot']; + + if ($rootNode->isAllowed('list')) { + $cvData[] = $tmpData; + } + } + } + + return $cvData; + } +} diff --git a/src/Controller/Admin/Asset/AssetController.php b/src/Controller/Admin/Asset/AssetController.php index 2e93cf14..e0a1d7d2 100644 --- a/src/Controller/Admin/Asset/AssetController.php +++ b/src/Controller/Admin/Asset/AssetController.php @@ -1,5 +1,7 @@ value)] +class AssetController extends ElementControllerBase { - use AdminStyleTrait; use ElementEditLockHelperTrait; - use ApplySchedulerDataTrait; - use UserNameTrait; - final const string PDF_MIMETYPE = 'application/pdf'; + public function __construct( + ElementServiceInterface $elementService, + ) { + parent::__construct($elementService); + } + + #[Route('/grid-proxy', name: 'opendxp_admin_asset_gridproxy', methods: ['GET', 'POST', 'PUT'])] + public function gridProxyAction( + Request $request, + GridProxyHandler $handler, + GridProxyPayload $payload, + CsrfProtectionHandler $csrfProtection, + ): JsonResponse { + if (isset($payload->params['data']) && $payload->params['data']) { + $csrfProtection->checkCsrfToken($request); + } - protected Asset\Service $_assetService; + return $this->adminJson($handler($payload)->data); + } #[Override] #[Route('/tree-get-root', name: 'opendxp_admin_asset_treegetroot', methods: ['GET'])] - public function treeGetRootAction(Request $request): JsonResponse - { - return parent::treeGetRootAction($request); + public function treeGetRootAction( + #[MapQueryParameter] ?string $elementType = null, + #[MapQueryParameter(flags: FILTER_NULL_ON_FAILURE)] ?int $id = null, + ): JsonResponse { + return parent::treeGetRootAction($elementType, $id); } #[Override] #[Route('/delete-info', name: 'opendxp_admin_asset_deleteinfo', methods: ['GET'])] - public function deleteInfoAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - return parent::deleteInfoAction($request, $eventDispatcher); + public function deleteInfoAction( + GetDeleteInfoHandler $handler, + GetDeleteInfoPayload $payload, + ): JsonResponse { + return parent::deleteInfoAction($handler, $payload); } #[Route('/get-data-by-id', name: 'opendxp_admin_asset_getdatabyid', methods: ['GET'])] - public function getDataByIdAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $assetId = $request->query->getInt('id'); - $type = $request->query->get('type'); - - $asset = Asset::getById($assetId); - if (!$asset instanceof Asset) { - return $this->adminJson(['success' => false, 'message' => "asset doesn't exist"]); - } - - // check for lock on non-folder items only. - if ($type !== 'folder' && ($asset->isAllowed('publish') || $asset->isAllowed('delete'))) { - if (Element\Editlock::isLocked($assetId, 'asset', $request->getSession()->getId())) { - return $this->getEditLockResponse($assetId, 'asset'); - } - - Element\Editlock::lock($assetId, 'asset', $request->getSession()->getId()); - } - - $asset = clone $asset; - $asset->setParent(null); - - $asset->setStream(null); - $data = $asset->getObjectVars(); - $data['locked'] = $asset->isLocked(); - - if ($asset instanceof Asset\Text) { - if ($asset->getFileSize() < 2000000) { - // it doesn't make sense to show a preview for files bigger than 2MB - $data['data'] = \ForceUTF8\Encoding::toUTF8($asset->getData()); - } else { - $data['data'] = false; - } - } elseif ($asset instanceof Asset\Document) { - $data['pdfPreviewAvailable'] = (bool)$this->getDocumentPreviewPdf($asset); - } elseif ($asset instanceof Asset\Video) { - $videoInfo = []; - - if (\OpenDxp\Video::isAvailable()) { - $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); - $thumbnail = $asset->getThumbnail($config, ['mp4']); - if ($thumbnail && $thumbnail['status'] === 'finished') { - $videoInfo['previewUrl'] = $thumbnail['formats']['mp4']; - $videoInfo['width'] = $asset->getWidth(); - $videoInfo['height'] = $asset->getHeight(); - $metaData = $asset->getSphericalMetaData(); - if (isset($metaData['ProjectionType']) && strtolower($metaData['ProjectionType']) === 'equirectangular') { - $videoInfo['isVrVideo'] = true; - } - } - } - - $data['videoInfo'] = $videoInfo; - } elseif ($asset instanceof Asset\Image) { - $imageInfo = []; - - $previewUrl = $this->generateUrl('opendxp_admin_asset_getimagethumbnail', [ - 'id' => $asset->getId(), - 'treepreview' => true, - '_dc' => time(), - ]); - - if ($asset->isAnimated()) { - $previewUrl = $this->generateUrl('opendxp_admin_asset_getasset', [ - 'id' => $asset->getId(), - '_dc' => time(), - ]); - } - - $imageInfo['previewUrl'] = $previewUrl; - - if ($asset->getWidth() && $asset->getHeight()) { - $imageInfo['dimensions'] = []; - $imageInfo['dimensions']['width'] = $asset->getWidth(); - $imageInfo['dimensions']['height'] = $asset->getHeight(); - } - - $imageInfo['exiftoolAvailable'] = (bool)\OpenDxp\Tool\Console::getExecutable('exiftool'); - - if (!$asset->getEmbeddedMetaData(false)) { - $asset->getEmbeddedMetaData(true, false); // read Exif, IPTC and XPM like in the old days ... - } - - $data['imageInfo'] = $imageInfo; - } - - $predefinedMetaData = Metadata\Predefined\Listing::getByTargetType('asset', [$asset->getType()]); - $predefinedMetaDataGroups = []; - /** @var Metadata\Predefined $item */ - foreach ($predefinedMetaData as $item) { - if ($item->getGroup()) { - $predefinedMetaDataGroups[$item->getGroup()] = true; - } - } - $data['predefinedMetaDataGroups'] = array_keys($predefinedMetaDataGroups); - $data['properties'] = Element\Service::minimizePropertiesForEditmode($asset->getProperties()); - $data['metadata'] = Asset\Service::expandMetadataForEditmode($asset->getMetadata()); - $data['versionDate'] = $asset->getModificationDate(); - $data['filesizeFormatted'] = $asset->getFileSize(true); - $data['filesize'] = $asset->getFileSize(); - $data['fileExtension'] = pathinfo($asset->getFilename(), PATHINFO_EXTENSION); - $data['idPath'] = Element\Service::getIdPath($asset); - $data['userPermissions'] = $asset->getUserPermissions($this->getAdminUser()); - $frontendPath = $asset->getFrontendFullPath(); - $data['url'] = preg_match('/^http(s)?:\\/\\/.+/', $frontendPath) ? - $frontendPath : - $request->getSchemeAndHttpHost() . $frontendPath; - - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $asset->getScheduledTasks() - ); - - $userOwnerName = $this->getUserName($asset->getUserOwner()); - $userModificationName = ($asset->getUserOwner() === $asset->getUserModification()) ? $userOwnerName : $this->getUserName($asset->getUserModification()); - $data['userOwnerUsername'] = $userOwnerName['userName']; - $data['userOwnerFullname'] = $userOwnerName['fullName']; - $data['userModificationUsername'] = $userModificationName['userName']; - $data['userModificationFullname'] = $userModificationName['fullName']; - - $this->addAdminStyle($asset, ElementAdminStyleEvent::CONTEXT_EDITOR, $data); - - $data['php'] = [ - 'classes' => [$asset::class, ...array_values(class_parents($asset))], - 'interfaces' => array_values(class_implements($asset)), - ]; - - $event = new GenericEvent($this, [ - 'data' => $data, - 'asset' => $asset, - ]); - $eventDispatcher->dispatch($event, AdminEvents::ASSET_GET_PRE_SEND_DATA); - $data = $event->getArgument('data'); - - if ($asset->isAllowed('view')) { - return $this->adminJson($data); - } - - throw $this->createAccessDeniedHttpException(); - } - - #[Route('/tree-get-children-by-id', name: 'opendxp_admin_asset_treegetchildrenbyid', methods: ['GET'])] - public function treeGetChildrenByIdAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $allParams = $request->query->all(); - - $assets = []; - $cv = []; - $asset = Asset::getById((int) $allParams['node']); - - $filter = $request->query->get('filter'); - $limit = (int)$allParams['limit']; - if (!is_null($filter)) { - if (!str_ends_with($filter, '*')) { - $filter .= '*'; - } - $filter = str_replace('*', '%', $filter); - - $limit = 100; - $offset = 0; - } elseif (!$allParams['limit']) { - $limit = 100000000; - } - - $offset = isset($allParams['start']) ? (int)$allParams['start'] : 0; - - $filteredTotalCount = 0; - - if ($asset->hasChildren()) { - if ($allParams['view']) { - $cv = $this->elementService->getCustomViewById($allParams['view']); - } - - // get assets - $childrenList = new Asset\Listing(); - $childrenList->addConditionParam('parentId = ?', [$asset->getId()]); - $childrenList->filterAccessibleByUser($this->getAdminUser(), $asset); - - if (!is_null($filter)) { - $childrenList->addConditionParam('CAST(assets.filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ?', [$filter]); - } - - $childrenList->setLimit($limit); - $childrenList->setOffset($offset); - $childrenList->setOrderKey("FIELD(assets.type, 'folder') DESC, CAST(assets.filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci ASC", false); - - \OpenDxp\Model\Element\Service::addTreeFilterJoins($cv, $childrenList); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); - /** @var Asset\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - $children = $childrenList->load(); - - $filteredTotalCount = $childrenList->getTotalCount(); - - foreach ($children as $childAsset) { - $assetTreeNode = $this->getTreeNodeConfig($childAsset); - if ($assetTreeNode['permissions']['list'] == 1) { - $assets[] = $assetTreeNode; - } - } - } - - //Hook for modifying return value - e.g. for changing permissions based on asset data - $event = new GenericEvent($this, [ - 'assets' => $assets, - ]); - $eventDispatcher->dispatch($event, AdminEvents::ASSET_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); - $assets = $event->getArgument('assets'); - - if ($allParams['limit']) { - return $this->adminJson([ - 'offset' => $offset, - 'limit' => $limit, - 'total' => $asset->getChildAmount($this->getAdminUser()), - 'overflow' => !is_null($filter) && ($filteredTotalCount > $limit), - 'nodes' => $assets, - 'filter' => $request->query->get('filter') ?: '', - 'inSearch' => (int)$request->query->get('inSearch'), - ]); - } - - return $this->adminJson($assets); - } - - #[Route('/add-asset', name: 'opendxp_admin_asset_addasset', methods: ['POST'])] - public function addAssetAction(Request $request, Config $config): JsonResponse - { - try { - $res = $this->addAsset($request, $config); - - $response = [ - 'success' => $res['success'], - ]; - - if ($res['success']) { - $response['asset'] = [ - 'id' => $res['asset']->getId(), - 'path' => $res['asset']->getFullPath(), - 'type' => $res['asset']->getType(), - ]; - } - - return $this->adminJson($response); - } catch (Exception $e) { - return $this->adminJson([ - 'success' => false, - 'message' => $e->getMessage(), - ]); - } - } - - #[Route('/add-asset-compatibility', name: 'opendxp_admin_asset_addassetcompatibility', methods: ['POST'])] - public function addAssetCompatibilityAction(Request $request, Config $config): JsonResponse - { + public function getDataByIdAction( + GetAssetDataPayload $payload, + GetAssetDataHandler $getAssetData, + ): JsonResponse { try { - // this is a special action for the compatibility mode upload (without flash) - $res = $this->addAsset($request, $config); - - $response = $this->adminJson([ - 'success' => $res['success'], - 'msg' => $res['success'] ? 'Success' : 'Error', - 'id' => $res['asset'] ? $res['asset']->getId() : null, - 'fullpath' => $res['asset'] ? $res['asset']->getRealFullPath() : null, - 'type' => $res['asset'] ? $res['asset']->getType() : null, - ]); - $response->headers->set('Content-Type', 'text/html'); - - return $response; - } catch (Exception $e) { - return $this->adminJson([ - 'success' => false, - 'message' => $e->getMessage(), - ]); - } - } - - /** - * @throws Exception - */ - #[Route('/exists', name: 'opendxp_admin_asset_exists', methods: ['GET'])] - public function existsAction(Request $request): JsonResponse - { - $parentAsset = \OpenDxp\Model\Asset::getById((int)$request->query->get('parentId')); - - $dir = $request->query->get('dir', ''); - if ($dir) { - // this is for uploading folders with Drag&Drop - // param "dir" contains the relative path of the file - if (str_contains($dir, '..')) { - throw new Exception('not allowed'); - } - $dir = '/' . trim($dir, '/ '); - } - - $assetPath = $parentAsset->getRealFullPath() . $dir . '/' . $request->query->get('filename'); - - return new JsonResponse([ - 'exists' => Asset\Service::pathExists($assetPath), - ]); - } - - /** - * @return array{success: bool, asset: ?Asset} - * - * @throws Exception - */ - protected function addAsset(Request $request, Config $config): array - { - $defaultUploadPath = $config['assets']['default_upload_path'] ?? '/'; - - if ($request->files->has('Filedata')) { - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $filename = $file->getClientOriginalName(); - $sourcePath = $file->getPathname(); - } elseif ($request->request->get('type') === 'base64') { - $filename = $request->request->get('filename'); - $sourcePath = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/upload-base64' . uniqid('', false) . '.tmp'; - $data = preg_replace('@^data:[^,]+;base64,@', '', $request->request->get('data')); - $filesystem = new Filesystem(); - $filesystem->dumpFile($sourcePath, base64_decode($data)); - } else { - throw new Exception('The filename of the asset is empty'); - } - - $parentId = $request->query->getInt('parentId'); - $parentPath = $request->query->get('parentPath'); - - if ($request->query->has('dir') && $request->query->has('parentId')) { - // this is for uploading folders with Drag&Drop - // param "dir" contains the relative path of the file - $parent = Asset::getById((int) $request->query->get('parentId')); - $dir = $request->query->get('dir'); - if (str_contains($dir, '..')) { - throw new Exception('not allowed'); - } - - $newPath = $parent->getRealFullPath() . '/' . trim($dir, '/ '); - - $maxRetries = 5; - $newParent = null; - for ($retries = 0; $retries < $maxRetries; $retries++) { - try { - $newParent = Asset\Service::createFolderByPath($newPath); - - break; - } catch (Exception $e) { - if ($retries < ($maxRetries - 1)) { - $waitTime = random_int(100000, 900000); // microseconds - usleep($waitTime); // wait specified time until we restart the transaction - } else { - // if the transaction still fail after $maxRetries retries, we throw out the exception - throw $e; - } - } - } - if ($newParent) { - $parentId = $newParent->getId(); - } - } elseif (!$request->query->get('parentId') && $parentPath) { - $parent = Asset::getByPath($parentPath); - if ($parent instanceof Asset\Folder) { - $parentId = $parent->getId(); - } else { //create defined parent folder, if doesn't exist. - $parentId = Asset\Service::createFolderByPath($parentPath)->getId(); - } - } - - $filename = Element\Service::getValidKey($filename, 'asset'); - if (empty($filename)) { - throw new Exception('The filename of the asset is empty'); - } - - $context = $request->query->get('context'); - if ($context) { - $context = json_decode($context, true); - $context = $context ?: []; - - $this->validateManyToManyRelationAssetType($context, $filename, $sourcePath); - - $event = new ResolveUploadTargetEvent($parentId, $filename); - $event->setArgument('context', $context); - - OpenDxp::getEventDispatcher()->dispatch($event, AssetEvents::RESOLVE_UPLOAD_TARGET); - $filename = Element\Service::getValidKey($event->getFilename(), 'asset'); - $parentId = $event->getParentId(); - } - - if (!$parentId) { - $parentId = Asset\Service::createFolderByPath($defaultUploadPath)->getId(); - } - - $parentAsset = Asset::getById((int)$parentId); - - if (!$request->query->get('allowOverwrite')) { - // check for duplicate filename - $filename = $this->getSafeFilename($parentAsset->getRealFullPath(), $filename); - } - - if (!$parentAsset->isAllowed('create')) { - throw $this->createAccessDeniedHttpException( - 'Missing the permission to create new assets in the folder: ' . $parentAsset->getRealFullPath() - ); - } - if (is_file($sourcePath) && filesize($sourcePath) < 1) { - throw new Exception('File is empty!'); - } - - if (!is_file($sourcePath)) { - throw new Exception('Something went wrong, please check upload_max_filesize and post_max_size in your php.ini as well as the write permissions of your temporary directories.'); - } - - // check if there is a requested type and if matches the asset type of the uploaded file - $uploadAssetType = $request->query->get('uploadAssetType'); - if ($uploadAssetType) { - $mimetype = MimeTypes::getDefault()->guessMimeType($sourcePath); - $assetType = Asset::getTypeFromMimeMapping($mimetype, $filename); - - if ($uploadAssetType !== $assetType) { - throw new Exception("Mime type $mimetype does not match with asset type: $uploadAssetType"); - } - } - - if ($request->query->get('allowOverwrite') && Asset\Service::pathExists($parentAsset->getRealFullPath().'/'.$filename)) { - $asset = Asset::getByPath($parentAsset->getRealFullPath().'/'.$filename); - $asset->setStream(fopen($sourcePath, 'rb', false, File::getContext())); - $asset->save(); - } else { - $asset = Asset::create($parentId, [ - 'filename' => $filename, - 'sourcePath' => $sourcePath, - 'userOwner' => $this->getAdminUser()->getId(), - 'userModification' => $this->getAdminUser()->getId(), - ]); - } - - @unlink($sourcePath); - - return [ - 'success' => true, - 'asset' => $asset, - ]; - } - - protected function getSafeFilename(string $targetPath, string $filename): string - { - $pathinfo = pathinfo($filename); - $originalFilename = $pathinfo['filename']; - $originalFileextension = empty($pathinfo['extension']) ? '' : '.' . $pathinfo['extension']; - $count = 1; - - if ($targetPath === '/') { - $targetPath = ''; + $result = $getAssetData($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - while (true) { - if (Asset\Service::pathExists($targetPath . '/' . $filename)) { - $filename = $originalFilename . '_' . $count . $originalFileextension; - $count++; - } else { - return $filename; - } - } + return $this->adminJson($result->data); } - /** - * @throws Exception - */ - #[Route('/replace-asset', name: 'opendxp_admin_asset_replaceasset', methods: ['POST', 'PUT'])] - public function replaceAssetAction(Request $request, TranslatorInterface $translator): JsonResponse - { - $asset = Asset::getById((int) $request->query->get('id')); - - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - - $newFilename = Element\Service::getValidKey($file->getClientOriginalName(), 'asset'); - $mimetype = MimeTypes::getDefault()->guessMimeType($file->getPathname()); - $newType = Asset::getTypeFromMimeMapping($mimetype, $newFilename); + #[Route('/tree-get-children-by-id', name: 'opendxp_admin_asset_treegetchildrenbyid', methods: ['GET'])] + public function treeGetChildrenByIdAction( + GetAssetChildrenPayload $payload, + GetAssetChildrenHandler $getChildren, + ): JsonResponse { + $result = $getChildren($payload); - if ($newType !== $asset->getType()) { + if ($payload->hasLimit) { return $this->adminJson([ - 'success' => false, - 'message' => sprintf($translator->trans('asset_type_change_not_allowed', [], 'admin'), $newType, $asset->getType()), - ]); - } - - $stream = fopen($file->getPathname(), 'rb+'); - $asset->setStream($stream); - $asset->setCustomSetting('thumbnails', null); - - if (method_exists($asset, 'getEmbeddedMetaData')) { - $asset->getEmbeddedMetaData(true); - } - - $asset->setUserModification($this->getAdminUser()->getId()); - - $newFileExt = pathinfo($newFilename, PATHINFO_EXTENSION); - $currentFileExt = pathinfo($asset->getFilename(), PATHINFO_EXTENSION); - if ($newFileExt != $currentFileExt) { - $newFilename = preg_replace('/\.' . $currentFileExt . '$/i', '.' . $newFileExt, $asset->getFilename()); - $newFilename = Element\Service::getSafeCopyName($newFilename, $asset->getParent()); - $asset->setFilename($newFilename); - } - - if ($asset->isAllowed('publish')) { - $asset->save(); - - $response = $this->adminJson([ - 'id' => $asset->getId(), - 'path' => $asset->getRealFullPath(), - 'success' => true, + 'offset' => $result->offset, + 'limit' => $result->limit, + 'total' => $result->totalChildCount, + 'overflow' => $payload->filter !== null && ($result->filteredTotalCount > $result->limit), + 'nodes' => $result->assets, + 'filter' => $result->filter ?: '', + 'inSearch' => $payload->inSearch, ]); - - // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in - // Ext.form.Action.Submit and mark the submission as failed - $response->headers->set('Content-Type', 'text/html'); - - return $response; } - throw new Exception('missing permission'); + return $this->adminJson($result->assets); } #[Route('/add-folder', name: 'opendxp_admin_asset_addfolder', methods: ['POST'])] - public function addFolderAction(Request $request): JsonResponse + public function addFolderAction(CreateAssetFolderPayload $payload, CreateAssetFolderHandler $createFolder): JsonResponse { - $success = false; - $parentAsset = Asset::getById((int)$request->request->get('parentId')); - $equalAsset = Asset::getByPath($parentAsset->getRealFullPath() . '/' . $request->request->get('name')); - - if ($parentAsset->isAllowed('create')) { - if (!$equalAsset) { - $asset = Asset::create($request->request->get('parentId'), [ - 'filename' => $request->request->get('name'), - 'type' => 'folder', - 'userOwner' => $this->getAdminUser()->getId(), - 'userModification' => $this->getAdminUser()->getId(), - ]); - $success = true; - } - } else { - Logger::debug('prevented creating asset because of missing permissions'); - } + $createFolder($payload); - return $this->adminJson(['success' => $success]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/delete', name: 'opendxp_admin_asset_delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse + public function deleteAction(DeleteAssetPayload $payload, DeleteAssetHandler $deleteAsset): JsonResponse { - $type = $request->request->get('type'); - - if ($type === 'children') { - $parentAsset = Asset::getById((int) $request->request->get('id')); + $result = $deleteAsset($payload); - $list = new Asset\Listing(); - $list->setCondition('`path` LIKE ?', [Helper::escapeLike($parentAsset->getRealFullPath()) . '/%']); - $list->setLimit((int)$request->request->get('amount')); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('DESC'); - - $deletedItems = []; - foreach ($list as $asset) { - $deletedItems[$asset->getId()] = $asset->getRealFullPath(); - if ($asset->isAllowed('delete') && !$asset->isLocked()) { - $asset->delete(); - } - } - - return $this->adminJson(['success' => true, 'deleted' => $deletedItems]); - } - if ($request->request->has('id')) { - $asset = Asset::getById((int) $request->request->get('id')); - if ($asset && $asset->isAllowed('delete')) { - if ($asset->isLocked()) { - return $this->adminJson([ - 'success' => false, - 'message' => 'prevented deleting asset, because it is locked: ID: ' . $asset->getId(), - ]); - } - - $asset->delete(); - - return $this->adminJson(['success' => true]); - } + if ($result->deleted) { + return $this->adminJson(ApiResponse::ok(['deleted' => $result->deleted])); } - throw $this->createAccessDeniedHttpException(); - } - - /** - * @throws Exception - */ - #[Override] - protected function getTreeNodeConfig(ElementInterface $element): array - { - return $this->elementService->getElementTreeNodeConfig($element); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - * @throws RuntimeException - */ #[Route('/update', name: 'opendxp_admin_asset_update', methods: ['PUT'])] - public function updateAction(Request $request): JsonResponse + public function updateAction(UpdateAssetPayload $payload, UpdateAssetHandler $updateAsset): JsonResponse { - $data = ['success' => false]; - $allowUpdate = true; - - $updateData = [...$request->request->all(), ...$request->query->all()]; - - $asset = Asset::getById((int) $request->request->get('id')); - if ($asset->isAllowed('settings')) { - $asset->setUserModification($this->getAdminUser()->getId()); - - // if the position is changed the path must be changed || also from the children - if ($parentId = $request->request->get('parentId')) { - $parentAsset = Asset::getById((int) $parentId); - - //check if parent is changed i.e. asset is moved - if ($asset->getParentId() !== $parentAsset->getId()) { - if (!$parentAsset->isAllowed('create')) { - throw new RuntimeException('Prevented moving asset - no create permission on new parent.'); - } - - $intendedPath = $parentAsset->getRealPath(); - $pKey = $parentAsset->getKey(); - if (!empty($pKey)) { - $intendedPath .= $parentAsset->getKey() . '/'; - } - - $assetWithSamePath = Asset::getByPath($intendedPath . $asset->getKey()); - - if ($assetWithSamePath != null) { - $allowUpdate = false; - } - - if ($asset->isLocked()) { - $allowUpdate = false; - } - } - } - - if ($allowUpdate) { - if ($request->request->get('filename') != $asset->getFilename() && !$asset->isAllowed('rename')) { - unset($updateData['filename']); - Logger::debug('prevented renaming asset because of missing permissions.'); - } - - $asset->setValues($updateData); - - try { - $asset->save(); - $data = [ - 'success' => true, - 'treeData' => $this->getTreeNodeConfig($asset), - ]; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - $msg = 'prevented moving asset, asset with same path+key already exists'; - $msg .= ' at target location or the asset is locked. ID: ' . $asset->getId(); - Logger::debug($msg); - - return $this->adminJson(['success' => false, 'message' => $msg]); - } - } elseif ($asset->isAllowed('rename') && $request->request->has('filename')) { - //just rename - try { - $asset->setFilename($request->request->get('filename')); - $asset->save(); - $data = [ - 'success' => true, - 'treeData' => $this->getTreeNodeConfig($asset), - ]; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - Logger::debug('prevented update asset because of missing permissions '); - } + $result = $updateAsset($payload); - return $this->adminJson($data); + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData])); } - /** - * @throws Exception - */ #[Route('/save', name: 'opendxp_admin_asset_save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $asset = Asset::getById((int) $request->request->get('id')); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if ($asset->isAllowed('publish')) { - // metadata - if ($request->request->has('metadata')) { - $metadata = $this->decodeJson($request->request->get('metadata')); - - $metadataEvent = new GenericEvent($this, [ - 'id' => $asset->getId(), - 'metadata' => $metadata, - ]); - $eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); - - $metadata = $metadataEvent->getArgument('metadata'); - $metadataValues = $metadata['values']; - - $metadataValues = Asset\Service::minimizeMetadata($metadataValues, 'editor'); - $asset->setMetadataRaw($metadataValues); - } - - // properties - if ($request->request->has('properties')) { - $properties = []; - $propertiesData = $this->decodeJson($request->request->get('properties')); - - if (is_array($propertiesData)) { - foreach ($propertiesData as $propertyName => $propertyData) { - $value = $propertyData['data']; - - try { - $property = new Model\Property(); - $property->setType($propertyData['type']); - $property->setName($propertyName); - $property->setCtype('asset'); - $property->setDataFromEditmode($value); - $property->setInheritable($propertyData['inheritable']); - - $properties[$propertyName] = $property; - } catch (Exception) { - Logger::err("Can't add " . $propertyName . ' to asset ' . $asset->getRealFullPath()); - } - } - - $asset->setProperties($properties); - } - } - - $this->applySchedulerDataToElement($request, $asset, $this->getAdminUser()); - - if ($request->request->get('data')) { - $asset->setData($request->request->get('data')); - } - - // image specific data - if ($asset instanceof Asset\Image) { - if ($request->request->has('image')) { - $imageData = $this->decodeJson($request->request->get('image')); - if (isset($imageData['focalPoint'])) { - $asset->setCustomSetting('focalPointX', $imageData['focalPoint']['x']); - $asset->setCustomSetting('focalPointY', $imageData['focalPoint']['y']); - } - } else { - // wipe all data - $asset->removeCustomSetting('focalPointX'); - $asset->removeCustomSetting('focalPointY'); - } - } - - $asset->setUserModification($this->getAdminUser()->getId()); - if ($request->request->get('task') === 'session') { - // save to session only - Asset\Service::saveElementToSession($asset, $request->getSession()->getId()); - } else { - $asset->save(); - } - - $treeData = $this->getTreeNodeConfig($asset); - - return $this->adminJson([ - 'success' => true, - 'data' => [ - 'versionDate' => $asset->getModificationDate(), - 'versionCount' => $asset->getVersionCount(), - ], - 'treeData' => $treeData, - ]); - } - - throw $this->createAccessDeniedHttpException(); - } - - #[Route('/publish-version', name: 'opendxp_admin_asset_publishversion', methods: ['POST'])] - public function publishVersionAction(Request $request): JsonResponse - { - $id = (int)$request->request->get('id'); - $version = Model\Version::getById($id); - $asset = $version?->loadData(); - - if (!$asset) { - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - - $currentAsset = Asset::getById($asset->getId()); - if ($currentAsset->isAllowed('publish')) { - try { - $asset->setUserModification($this->getAdminUser()->getId()); - $asset->save(); - - $treeData = $this->getTreeNodeConfig($asset); - - return $this->adminJson(['success' => true, 'treeData' => $treeData]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - throw $this->createAccessDeniedHttpException(); - } - - #[Route('/show-version', name: 'opendxp_admin_asset_showversion', methods: ['GET'])] - public function showVersionAction(Request $request, Environment $twig): Response + public function saveAction(SaveAssetHandler $saveAsset, SaveAssetPayload $payload): JsonResponse { - $id = (int)$request->query->get('id'); - $version = Model\Version::getById($id); - $asset = $version?->loadData(); - if (!$asset) { - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - - if (!$asset->isAllowed('versions')) { - throw $this->createAccessDeniedHttpException('Permission denied, version id [' . $id . ']'); - } - - if ($asset instanceof Asset\Document && $asset->getMimeType() === self::PDF_MIMETYPE) { - $previewData = ['thumbnailPath' => '']; - $previewData['assetPath'] = $asset->getRealFullPath(); - - return $this->render( - '@OpenDxpAdmin/admin/asset/get_preview_pdf_open_in_new_tab.html.twig', - $previewData - ); - } + $result = $saveAsset($payload); - Tool\UserTimezone::setUserTimezone($request->query->get('userTimezone')); - - if ($timezone = Tool\UserTimezone::getUserTimezone()) { - $twig->getExtension(CoreExtension::class)->setTimezone($timezone); - } - - $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); - - return $this->render( - '@OpenDxpAdmin/admin/asset/show_version_' . strtolower($asset->getType()) . '.html.twig', - [ - 'asset' => $asset, - 'version' => $version, - 'loader' => $loader, - ] - ); + return $this->adminJson(ApiResponse::ok([ + 'data' => [ + 'versionDate' => $result->versionDate, + 'versionCount' => $result->versionCount, + ], + 'treeData' => $result->treeData, + ])); } - #[Route('/download', name: 'opendxp_admin_asset_download', methods: ['GET'])] - public function downloadAction(Request $request): StreamedResponse + #[Route('/clear-thumbnail', name: 'opendxp_admin_asset_clearthumbnail', methods: ['POST'])] + public function clearThumbnailAction(ClearAssetThumbnailPayload $payload, ClearAssetThumbnailHandler $clearThumbnail): JsonResponse { - $asset = Asset::getById((int) $request->query->get('id')); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view asset'); - } - - $stream = $asset->getStream(); - - if (!is_resource($stream)) { - throw $this->createNotFoundException('Unable to get resource for asset ' . $asset->getId()); - } + $clearThumbnail($payload); - return new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => $asset->getMimeType(), - 'Content-Disposition' => sprintf('attachment; filename: "%s"', $asset->getFilename()), - 'Content-Length' => $asset->getFileSize(), - ]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/download-image-thumbnail', name: 'opendxp_admin_asset_downloadimagethumbnail', methods: ['GET'])] - public function downloadImageThumbnailAction(Request $request): BinaryFileResponse + #[Override] + protected function getTreeNodeConfig(ElementInterface $element): array { - $image = Asset\Image::getById((int) $request->query->get('id')); - - if (!$image) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$image->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view thumbnail'); - } - - $config = null; - $thumbnail = null; - $thumbnailName = $request->query->get('thumbnail'); - $thumbnailFile = null; - $deleteThumbnail = true; - - if ($request->query->has('config')) { - $config = $this->decodeJson($request->query->get('config')); - } elseif ($request->query->has('type')) { - $predefined = [ - 'web' => [ - 'resize_mode' => 'scaleByWidth', - 'width' => 3500, - 'dpi' => 72, - 'format' => 'JPEG', - 'quality' => 85, - ], - 'print' => [ - 'resize_mode' => 'scaleByWidth', - 'width' => 6000, - 'dpi' => 300, - 'format' => 'JPEG', - 'quality' => 95, - ], - 'office' => [ - 'resize_mode' => 'scaleByWidth', - 'width' => 1190, - 'dpi' => 144, - 'format' => 'JPEG', - 'quality' => 90, - ], - ]; - - $config = $predefined[$request->query->get('type')]; - } elseif ($thumbnailName) { - $thumbnail = $image->getThumbnail($thumbnailName); - $deleteThumbnail = false; - } - - if ($config) { - $thumbnailConfig = new Asset\Image\Thumbnail\Config(); - $thumbnailConfig->setName('opendxp-download-' . $image->getId() . '-' . md5($request->query->get('config'))); - - if ($config['resize_mode'] === 'scaleByWidth') { - $thumbnailConfig->addItem('scaleByWidth', [ - 'width' => $config['width'], - ]); - } elseif ($config['resize_mode'] === 'scaleByHeight') { - $thumbnailConfig->addItem('scaleByHeight', [ - 'height' => $config['height'], - ]); - } else { - $thumbnailConfig->addItem('resize', [ - 'width' => $config['width'], - 'height' => $config['height'], - ]); - } - - if (!empty($config['quality']) && $config['quality'] <= 100 && $config['quality'] > 0) { - $thumbnailConfig->setQuality($config['quality']); - } - - if (!empty($config['format'])) { - $thumbnailConfig->setFormat($config['format']); - } - - $thumbnailConfig->setRasterizeSVG(true); - - if ($thumbnailConfig->getFormat() === 'JPEG') { - $thumbnailConfig->setPreserveMetaData(true); - - if (empty($config['quality'])) { - $thumbnailConfig->setPreserveColor(true); - } - } - - $thumbnail = $image->getThumbnail($thumbnailConfig); - $thumbnailFile = $thumbnail->getLocalFile(); - - $exiftool = \OpenDxp\Tool\Console::getExecutable('exiftool'); - if ($thumbnailConfig->getFormat() === 'JPEG' && $exiftool && isset($config['dpi']) && $config['dpi']) { - $process = new Process([$exiftool, '-overwrite_original', '-xresolution=' . (int)$config['dpi'], '-yresolution=' . (int)$config['dpi'], '-resolutionunit=inches', $thumbnailFile]); - $process->run(); - } - } - - if ($thumbnail) { - $thumbnailConfig = $thumbnail->getConfig(); - if ($thumbnailConfig->getFormat() === 'SOURCE' && - $autoFormatConfigs = $thumbnailConfig->getAutoFormatThumbnailConfigs()) { - $autoFormatConfig = current($autoFormatConfigs); - $thumbnail = $image->getThumbnail($autoFormatConfig); - } - - $thumbnailFile = $thumbnailFile ?: $thumbnail->getLocalFile(); - - $downloadFilename = preg_replace( - '/\.' . preg_quote(pathinfo($image->getFilename(), PATHINFO_EXTENSION), '/') . '$/i', - '.' . $thumbnail->getFileExtension(), - $image->getFilename() - ); - - clearstatcache(); - - $response = new BinaryFileResponse($thumbnailFile); - $response->headers->set('Content-Type', $thumbnail->getMimeType()); - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $downloadFilename); - $this->addThumbnailCacheHeaders($response); - $response->deleteFileAfterSend($deleteThumbnail); - - return $response; - } - - throw $this->createNotFoundException('Thumbnail not found'); - } - - #[Route('/get-asset', name: 'opendxp_admin_asset_getasset', methods: ['GET'])] - public function getAssetAction(Request $request): StreamedResponse - { - $image = Asset::getById((int)$request->query->get('id')); - - if (!$image) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$image->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view asset'); - } - - $stream = $image->getStream(); - - if (!is_resource($stream)) { - throw $this->createNotFoundException('Unable to get resource for asset ' . $image->getId()); - } - - $response = new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => $image->getMimeType(), - 'Access-Control-Allow-Origin' => '*', - ]); - $this->addThumbnailCacheHeaders($response); - - return $response; - } - - #[Route('/get-image-thumbnail', name: 'opendxp_admin_asset_getimagethumbnail', methods: ['GET'])] - public function getImageThumbnailAction(Request $request): BinaryFileResponse|JsonResponse|StreamedResponse - { - $fileinfo = $request->query->get('fileinfo'); - $image = Asset\Image::getById((int)$request->query->get('id')); - - if (!$image) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$image->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view thumbnail'); - } - - $thumbnailConfig = null; - - if ($thumbnailParam = $request->query->all()['thumbnail'] ?? null) { - $thumbnailConfig = $image->getThumbnail($thumbnailParam)->getConfig(); - } - if (!$thumbnailConfig) { - if ($request->query->get('config')) { - $thumbnailConfig = $image->getThumbnail($this->decodeJson($request->query->get('config')))->getConfig(); - } else { - $thumbnailConfig = $image->getThumbnail($request->query->all())->getConfig(); - } - } else { - // no high-res images in admin mode (editmode) - // this is mostly because of the document's image editable, which doesn't know anything about the thumbnail - // configuration, so the dimensions would be incorrect (double the size) - $thumbnailConfig->setHighResolution(1); - } - - $format = strtolower($thumbnailConfig->getFormat()); - if ($format === 'source' || $format === 'print') { - $thumbnailConfig->setFormat('PNG'); - $thumbnailConfig->setRasterizeSVG(true); - } - - if ($request->query->get('treepreview')) { - $thumbnailConfig = Asset\Image\Thumbnail\Config::getPreviewConfig(); - if (!$image->getThumbnail($thumbnailConfig)->exists()) { - - OpenDxp::getContainer()->get('messenger.bus.opendxp-core')->dispatch( - new AssetPreviewImageMessage($image->getId()) - ); - - if ($request->query->get('origin') === 'folderPreview') { - - $response = new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/video-loading.gif'); - $response->headers->set('Cache-Control', 'no-store'); - - return $response; - } - - throw $this->createNotFoundException(sprintf('Tree preview thumbnail not available for asset %s', $image->getId())); - } - } - - $cropPercent = $request->query->get('cropPercent'); - if ($cropPercent && filter_var($cropPercent, FILTER_VALIDATE_BOOLEAN)) { - $thumbnailConfig->addItemAt(0, 'cropPercent', [ - 'width' => $request->query->get('cropWidth'), - 'height' => $request->query->get('cropHeight'), - 'y' => $request->query->get('cropTop'), - 'x' => $request->query->get('cropLeft'), - ]); - - $thumbnailConfig->generateAutoName(); - } - - $thumbnail = $image->getThumbnail($thumbnailConfig); - - if ($fileinfo) { - return $this->adminJson([ - 'width' => $thumbnail->getWidth(), - 'height' => $thumbnail->getHeight(), ]); - } - - $stream = $thumbnail->getStream(); - - if (!$stream) { - return new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/filetype-not-supported.svg'); - } - - $response = new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => $thumbnail->getMimeType(), - 'Access-Control-Allow-Origin' => '*', - ]); - - $this->addThumbnailCacheHeaders($response); - - return $response; - } - - #[Route('/get-folder-thumbnail', name: 'opendxp_admin_asset_getfolderthumbnail', methods: ['GET'])] - public function getFolderThumbnailAction(Request $request): StreamedResponse - { - if ($request->query->has('id')) { - $folder = Asset\Folder::getById((int)$request->query->get('id')); - if ($folder instanceof Asset\Folder) { - if (!$folder->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view thumbnail'); - } - - $stream = $folder->getPreviewImage(); - if (!$stream) { - throw $this->createNotFoundException(sprintf('Tree preview thumbnail not available for asset %s', $folder->getId())); - } - $response = new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => 'image/jpg', - ]); - - $this->addThumbnailCacheHeaders($response); - - return $response; - } - } - - throw $this->createNotFoundException('could not load asset folder'); - } - - #[Route('/get-video-thumbnail', name: 'opendxp_admin_asset_getvideothumbnail', methods: ['GET'])] - public function getVideoThumbnailAction(Request $request): StreamedResponse - { - $video = null; - - if ($request->query->has('id')) { - $video = Asset\Video::getById((int)$request->query->get('id')); - } elseif ($request->query->has('path')) { - $video = Asset\Video::getByPath($request->query->get('path')); - } - - if (!$video) { - throw $this->createNotFoundException('could not load video asset'); - } - - if (!$video->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view thumbnail'); - } - - $thumbnailConfig = $request->query->all(); - - if ($request->query->has('treepreview')) { - $thumbnailConfig = Asset\Image\Thumbnail\Config::getPreviewConfig(); - } - - $time = null; - if (is_numeric($request->query->get('time'))) { - $time = (int)$request->query->get('time'); - } - - if ($request->query->has('settime')) { - $video->removeCustomSetting('image_thumbnail_asset'); - $video->setCustomSetting('image_thumbnail_time', $time); - $video->save(); - } - - $image = null; - if ($request->query->has('image')) { - $image = Asset\Image::getById((int)$request->query->get('image')); - } - - if ($request->query->has('setimage') && $image) { - $video->removeCustomSetting('image_thumbnail_time'); - $video->setCustomSetting('image_thumbnail_asset', $image->getId()); - $video->save(); - } - - $thumb = $video->getImageThumbnail($thumbnailConfig, $time, $image); - - if ($request->query->get('origin') === 'treeNode' && !$thumb->exists()) { - OpenDxp::getContainer()->get('messenger.bus.opendxp-core')->dispatch( - new AssetPreviewImageMessage($video->getId()) - ); - - throw $this->createNotFoundException(sprintf('Tree preview thumbnail not available for asset %s', $video->getId())); - } - - $stream = $thumb->getStream(); - if (!$stream) { - throw $this->createNotFoundException('Unable to get video thumbnail for video ' . $video->getId()); - } - - $response = new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => 'image/' . $thumb->getFileExtension(), - ]); - - $this->addThumbnailCacheHeaders($response); - - return $response; - } - - #[Route('/get-document-thumbnail', name: 'opendxp_admin_asset_getdocumentthumbnail', methods: ['GET'])] - public function getDocumentThumbnailAction(Request $request): BinaryFileResponse|StreamedResponse - { - $document = Asset\Document::getById((int)$request->query->get('id')); - - if (!$document) { - throw $this->createNotFoundException('could not load document asset'); - } - - if (!$document->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view thumbnail'); - } - - $thumbnail = Asset\Image\Thumbnail\Config::getByAutoDetect($request->query->all()); - - $format = strtolower($thumbnail->getFormat()); - if ($format === 'source') { - $thumbnail->setFormat('jpeg'); // default format for documents is JPEG not PNG (=too big) - } - - if ($request->query->get('treepreview')) { - $thumbnail = Asset\Image\Thumbnail\Config::getPreviewConfig(); - } - - $page = 1; - if (is_numeric($request->query->get('page'))) { - $page = (int)$request->query->get('page'); - } - - $thumb = $document->getImageThumbnail($thumbnail, $page); - - if ($request->query->get('origin') === 'treeNode' && !$thumb->exists()) { - OpenDxp::getContainer()->get('messenger.bus.opendxp-core')->dispatch( - new AssetPreviewImageMessage($document->getId()) - ); - - throw $this->createNotFoundException(sprintf('Tree preview thumbnail not available for asset %s', $document->getId())); - } - - $stream = $thumb->getStream(); - if ($stream) { - $response = new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => 'image/' . $thumb->getFileExtension(), - ]); - } else { - $response = new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/filetype-not-supported.svg'); - } - - $this->addThumbnailCacheHeaders($response); - - return $response; - } - - protected function addThumbnailCacheHeaders(Response $response): void - { - $lifetime = 300; - $date = new DateTime('now'); - $date->add(new DateInterval('PT' . $lifetime . 'S')); - - $response->setMaxAge($lifetime); - $response->setPublic(); - $response->setExpires($date); - $response->headers->set('Pragma', ''); - } - - #[Route('/get-preview-document', name: 'opendxp_admin_asset_getpreviewdocument', methods: ['GET'])] - public function getPreviewDocumentAction(Request $request): StreamedResponse|Response - { - $asset = Asset\Document::getById((int) $request->query->get('id')); - - if (!$asset instanceof Asset\Document) { - throw $this->createNotFoundException('could not load document asset'); - } - - if ($asset->isAllowed('view')) { - - if ($asset->getMimeType() === self::PDF_MIMETYPE) { - $scanResponse = $this->getResponseByScanStatus($asset); - $openPdfConfig = Config::getSystemConfiguration('assets')['document']['open_pdf_in_new_tab']; - - if ($openPdfConfig === 'all-pdfs' || - ($openPdfConfig === 'only-unsafe' && $scanResponse === PdfScanStatus::UNSAFE)) { - $thumbnail = $asset->getImageThumbnail(Asset\Image\Thumbnail\Config::getPreviewConfig()); - $previewData = ['thumbnailPath' => $thumbnail->getPath()]; - $previewData['assetPath'] = $asset->getRealFullPath(); - - return $this->render( - '@OpenDxpAdmin/admin/asset/get_preview_pdf_open_in_new_tab.html.twig', - $previewData - ); - } - - if ($scanResponse === PdfScanStatus::IN_PROGRESS) { - return $this->render('@OpenDxpAdmin/admin/asset/get_preview_pdf_in_progress.html.twig'); - } - - if ($scanResponse === PdfScanStatus::UNSAFE) { - return $this->render('@OpenDxpAdmin/admin/asset/get_preview_pdf_unsafe.html.twig'); - } - } - - $stream = $this->getDocumentPreviewPdf($asset); - if ($stream) { - return new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => self::PDF_MIMETYPE, - ]); - } - - throw $this->createNotFoundException('Unable to get preview for asset ' . $asset->getId()); - - } - - throw $this->createAccessDeniedException('Access to asset ' . $asset->getId() . ' denied'); - } - - private function getResponseByScanStatus(Asset\Document $asset, bool $processBackground = true): ?PdfScanStatus - { - if (!Config::getSystemConfiguration('assets')['document']['scan_pdf']) { - return null; - } - - $scanStatus = $asset->getScanStatus(); - if (!$scanStatus instanceof \OpenDxp\Model\Asset\Enum\PdfScanStatus) { - $scanStatus = Asset\Enum\PdfScanStatus::IN_PROGRESS; - if ($processBackground) { - $asset->addToUpdateTaskQueue(); - } - } - - return $scanStatus; - } - - /** - * @return resource|null - */ - protected function getDocumentPreviewPdf(Asset\Document $asset) - { - $stream = null; - - if ($asset->getMimeType() == self::PDF_MIMETYPE) { - $stream = $asset->getStream(); - } - - if ( - !$stream && - $asset->getPageCount() && - \OpenDxp\Document::isAvailable() && - \OpenDxp\Document::isFileTypeSupported($asset->getFilename()) - ) { - try { - $document = \OpenDxp\Document::getInstance(); - $stream = $document->getPdf($asset); - } catch (Exception) { - // nothing to do - } - } - - return $stream; - } - - #[Route('/get-preview-video', name: 'opendxp_admin_asset_getpreviewvideo', methods: ['GET'])] - public function getPreviewVideoAction(Request $request): Response - { - $asset = Asset\Video::getById((int) $request->query->get('id')); - $configName = $request->query->get('config'); - - if (!$asset) { - throw $this->createNotFoundException('could not load video asset'); - } - - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to preview'); - } - - $previewData = ['asset' => $asset]; - - $config = Asset\Video\Thumbnail\Config::getByName($configName); - - if (!$config instanceof Asset\Video\Thumbnail\Config) { - $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); - } - - $thumbnail = $asset->getThumbnail($config, ['mp4']); - - if ($thumbnail) { - $previewData['asset'] = $asset; - $previewData['thumbnail'] = $thumbnail; - $previewData['config'] = $config->getName(); - - if ($thumbnail['status'] === 'finished') { - return $this->render( - '@OpenDxpAdmin/admin/asset/get_preview_video_display.html.twig', - $previewData - ); - } - - return $this->render( - '@OpenDxpAdmin/admin/asset/get_preview_video_error.html.twig', - $previewData - ); - } - - return $this->render( - '@OpenDxpAdmin/admin/asset/get_preview_video_error.html.twig', - $previewData - ); - } - - #[Route('/serve-video-preview', name: 'opendxp_admin_asset_servevideopreview', methods: ['GET'])] - public function serveVideoPreviewAction(Request $request): StreamedResponse - { - $asset = Asset\Video::getById((int) $request->query->get('id')); - $configName = $request->query->get('config'); - - if (!$asset) { - throw $this->createNotFoundException('could not load video asset'); - } - - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to preview'); - } - - $config = Asset\Video\Thumbnail\Config::getByName($configName); - - if (!$config instanceof Asset\Video\Thumbnail\Config) { - $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); - } - - $thumbnail = $asset->getThumbnail($config, ['mp4']); - $storagePath = $asset->getRealPath() . '/' . preg_replace('@^' . preg_quote($asset->getPath(), '@') . '@', '', urldecode($thumbnail['formats']['mp4'])); - - $storage = Tool\Storage::get('thumbnail'); - if ($storage->fileExists($storagePath)) { - $fs = $storage->fileSize($storagePath); - $stream = $storage->readStream($storagePath); - - return new StreamedResponse(function () use ($stream): void { - fpassthru($stream); - }, 200, [ - 'Content-Type' => 'video/mp4', - 'Content-Length' => $fs, - 'Accept-Ranges' => 'bytes', - ]); - } - - throw $this->createNotFoundException('Video thumbnail not found'); - } - - #[Route('/image-editor', name: 'opendxp_admin_asset_imageeditor', methods: ['GET'])] - public function imageEditorAction(Request $request): Response - { - $asset = Asset::getById((int) $request->query->get('id')); - - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('Not allowed to preview'); - } - - return $this->render( - '@OpenDxpAdmin/admin/asset/image_editor.html.twig', - ['asset' => $asset] - ); - } - - #[Route('/image-editor-save', name: 'opendxp_admin_asset_imageeditorsave', methods: ['PUT'])] - public function imageEditorSaveAction(Request $request): JsonResponse - { - $asset = Asset::getById((int) $request->query->get('id')); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$asset->isAllowed('publish')) { - throw $this->createAccessDeniedException('not allowed to publish'); - } - - $data = $request->request->get('dataUri'); - $data = substr($data, strpos($data, ',')); - $data = base64_decode($data); - $asset->setData($data); - $asset->setUserModification($this->getAdminUser()->getId()); - $asset->save(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/get-folder-content-preview', name: 'opendxp_admin_asset_getfoldercontentpreview', methods: ['GET'])] - public function getFolderContentPreviewAction(Request $request, - EventDispatcherInterface $eventDispatcher, - GridHelperService $gridHelperService): JsonResponse - { - $allParams = $request->query->all(); - - $filterPrepareEvent = new GenericEvent($this, [ - 'requestParams' => $allParams, - ]); - $eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::ASSET_LIST_BEFORE_FILTER_PREPARE); - - $allParams = $filterPrepareEvent->getArgument('requestParams'); - - $folder = Asset::getById((int) $allParams['id']); - - $start = 0; - $limit = 10; - - if ($allParams['limit']) { - $limit = $allParams['limit']; - } - if ($allParams['start']) { - $start = $allParams['start']; - } - - $conditionFilters = []; - $list = new Asset\Listing(); - $conditionFilters[] = '`path` LIKE ' . ($folder->getRealFullPath() === '/' ? "'/%'" : $list->quote(Helper::escapeLike($folder->getRealFullPath()) . '/%')) . " AND `type` != 'folder'"; - - $adminUser = $this->getAdminUser(); - - if (!$adminUser->isAdmin()) { - $conditionFilters[] = $gridHelperService->getPermittedPathsByUser('asset', $adminUser); - } - - $condition = implode(' AND ', $conditionFilters); - - $list->setCondition($condition); - $list->setLimit($limit); - $list->setOffset($start); - $list->setOrderKey('CAST(filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci ASC', false); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $list, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); - /** @var Asset\Listing $list */ - $list = $beforeListLoadEvent->getArgument('list'); - - $list->load(); - - $assets = []; - - foreach ($list as $asset) { - $filenameDisplay = $asset->getFilename(); - if (strlen($filenameDisplay) > 32) { - $filenameDisplay = substr($filenameDisplay, 0, 25) . '...' . pathinfo($filenameDisplay, PATHINFO_EXTENSION); - } - - // Like for treeGetChildrenByIdAction, so we respect isAllowed method which can be extended (object DI) for custom permissions, - // so relying only users_workspaces_asset is insufficient and could lead security breach - if ($asset->isAllowed('list')) { - - $assets[] = [ - 'id' => $asset->getId(), - 'type' => $asset->getType(), - 'filename' => $asset->getFilename(), - 'filenameDisplay' => htmlspecialchars($filenameDisplay ?? ''), - 'url' => $this->elementService->getThumbnailUrl($asset, ['origin' => 'folderPreview']), - 'idPath' => Element\Service::getIdPath($asset), - ]; - } - } - - // We need to temporary use data key to be compatible with the ASSET_LIST_AFTER_LIST_LOAD global event - $result = ['data' => $assets, 'success' => true, 'total' => $list->getTotalCount()]; - - $afterListLoadEvent = new GenericEvent($this, [ - 'list' => $result, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::ASSET_LIST_AFTER_LIST_LOAD); - $result = $afterListLoadEvent->getArgument('list'); - - // Here we revert to assets key - return $this->adminJson(['assets' => $result['data'], 'success' => $result['success'], 'total' => $result['total']]); - } - - #[Route('/copy-info', name: 'opendxp_admin_asset_copyinfo', methods: ['GET'])] - public function copyInfoAction(Request $request): JsonResponse - { - $transactionId = time(); - $pasteJobs = []; - - Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($transactionId): void { - $session->set((string) $transactionId, []); - }, 'opendxp_copy'); - - if ($request->query->get('type') === 'recursive') { - $asset = Asset::getById((int) $request->query->get('sourceId')); - - if (!$asset) { - throw $this->createNotFoundException('Source not found'); - } - - // first of all the new parent - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_asset_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => 'child', - 'transactionId' => $transactionId, - 'saveParentId' => true, - ], - ]]; - - if ($asset->hasChildren()) { - // get amount of children - $list = new Asset\Listing(); - $list->setCondition('`path` LIKE ?', [$list->escapeLike($asset->getRealFullPath()) . '/%']); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('ASC'); - $childIds = $list->loadIdList(); - - if (count($childIds) > 0) { - foreach ($childIds as $id) { - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_asset_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $id, - 'targetParentId' => $request->query->get('targetId'), - 'sourceParentId' => $request->query->get('sourceId'), - 'type' => 'child', - 'transactionId' => $transactionId, - ], - ]]; - } - } - } - } elseif ($request->query->get('type') === 'child' || $request->query->get('type') === 'replace') { - // the object itself is the last one - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_asset_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => $request->query->get('type'), - 'transactionId' => $transactionId, - ], - ]]; - } - - return $this->adminJson([ - 'pastejobs' => $pasteJobs, - ]); - } - - #[Route('/copy', name: 'opendxp_admin_asset_copy', methods: ['POST'])] - public function copyAction(Request $request): JsonResponse - { - $success = false; - $sourceId = (int)$request->request->get('sourceId'); - $source = Asset::getById($sourceId); - - $session = Tool\Session::getSessionBag($request->getSession(), 'opendxp_copy'); - $sessionBag = $session->get($request->request->get('transactionId')); - - $targetId = (int)$request->request->get('targetId'); - if ($request->request->has('targetParentId')) { - $sourceParent = Asset::getById((int) $request->request->get('sourceParentId')); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($sessionBag['parentId']) { - $targetParent = Asset::getById((int) $sessionBag['parentId']); - } else { - $targetParent = Asset::getById((int) $request->request->get('targetParentId')); - } - - $targetPath = preg_replace('@^' . $sourceParent->getRealFullPath() . '@', $targetParent . '/', $source->getRealPath()); - $target = Asset::getByPath($targetPath); - } else { - $target = Asset::getById($targetId); - } - - if (!$target) { - throw $this->createNotFoundException('Target not found'); - } - - if ($target->isAllowed('create')) { - $source = Asset::getById($sourceId); - if ($source !== null) { - if ($request->request->get('type') === 'child') { - $newAsset = $this->_assetService->copyAsChild($target, $source); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($request->request->get('saveParentId')) { - $sessionBag['parentId'] = $newAsset->getId(); - } - } elseif ($request->request->get('type') === 'replace') { - $this->_assetService->copyContents($target, $source); - } - - $session->set($request->request->get('transactionId'), $sessionBag); - - $success = true; - } else { - Logger::debug('prevended copy/paste because asset with same path+key already exists in this location'); - } - } else { - Logger::error('could not execute copy/paste because of missing permissions on target [ ' . $targetId . ' ]'); - - throw $this->createAccessDeniedHttpException(); - } - - return $this->adminJson(['success' => $success]); - } - - #[Route('/download-as-zip-jobs', name: 'opendxp_admin_asset_downloadaszipjobs', methods: ['GET'])] - public function downloadAsZipJobsAction(Request $request): JsonResponse - { - $jobId = uniqid('', false); - $filesPerJob = 5; - $jobs = []; - $asset = Asset::getById((int) $request->query->get('id')); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if ($asset->isAllowed('view')) { - $parentPath = $asset->getRealFullPath(); - if ($asset->getId() == 1) { - $parentPath = ''; - } - - $db = \OpenDxp\Db::get(); - $conditionFilters = []; - $selectedIds = explode(',', $request->query->get('selectedIds', '')); - $quotedSelectedIds = []; - foreach ($selectedIds as $selectedId) { - if ($selectedId) { - $quotedSelectedIds[] = $db->quote($selectedId); - } - } - if ($quotedSelectedIds !== []) { - //add a condition if id numbers are specified - $conditionFilters[] = 'id IN (' . implode(',', $quotedSelectedIds) . ')'; - } - $conditionFilters[] = '`path` LIKE ' . $db->quote(Helper::escapeLike($parentPath) . '/%') . ' AND `type` != ' . $db->quote('folder'); - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $userIds[] = $this->getAdminUser()->getId(); - $conditionFilters[] = ' ( - (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`, filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - OR - (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`, filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - )'; - } - - $condition = implode(' AND ', $conditionFilters); - - $assetList = new Asset\Listing(); - $assetList->setCondition($condition); - $assetList->setOrderKey('LENGTH(`path`)', false); - $assetList->setOrder('ASC'); - - for ($i = 0; $i < ceil($assetList->getTotalCount() / $filesPerJob); $i++) { - $jobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_asset_downloadaszipaddfiles'), - 'method' => 'GET', - 'params' => [ - 'id' => $asset->getId(), - 'selectedIds' => implode(',', $selectedIds), - 'offset' => $i * $filesPerJob, - 'limit' => $filesPerJob, - 'jobId' => $jobId, - ], - ]]; - } - } - - return $this->adminJson([ - 'success' => true, - 'jobs' => $jobs, - 'jobId' => $jobId, - ]); - } - - #[Route('/download-as-zip-add-files', name: 'opendxp_admin_asset_downloadaszipaddfiles', methods: ['GET'])] - public function downloadAsZipAddFilesAction(Request $request): JsonResponse - { - $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/download-zip-' . $request->query->get('jobId') . '.zip'; - $asset = Asset::getById((int) $request->query->get('id')); - $success = false; - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if ($asset->isAllowed('view')) { - $zip = new ZipArchive(); - $zipState = is_file($zipFile) ? $zip->open($zipFile) : $zip->open($zipFile, ZipArchive::CREATE); - - if ($zipState === true) { - $parentPath = $asset->getRealFullPath(); - if ($asset->getId() === 1) { - $parentPath = ''; - } - - $db = \OpenDxp\Db::get(); - $conditionFilters = []; - - $selectedIds = $request->query->get('selectedIds', null); - - if (!empty($selectedIds)) { - $selectedIds = explode(',', $selectedIds); - $quotedSelectedIds = []; - foreach ($selectedIds as $selectedId) { - if ($selectedId) { - $quotedSelectedIds[] = $db->quote($selectedId); - } - } - //add a condition if id numbers are specified - $conditionFilters[] = 'id IN (' . implode(',', $quotedSelectedIds) . ')'; - } - $conditionFilters[] = "`type` != 'folder' AND `path` like " . $db->quote(Helper::escapeLike($parentPath) . '/%'); - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $userIds[] = $this->getAdminUser()->getId(); - $conditionFilters[] = ' ( - (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`, filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - OR - (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`, filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - )'; - } - - $condition = implode(' AND ', $conditionFilters); - - $assetList = new Asset\Listing(); - $assetList->setCondition($condition); - $assetList->setOrderKey('LENGTH(`path`) ASC, id ASC', false); - $assetList->setOffset((int)$request->query->get('offset')); - $assetList->setLimit((int)$request->query->get('limit')); - - foreach ($assetList as $a) { - if (!$a->isAllowed('view')) { - continue; - } - if ($a instanceof Asset\Folder) { - continue; - } - // add the file with the relative path to the parent directory - $zip->addFile($a->getLocalFile(), preg_replace('@^' . preg_quote($asset->getRealPath(), '@') . '@i', '', $a->getRealFullPath())); - } - - $zip->close(); - $success = true; - } - } - - return $this->adminJson([ - 'success' => $success, - ]); - } - - /** - * Download all assets contained in the folder with parameter id as ZIP file. - * The suggested filename is either [folder name].zip or assets.zip for the root folder. - */ - #[Route('/download-as-zip', name: 'opendxp_admin_asset_downloadaszip', methods: ['GET'])] - public function downloadAsZipAction(Request $request): BinaryFileResponse - { - $asset = Asset::getById((int) $request->query->get('id')); - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/download-zip-' . $request->query->get('jobId') . '.zip'; - $suggestedFilename = $asset->getFilename(); - if (empty($suggestedFilename)) { - $suggestedFilename = 'assets'; - } - - $response = new BinaryFileResponse($zipFile); - $response->headers->set('Content-Type', 'application/zip'); - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $suggestedFilename . '.zip'); - $response->deleteFileAfterSend(true); - - return $response; - } - - #[Route('/import-zip', name: 'opendxp_admin_asset_importzip', methods: ['POST'])] - public function importZipAction(Request $request, TranslatorInterface $translator): Response - { - $jobId = uniqid('', false); - $filesPerJob = 5; - $jobs = []; - $asset = Asset::getById((int) $request->query->get('parentId')); - - $filePath = null; - if ($request->files->has('Filedata')) { - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $filePath = $file->getPathname(); - } - - if ($filePath === null || !is_file($filePath)) { - return $this->adminJson([ - 'success' => false, - 'message' => 'Something went wrong, please check upload_max_filesize and post_max_size in your php.ini as well as the write permissions on the file system', - ]); - } - - if (!$asset) { - throw $this->createNotFoundException('Parent asset not found'); - } - - if (!$asset->isAllowed('create')) { - throw $this->createAccessDeniedException('not allowed to create'); - } - - $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $jobId . '.zip'; - - copy($filePath, $zipFile); - - $zip = new ZipArchive; - $retCode = $zip->open($zipFile); - if ($retCode === true) { - $jobAmount = ceil($zip->numFiles / $filesPerJob); - for ($i = 0; $i < $jobAmount; $i++) { - $jobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_asset_importzipfiles'), - 'method' => 'POST', - 'params' => [ - 'parentId' => $asset->getId(), - 'offset' => $i * $filesPerJob, - 'limit' => $filesPerJob, - 'jobId' => $jobId, - 'last' => (($i + 1) >= $jobAmount) ? 'true' : '', - 'allowOverwrite' => $request->query->get('allowOverwrite') ?: 'false', - ], - ]]; - } - $zip->close(); - - // here we have to use this method and not the JSON action helper ($this->_helper->json()) because this will add - // Content-Type: application/json which fires a download window in most browsers, because this is a normal POST - // request and not XHR where the content-type doesn't matter - $responseJson = $this->encodeJson([ - 'success' => true, - 'jobs' => $jobs, - 'jobId' => $jobId, - ]); - - return new Response($responseJson); - } - - return $this->adminJson([ - 'success' => false, - 'message' => $translator->trans('could_not_open_zip_file', [], 'admin'), - ]); - } - - #[Route('/import-zip-files', name: 'opendxp_admin_asset_importzipfiles', methods: ['POST'])] - public function importZipFilesAction(Request $request, Filesystem $filesystem): JsonResponse - { - $jobId = $request->request->get('jobId'); - $limit = (int)$request->request->get('limit'); - $offset = (int)$request->request->get('offset'); - $importAsset = Asset::getById((int) $request->request->get('parentId')); - $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $jobId . '.zip'; - $tmpDir = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/zip-import'; - - if (!is_dir($tmpDir)) { - $filesystem->mkdir($tmpDir); - } - - $zip = new ZipArchive; - if ($zip->open($zipFile) === true) { - for ($i = $offset; $i < ($offset + $limit); $i++) { - $path = $zip->getNameIndex($i); - if (str_starts_with($path, '__MACOSX/')) { - continue; - } - if (str_ends_with($path, '/Thumbs.db')) { - continue; - } - if (str_ends_with($path, '/.DS_Store')) { - continue; - } - - if ($path !== false && $zip->extractTo($tmpDir . '/', $path)) { - $tmpFile = $tmpDir . '/' . preg_replace('@^/@', '', $path); - $filename = Element\Service::getValidKey(basename($path), 'asset'); - $relativePath = ''; - if (dirname($path) !== '.') { - $relativePath = dirname($path); - } - $parentPath = $importAsset->getRealFullPath() . '/' . preg_replace('@^/@', '', $relativePath); - $parent = Asset\Service::createFolderByPath($parentPath); - // check for duplicate filename - if ($request->request->has('allowOverwrite') && $request->request->get('allowOverwrite') !== 'true') { - $filename = $this->getSafeFilename($parent->getRealFullPath(), $filename); - } - - if ($parent->isAllowed('create')) { - if ($request->request->has('allowOverwrite') && $request->request->get('allowOverwrite') === 'true' - && Asset\Service::pathExists($parent->getRealFullPath().'/'.$filename)) { - $asset = Asset::getByPath($parent->getRealFullPath().'/'.$filename); - $asset->setStream(fopen($tmpFile, 'rb', false, File::getContext())); - $asset->save(); - } else { - Asset::create($parent->getId(), [ - 'filename' => $filename, - 'sourcePath' => $tmpFile, - 'userOwner' => $this->getAdminUser()->getId(), - 'userModification' => $this->getAdminUser()->getId(), - ]); - } - - @unlink($tmpFile); - } else { - Logger::debug('prevented creating asset because of missing permissions'); - } - } - } - $zip->close(); - } - - if ($request->request->get('last')) { - unlink($zipFile); - } - - return $this->adminJson([ - 'success' => true, - ]); - } - - #[Route('/clear-thumbnail', name: 'opendxp_admin_asset_clearthumbnail', methods: ['POST'])] - public function clearThumbnailAction(Request $request): JsonResponse - { - $success = false; - - if ($asset = Asset::getById((int) $request->request->get('id'))) { - if (!$asset->isAllowed('publish')) { - throw $this->createAccessDeniedException('not allowed to publish'); - } - - $asset->clearThumbnails(true); // force clear - $asset->save(); - - $success = true; - } - - return $this->adminJson(['success' => $success]); - } - - #[Route('/grid-proxy', name: 'opendxp_admin_asset_gridproxy', methods: ['GET', 'POST', 'PUT'])] - public function gridProxyAction(Request $request, EventDispatcherInterface $eventDispatcher, GridHelperService $gridHelperService, CsrfProtectionHandler $csrfProtection): JsonResponse - { - $allParams = [...$request->request->all(), ...$request->query->all()]; - - $filterPrepareEvent = new GenericEvent($this, [ - 'requestParams' => $allParams, - ]); - $language = $request->query->get('language') !== 'default' ? $request->query->get('language') : null; - - $eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::ASSET_LIST_BEFORE_FILTER_PREPARE); - - $allParams = $filterPrepareEvent->getArgument('requestParams'); - - $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); - - if (isset($allParams['data']) && $allParams['data']) { - $csrfProtection->checkCsrfToken($request); - if ($allParams['xaction'] === 'update') { - try { - $data = $this->decodeJson($allParams['data']); - - $updateEvent = new GenericEvent($this, [ - 'data' => $data, - 'processed' => false, - ]); - - $eventDispatcher->dispatch($updateEvent, AdminEvents::ASSET_LIST_BEFORE_UPDATE); - - $processed = $updateEvent->getArgument('processed'); - - if ($processed) { - // update already processed by event handler - return $this->adminJson(['success' => true]); - } - - $data = $updateEvent->getArgument('data'); - - // save - $asset = Asset::getById((int) $data['id']); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$asset->isAllowed('publish')) { - throw $this->createAccessDeniedException("Permission denied. You don't have the rights to save this asset."); - } - - $metadata = $asset->getMetadata(null, null, false, true); - $dirty = false; - - unset($data['id']); - foreach ($data as $key => $value) { - $fieldDef = explode('~', $key); - $key = $fieldDef[0]; - if (isset($fieldDef[1])) { - $language = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); - } - - foreach ($metadata as &$em) { - if ($em['name'] == $key && $em['language'] == $language) { - try { - $dataImpl = $loader->build($em['type']); - $value = $dataImpl->getDataFromListfolderGrid($value, $em); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $em['type']); - } - - $em['data'] = $value; - $dirty = true; - - break; - } - } - - if (!$dirty) { - $defaulMetadata = ['title', 'alt', 'copyright']; - if (in_array($key, $defaulMetadata)) { - $newEm = [ - 'name' => $key, - 'language' => $language, - 'type' => 'input', - 'data' => $value, - ]; - - try { - $dataImpl = $loader->build($newEm['type']); - $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $newEm['type']); - } - - $metadata[] = $newEm; - - $dirty = true; - } else { - $predefined = Model\Metadata\Predefined::getByName($key); - if ($predefined && (empty($predefined->getTargetSubtype()) - || $predefined->getTargetSubtype() === $asset->getType())) { - $newEm = [ - 'name' => $key, - 'language' => $language, - 'type' => $predefined->getType(), - 'data' => $value, - ]; - - try { - $dataImpl = $loader->build($newEm['type']); - $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $newEm['type']); - } - - $metadata[] = $newEm; - $dirty = true; - } - } - } - } - - if ($dirty) { - $metadataEvent = new GenericEvent($this, [ - 'id' => $asset->getId(), - 'metadata' => $metadata, - ]); - $eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); - - // $metadata = Asset\Service::minimizeMetadata($metadata, "grid"); - $asset->setMetadataRaw($metadata); - $asset->save(); - - return $this->adminJson(['success' => true]); - } - - return $this->adminJson(['success' => false, 'message' => 'something went wrong.']); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - } else { - $list = $gridHelperService->prepareAssetListingForGrid($allParams, $this->getAdminUser()); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $list, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); - /** @var Asset\Listing $list */ - $list = $beforeListLoadEvent->getArgument('list'); - - $list->load(); - - $assets = []; - foreach ($list->getAssets() as $asset) { - // Like for treeGetChildrenByIdAction, so we respect isAllowed method which can be extended (object DI) for custom permissions, so relying only users_workspaces_asset is insufficient and could lead security breach - if ($asset->isAllowed('list')) { - $a = GridData\Asset::getData($asset, $allParams['fields'], $allParams['language'] ?? ''); - $assets[] = $a; - } - } - - $result = ['data' => $assets, 'success' => true, 'total' => $list->getTotalCount()]; - - $afterListLoadEvent = new GenericEvent($this, [ - 'list' => $result, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::ASSET_LIST_AFTER_LIST_LOAD); - $result = $afterListLoadEvent->getArgument('list'); - - return $this->adminJson($result); - } - - return $this->adminJson(['success' => false]); - } - - #[Route('/get-text', name: 'opendxp_admin_asset_gettext', methods: ['GET'])] - public function getTextAction(Request $request): JsonResponse - { - $asset = Asset::getById((int) $request->query->get('id')); - - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('not allowed to view'); - } - - $page = $request->query->get('page'); - $text = null; - if ($asset instanceof Asset\Document) { - $text = $asset->getText(empty($page) ? null : (int)$page); - } - - return $this->adminJson(['success' => 'true', 'text' => $text]); - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'assets', [ - 'getImageThumbnailAction', 'getVideoThumbnailAction', 'getDocumentThumbnailAction', - ]); - - $this->_assetService = new Asset\Service($this->getAdminUser()); - } - - /** - * @throws ValidationException - */ - private function validateManyToManyRelationAssetType(array $context, string $filename, string $sourcePath): void - { - if (isset($context['containerType'], $context['objectId'], $context['fieldname']) - && 'object' === $context['containerType'] - && $object = Concrete::getById($context['objectId']) - ) { - $fieldDefinition = $object->getClass()->getFieldDefinition($context['fieldname']); - if (!$fieldDefinition instanceof ManyToManyRelation) { - return; - } - - $mimeType = MimeTypes::getDefault()->guessMimeType($sourcePath); - $type = Asset::getTypeFromMimeMapping($mimeType, $filename); - - $allowedAssetTypes = $fieldDefinition->getAssetTypes(); - $allowedAssetTypes = array_column($allowedAssetTypes, 'assetTypes'); - - if ( - !( - $fieldDefinition->getAssetsAllowed() - && ($allowedAssetTypes === [] || in_array($type, $allowedAssetTypes, true)) - ) - ) { - throw new ValidationException(sprintf('Invalid relation in field `%s` [type: %s]', $context['fieldname'], $type)); - } - } + return $this->elementService->getElementTreeNodeConfig($element); } } diff --git a/src/Controller/Admin/Asset/AssetCopyController.php b/src/Controller/Admin/Asset/AssetCopyController.php new file mode 100644 index 00000000..ea998df0 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetCopyController.php @@ -0,0 +1,73 @@ +value)] +class AssetCopyController extends AdminAbstractController +{ + #[Route('/copy-info', name: 'opendxp_admin_asset_copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + Request $request, + ): JsonResponse { + $result = $handler($payload); + + Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { + $session->set((string) $result->transactionId, []); + }, 'opendxp_copy'); + + return $this->adminJson(['pastejobs' => $result->pasteJobs]); + } + + #[Route('/copy', name: 'opendxp_admin_asset_copy', methods: ['POST'])] + public function copyAction( + CopyAssetPayload $payload, + CopyAssetHandler $copyAsset, + Request $request, + ): JsonResponse { + $result = $copyAsset($payload); + + if ($result->newAsset !== null && $payload->saveParentId) { + $session = Tool\Session::getSessionBag($request->getSession(), 'opendxp_copy'); + $sessionBag = $session->get($payload->transactionId); + $sessionBag['parentId'] = $result->newAsset->getId(); + $session->set($payload->transactionId, $sessionBag); + } + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/Asset/AssetDownloadController.php b/src/Controller/Admin/Asset/AssetDownloadController.php new file mode 100644 index 00000000..f2edfda2 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetDownloadController.php @@ -0,0 +1,144 @@ +value)] +class AssetDownloadController extends AdminAbstractController +{ + #[Route('/download', name: 'opendxp_admin_asset_download', methods: ['GET'])] + public function downloadAction(DownloadAssetPayload $payload, DownloadAssetHandler $downloadAsset): StreamedResponse + { + $result = $downloadAsset($payload); + $asset = $result->asset; + $stream = $asset->getStream(); + + if (!is_resource($stream)) { + throw $this->createNotFoundException('Unable to get resource for asset ' . $asset->getId()); + } + + return new StreamedResponse(static function () use ($stream): void { + fpassthru($stream); + }, 200, [ + 'Content-Type' => $asset->getMimeType(), + 'Content-Disposition' => sprintf('attachment; filename: "%s"', $asset->getFilename()), + 'Content-Length' => $asset->getFileSize(), + ]); + } + + #[Route('/download-image-thumbnail', name: 'opendxp_admin_asset_downloadimagethumbnail', methods: ['GET'])] + public function downloadImageThumbnailAction( + DownloadImageThumbnailPayload $payload, + DownloadImageThumbnailHandler $downloadImageThumbnail, + ): BinaryFileResponse { + $result = $downloadImageThumbnail($payload); + + $downloadFilename = preg_replace( + '/\.' . preg_quote(pathinfo($result->image->getFilename(), PATHINFO_EXTENSION), '/') . '$/i', + '.' . $result->thumbnail->getFileExtension(), + $result->image->getFilename() + ); + + clearstatcache(); + + $response = new BinaryFileResponse($result->localFile); + $response->headers->set('Content-Type', $result->thumbnail->getMimeType()); + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $downloadFilename); + $this->addThumbnailCacheHeaders($response); + $response->deleteFileAfterSend($result->deleteThumbnail); + + return $response; + } + + #[Route('/download-as-zip-jobs', name: 'opendxp_admin_asset_downloadaszipjobs', methods: ['GET'])] + public function downloadAsZipJobsAction( + GetDownloadZipJobsPayload $payload, + GetDownloadZipJobsHandler $getZipJobs, + ): JsonResponse { + $result = $getZipJobs($payload); + + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs, 'jobId' => $result->jobId])); + } + + #[Route('/download-as-zip-add-files', name: 'opendxp_admin_asset_downloadaszipaddfiles', methods: ['GET'])] + public function downloadAsZipAddFilesAction( + AddFilesToZipPayload $payload, + AddFilesToZipHandler $addFilesToZip, + ): JsonResponse { + $addFilesToZip($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + /** + * Download all assets contained in the folder with parameter id as ZIP file. + * The suggested filename is either [folder name].zip or assets.zip for the root folder. + */ + #[Route('/download-as-zip', name: 'opendxp_admin_asset_downloadaszip', methods: ['GET'])] + public function downloadAsZipAction( + DownloadZipPayload $payload, + DownloadZipHandler $downloadZip, + ): BinaryFileResponse { + $result = $downloadZip($payload); + + $response = new BinaryFileResponse($result->zipFile); + $response->headers->set('Content-Type', 'application/zip'); + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $result->suggestedFilename . '.zip'); + $response->deleteFileAfterSend(true); + + return $response; + } + + private function addThumbnailCacheHeaders(Response $response): void + { + $lifetime = 300; + $date = new DateTime('now'); + $date->add(new DateInterval('PT' . $lifetime . 'S')); + + $response->setMaxAge($lifetime); + $response->setPublic(); + $response->setExpires($date); + $response->headers->set('Pragma', ''); + } +} diff --git a/src/Controller/Admin/Asset/AssetEditorController.php b/src/Controller/Admin/Asset/AssetEditorController.php new file mode 100644 index 00000000..d58f725f --- /dev/null +++ b/src/Controller/Admin/Asset/AssetEditorController.php @@ -0,0 +1,56 @@ +value)] +class AssetEditorController extends AdminAbstractController +{ + #[Route('/image-editor', name: 'opendxp_admin_asset_imageeditor', methods: ['GET'])] + public function imageEditorAction(LoadAssetForEditorPayload $payload, LoadAssetForEditorHandler $loadForEditor): Response + { + $result = $loadForEditor($payload); + + return $this->render('@OpenDxpAdmin/admin/asset/image_editor.html.twig', ['asset' => $result->asset]); + } + + #[Route('/image-editor-save', name: 'opendxp_admin_asset_imageeditorsave', methods: ['PUT'])] + public function imageEditorSaveAction( + SaveImageEditorPayload $payload, + SaveImageEditorHandler $saveImageEditor, + ): JsonResponse { + $saveImageEditor($payload); + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/Asset/AssetHelperController.php b/src/Controller/Admin/Asset/AssetHelperController.php index 019f3beb..6d511a91 100644 --- a/src/Controller/Admin/Asset/AssetHelperController.php +++ b/src/Controller/Admin/Asset/AssetHelperController.php @@ -16,41 +16,35 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\Asset; -use Doctrine\DBAL\ArrayParameterType; -use Doctrine\DBAL\ParameterType; -use Exception; -use League\Flysystem\FilesystemException; -use League\Flysystem\UnableToReadFile; -use OpenDxp; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Helper\GridHelperService; -use OpenDxp\Bundle\AdminBundle\Model\GridConfig; -use OpenDxp\Bundle\AdminBundle\Model\GridConfigFavourite; -use OpenDxp\Bundle\AdminBundle\Model\GridConfigShare; -use OpenDxp\Bundle\AdminBundle\Tool; -use OpenDxp\Db; -use OpenDxp\File; -use OpenDxp\Loader\ImplementationLoader\Exception\UnsupportedException; -use OpenDxp\Logger; -use OpenDxp\Model\Asset; -use OpenDxp\Model\Element; -use OpenDxp\Model\Metadata; -use OpenDxp\Model\User; -use OpenDxp\Security\SecurityHelper; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\DeleteGridColumnConfig\DeleteGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\DeleteGridColumnConfig\DeleteGridColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\DoAssetExport\DoAssetExportPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\DoAssetExport\DoAssetExportHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\ExecuteAssetBatch\ExecuteAssetBatchPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\ExecuteAssetBatch\ExecuteAssetBatchHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetAssetBatchJobs\GetAssetBatchJobsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetAssetBatchJobs\GetAssetBatchJobsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\PrepareHelperColumnConfigs\PrepareHelperColumnConfigsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\PrepareHelperColumnConfigs\PrepareHelperColumnConfigsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetAssetMetadataForColumnConfig\GetAssetMetadataForColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetExportJobs\GetExportJobsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetExportJobs\GetExportJobsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\MarkGridConfigFavourite\MarkGridConfigFavouritePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\MarkGridConfigFavourite\MarkGridConfigFavouriteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\SaveGridColumnConfig\SaveGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\SaveGridColumnConfig\SaveGridColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Service\Grid\AssetGridColumnConfigResolver; +use OpenDxp\Bundle\AdminBundle\Service\Grid\GridExportService; use OpenDxp\Tool\Session; -use OpenDxp\Tool\Storage; -use OpenDxp\Version; -use stdClass; -use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal @@ -58,1021 +52,162 @@ #[Route('/asset-helper')] class AssetHelperController extends AdminAbstractController { - public function __construct(protected EventDispatcherInterface $eventDispatcher) - { - } - - public function getMyOwnGridColumnConfigs(int $userId, string $classId, string $searchType): array - { - $db = Db::get(); - $configListingConditionParts = []; - $configListingConditionParts[] = 'ownerId = ' . $userId; - $configListingConditionParts[] = 'classId = ' . $db->quote($classId); - - if ($searchType) { - $configListingConditionParts[] = 'searchType = ' . $db->quote($searchType); - } - - $configCondition = implode(' AND ', $configListingConditionParts); - $configListing = new GridConfig\Listing(); - $configListing->setOrderKey('name'); - $configListing->setOrder('ASC'); - $configListing->setCondition($configCondition); - $configListing = $configListing->load(); - - $configData = []; - foreach ($configListing as $config) { - $configData[] = $config->getObjectVars(); - } - - return $configData; - } - - public function getSharedGridColumnConfigs(User $user, string $classId, ?string $searchType = null): array - { - $db = Db::get(); - - $configListing = []; - - $userIds = [$user->getId()]; - // collect all roles - $userIds = [...$userIds, ...$user->getRoles()]; - - $ids = $db->fetchFirstColumn( - 'SELECT DISTINCT c1.id FROM gridconfigs c1, gridconfig_shares s - WHERE (c1.searchType = ? AND c1.id = s.gridConfigId AND s.sharedWithUserId IN (?) AND c1.classId = ?) - UNION DISTINCT SELECT c2.id FROM gridconfigs c2 WHERE shareGlobally = 1 AND c2.classId = ? AND c2.ownerId != ?', - [$searchType, $userIds, $classId, $classId, $user->getId()], - [ParameterType::STRING, ArrayParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER] - ); - - if ($ids) { - $ids = implode(',', $ids); - $configListing = new GridConfig\Listing(); - $configListing->setOrderKey('name'); - $configListing->setOrder('ASC'); - $configListing->setCondition('id in (' . $ids . ')'); - $configListing = $configListing->load(); - } - - $configData = []; - foreach ($configListing as $config) { - $configData[] = $config->getObjectVars(); - } - - return $configData; + public function __construct( + private readonly AssetGridColumnConfigResolver $gridConfigResolver, + ) { } #[Route('/grid-delete-column-config', name: 'opendxp_admin_asset_assethelper_griddeletecolumnconfig', methods: ['DELETE'])] - public function gridDeleteColumnConfigAction(Request $request): JsonResponse - { + public function gridDeleteColumnConfigAction( + DeleteGridColumnConfigPayload $deletePayload, + DeleteGridColumnConfigHandler $deleteGridColumnConfig, + Request $request, + ): JsonResponse { $params = [ - 'id' => $request->request->get('id'), - 'type' => $request->request->get('type'), - 'types' => $request->request->get('types'), - 'gridConfigId' => $request->request->get('gridConfigId'), - 'searchType' => $request->request->get('searchType'), - 'noSystemColumns' => $request->query->getBoolean('no_system_columns'), + 'id' => $request->request->getString('id'), + 'type' => $request->request->getString('type'), + 'types' => $request->request->getString('types'), + 'gridConfigId' => $request->request->getString('gridConfigId'), + 'searchType' => $request->request->getString('searchType'), + 'noSystemColumns' => $deletePayload->noSystemColumns, ]; - $gridConfigId = (int) $request->request->get('gridConfigId'); - $gridConfig = GridConfig::getById($gridConfigId); - $success = false; - if ($gridConfig) { - if ($gridConfig->getOwnerId() !== $this->getAdminUser()->getId()) { - throw new Exception("don't mess with someone elses grid config"); - } + $deleteGridColumnConfig($deletePayload); - $gridConfig->delete(); - $success = true; - } + $resolverResult = $this->gridConfigResolver->resolve($params, true); - $newGridConfig = $this->doGetGridColumnConfig($params, true); - $newGridConfig['deleteSuccess'] = $success; - - return $this->adminJson($newGridConfig); + return $this->adminJson([...$resolverResult->jsonSerialize(), 'deleteSuccess' => true]); } #[Route('/grid-get-column-config', name: 'opendxp_admin_asset_assethelper_gridgetcolumnconfig', methods: ['GET'])] - public function gridGetColumnConfigAction(Request $request): JsonResponse - { + public function gridGetColumnConfigAction( + #[MapQueryParameter] ?string $id = null, + #[MapQueryParameter] ?string $type = null, + #[MapQueryParameter] ?string $types = null, + #[MapQueryParameter] ?string $gridConfigId = null, + #[MapQueryParameter] ?string $searchType = null, + #[MapQueryParameter(name: 'no_system_columns')] bool $noSystemColumns = false, + ): JsonResponse { $params = [ - 'id' => $request->query->get('id'), - 'type' => $request->query->get('type'), - 'types' => $request->query->get('types'), - 'gridConfigId' => $request->query->get('gridConfigId'), - 'searchType' => $request->query->get('searchType'), - 'noSystemColumns' => $request->query->getBoolean('no_system_columns'), + 'id' => $id, + 'type' => $type, + 'types' => $types, + 'gridConfigId' => $gridConfigId, + 'searchType' => $searchType, + 'noSystemColumns' => $noSystemColumns, ]; - $result = $this->doGetGridColumnConfig($params); - - return $this->adminJson($result); - } - - private function doGetGridColumnConfig(array $params, bool $isDelete = false): array - { - $gridConfigId = null; - - $classId = $params['id']; - $context = ['purpose' => 'gridconfig']; - - $types = []; - if (!empty($params['types'])) { - $types = explode(',', $params['types']); - } - - $userId = $this->getAdminUser()->getId(); - - $requestedGridConfigId = $isDelete ? '' : $params['gridConfigId'] ?? ''; - - // grid config - $gridConfig = []; - $searchType = $params['searchType']; - - if ((string) $requestedGridConfigId === '') { - // check if there is a favourite view - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $classId, 0, $searchType); - - if ($favourite) { - $requestedGridConfigId = $favourite->getGridConfigId(); - } - } - - if (is_numeric($requestedGridConfigId) && $requestedGridConfigId > 0) { - $db = Db::get(); - $savedGridConfig = GridConfig::getById((int) $requestedGridConfigId); - - if ($savedGridConfig) { - $shared = false; - if (!$this->getAdminUser()->isAdmin()) { - $userIds = [$this->getAdminUser()->getId()]; - $userIds = [...$userIds, ...$this->getAdminUser()->getRoles()]; - $isSharedGlobally = $savedGridConfig->getOwnerId() !== $userId && $savedGridConfig->isShareGlobally(); - - $isSharedWithUser = (bool) $db->fetchOne( - 'SELECT 1 FROM gridconfig_shares WHERE sharedWithUserId IN (?) AND gridConfigId = ?', - [$userIds, $savedGridConfig->getId()], - [ArrayParameterType::INTEGER, ParameterType::INTEGER] - ); - - $shared = $isSharedGlobally || $isSharedWithUser; - - if (!$shared && $savedGridConfig->getOwnerId() !== $this->getAdminUser()->getId()) { - throw new Exception('You are neither the owner of this config nor it is shared with you'); - } - } - - $gridConfigId = $savedGridConfig->getId(); - $gridConfig = $savedGridConfig->getConfig(); - $gridConfig = json_decode($gridConfig, true); - $gridConfigName = SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getName()); - $gridConfigDescription = SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getDescription()); - $sharedGlobally = $savedGridConfig->isShareGlobally(); - $setAsFavourite = $savedGridConfig->isSetAsFavourite(); - - foreach ($gridConfig['columns'] as &$column) { - if (array_key_exists('isOperator', $column) && $column['isOperator']) { - $colAttributes = &$column['fieldConfig']['attributes']; - SecurityHelper::convertHtmlSpecialCharsArrayKeys($colAttributes, ['label', 'attribute', 'param1']); - } - } - } - } - - $availableFields = []; - $language = ''; - - if (empty($gridConfig)) { - $availableFields = $this->getDefaultGridFields( - $params['noSystemColumns'], - [], //maybe required for types other than metadata - $context, - $types - ); - } else { - $savedColumns = $gridConfig['columns']; - - foreach ($savedColumns as $sc) { - if (!$sc['hidden']) { - $colConfig = $this->getFieldGridConfig($sc, $language); - if ($colConfig) { - $availableFields[] = $colConfig; - } - } - } - } - usort($availableFields, static fn ($a, $b) => $a['position'] <=> $b['position']); - - $availableConfigs = $classId ? $this->getMyOwnGridColumnConfigs($userId, $classId, $searchType) : []; - $sharedConfigs = $classId ? $this->getSharedGridColumnConfigs($this->getAdminUser(), $classId, $searchType) : []; - $settings = $this->getShareSettings((int)$gridConfigId); - $settings['gridConfigId'] = (int)$gridConfigId; - $settings['gridConfigName'] = $gridConfigName ?? null; - $settings['gridConfigDescription'] = $gridConfigDescription ?? null; - $settings['shareGlobally'] = $sharedGlobally ?? null; - $settings['setAsFavourite'] = $setAsFavourite ?? null; - $settings['isShared'] = !$gridConfigId || ($shared ?? null); - - $context = $gridConfig['context'] ?? null; - if ($context) { - $context = json_decode($context, true); - } - - return [ - 'sortinfo' => $gridConfig['sortinfo'] ?? false, - 'availableFields' => $availableFields, - 'settings' => $settings, - 'onlyDirectChildren' => $gridConfig['onlyDirectChildren'] ?? false, - 'onlyUnreferenced' => $gridConfig['onlyUnreferenced'] ?? false, - 'pageSize' => $gridConfig['pageSize'] ?? false, - 'availableConfigs' => $availableConfigs, - 'sharedConfigs' => $sharedConfigs, - 'context' => $context, - ]; - } - - protected function getFieldGridConfig(array $field, string $language = '', ?string $keyPrefix = null): ?array - { - $defaulMetadataFields = ['copyright', 'alt', 'title']; - $predefined = null; - - if (isset($field['fieldConfig']['layout']['name'])) { - $predefined = Metadata\Predefined::getByName($field['fieldConfig']['layout']['name']); - } - - $key = $field['name']; - if ($keyPrefix) { - $key = $keyPrefix . $key; - } - $fieldDef = explode('~', $field['name']); - $field['name'] = $fieldDef[0]; - - if (isset($fieldDef[1]) && $fieldDef[1] === 'system') { - $type = 'system'; - } elseif (in_array($fieldDef[0], $defaulMetadataFields)) { - $type = 'input'; - } else { - $type = $field['fieldConfig']['type']; - if (isset($fieldDef[1])) { - $field['fieldConfig']['label'] = $field['fieldConfig']['layout']['title'] = $fieldDef[0] . ' (' . $fieldDef[1] . ')'; - $field['fieldConfig']['layout']['icon'] = Tool::getLanguageFlagFile($fieldDef[1], true); - } - } - - $result = [ - 'key' => $key, - 'type' => $type, - 'label' => $field['fieldConfig']['label'] ?? $key, - 'width' => $field['width'], - 'position' => $field['position'], - 'language' => $field['fieldConfig']['language'] ?? null, - 'layout' => $field['fieldConfig']['layout'] ?? null, - ]; - - if (isset($field['locked'])) { - $result['locked'] = $field['locked']; - } - - if ($type === 'select' && $predefined) { - $field['fieldConfig']['layout']['config'] = $predefined->getConfig(); - $result['layout'] = $field['fieldConfig']['layout']; - } elseif (in_array($type, ['document', 'asset', 'object'], true)) { - $result['layout']['fieldtype'] = 'manyToOneRelation'; - $result['layout']['subtype'] = $type; - } - - $assetGetFieldGridConfig = new GenericEvent($this, [ - 'field' => $field, - 'result' => $result, - ]); - - $this->eventDispatcher->dispatch($assetGetFieldGridConfig, AdminEvents::ASSET_GET_FIELD_GRID_CONFIG); - - return $assetGetFieldGridConfig->getArgument('result'); - } - - public function getDefaultGridFields(bool $noSystemColumns, array $fields, array $context, array $types = []): array - { - $count = 0; - $availableFields = []; - - if (!$noSystemColumns) { - foreach (Asset\Service::GRID_SYSTEM_COLUMNS as $sc) { - if ($types === []) { - $availableFields[] = [ - 'key' => $sc . '~system', - 'type' => 'system', - 'label' => $sc, - 'position' => $count, ]; - $count++; - } - } - } - - return $availableFields; + return $this->adminJson($this->gridConfigResolver->resolve($params)); } #[Route('/prepare-helper-column-configs', name: 'opendxp_admin_asset_assethelper_preparehelpercolumnconfigs', methods: ['POST'])] - public function prepareHelperColumnConfigs(Request $request): JsonResponse - { - $helperColumns = []; - $newData = []; - $data = json_decode($request->request->get('columns')); - - /** @var stdClass $item */ - foreach ($data as $item) { - if (!empty($item->isOperator)) { - $itemKey = '#' . uniqid('', false); - - $item->key = $itemKey; - $newData[] = $item; - $helperColumns[$itemKey] = $item; - } else { - $newData[] = $item; - } - } - - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($helperColumns): void { + public function prepareHelperColumnConfigs( + PrepareHelperColumnConfigsPayload $payload, + PrepareHelperColumnConfigsHandler $prepareHelperColumnConfigs, + Request $request, + ): JsonResponse { + $result = $prepareHelperColumnConfigs($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { $existingColumns = $session->get('helpercolumns', []); - $helperColumns = [...$helperColumns, ...$existingColumns]; + $helperColumns = [...$result->helperColumns, ...$existingColumns]; $session->set('helpercolumns', $helperColumns); }, 'opendxp_gridconfig'); - return $this->adminJson(['success' => true, 'columns' => $newData]); + return $this->adminJson(ApiResponse::ok(['columns' => $result->newData])); } #[Route('/grid-mark-favourite-column-config', name: 'opendxp_admin_asset_assethelper_gridmarkfavouritecolumnconfig', methods: ['POST'])] - public function gridMarkFavouriteColumnConfigAction(Request $request): JsonResponse - { - $classId = $request->request->get('classId'); - $asset = Asset::getById(is_numeric($classId) ? (int) $classId : 0); - - if ($asset->isAllowed('list')) { - $gridConfigId = (int) $request->request->get('gridConfigId'); - $searchType = $request->request->get('searchType'); - $type = $request->request->get('type'); - $user = $this->getAdminUser(); - - $favourite = new GridConfigFavourite(); - $favourite->setOwnerId($user->getId()); - $favourite->setClassId($classId); - $favourite->setSearchType($searchType); - $favourite->setType($type); - - try { - if ($gridConfigId !== 0) { - $gridConfig = GridConfig::getById($gridConfigId); - $favourite->setGridConfigId($gridConfig->getId()); - } + public function gridMarkFavouriteColumnConfigAction( + MarkGridConfigFavouritePayload $payload, + MarkGridConfigFavouriteHandler $markFavourite, + ): JsonResponse { + $result = $markFavourite($payload); - $favourite->setObjectId(0); - $favourite->save(); - } catch (Exception) { - $favourite->delete(); - } - - return $this->adminJson(['success' => true, 'specializedConfigs' => false]); - } - - throw $this->createAccessDeniedHttpException(); - } - - protected function getShareSettings(int $gridConfigId): array - { - $result = [ - 'sharedUserIds' => [], - 'sharedRoleIds' => [], - ]; - - $db = Db::get(); - $allShares = $db->fetchAllAssociative( - 'SELECT s.sharedWithUserId, u.type FROM gridconfig_shares s, users u - WHERE s.sharedWithUserId = u.id AND s.gridConfigId = ?', - [$gridConfigId] - ); - - foreach ($allShares as $share) { - $type = $share['type']; - $key = 'shared' . ucfirst($type) . 'Ids'; - $result[$key][] = $share['sharedWithUserId']; - } - - foreach ($result as $idx => $value) { - $value = $value ? implode(',', $value) : ''; - $result[$idx] = $value; - } - - return $result; + return $this->adminJson(ApiResponse::ok(['specializedConfigs' => $result->specializedConfigs])); } #[Route('/grid-save-column-config', name: 'opendxp_admin_asset_assethelper_gridsavecolumnconfig', methods: ['POST'])] - public function gridSaveColumnConfigAction(Request $request): JsonResponse - { - $asset = Asset::getById((int) $request->request->get('id')); - - if (!$asset) { - throw $this->createNotFoundException(); - } - - if ($asset->isAllowed('list')) { - try { - $classId = $request->request->get('class_id'); - $context = $request->request->get('context'); - - $searchType = $request->request->get('searchType'); - $type = $request->request->get('type'); - - // grid config - $gridConfigData = $this->decodeJson($request->request->get('gridconfig')); - $gridConfigData['opendxp_version'] = Version::getVersion(); - $gridConfigData['opendxp_revision'] = Version::getRevision(); - $gridConfigData['context'] = $context; - unset($gridConfigData['settings']['isShared']); - - $metadata = $request->request->get('settings'); - $metadata = json_decode($metadata, true); - - $gridConfigId = $metadata['gridConfigId']; - $gridConfig = null; - if ($gridConfigId) { - $gridConfig = GridConfig::getById($gridConfigId); - } - - if ($gridConfig && $gridConfig->getOwnerId() !== $this->getAdminUser()->getId()) { - throw new Exception("don't mess around with somebody else's configuration"); - } - - $this->updateGridConfigShares($gridConfig, $metadata); - - if ($metadata['setAsFavourite'] && $this->getAdminUser()->isAdmin()) { - $this->updateGridConfigFavourites($gridConfig, $metadata); - } - - if (!$gridConfig) { - $gridConfig = new GridConfig(); - $gridConfig->setName(date('c')); - $gridConfig->setClassId($classId); - $gridConfig->setSearchType($searchType); - $gridConfig->setType($type); - - $gridConfig->setOwnerId($this->getAdminUser()->getId()); - } - - if ($metadata) { - $gridConfig->setName($metadata['gridConfigName']); - $gridConfig->setDescription($metadata['gridConfigDescription']); - $gridConfig->setShareGlobally($metadata['shareGlobally'] && $this->getAdminUser()->isAdmin()); - $gridConfig->setSetAsFavourite($metadata['setAsFavourite'] && $this->getAdminUser()->isAdmin()); - } - - $gridConfigData = json_encode($gridConfigData); - $gridConfig->setConfig($gridConfigData); - $gridConfig->save(); - - $userId = $this->getAdminUser()->getId(); + public function gridSaveColumnConfigAction( + SaveGridColumnConfigPayload $payload, + SaveGridColumnConfigHandler $saveGridColumnConfig, + ): JsonResponse { + $result = $saveGridColumnConfig($payload); - $availableConfigs = $this->getMyOwnGridColumnConfigs($userId, $classId, $searchType); - $sharedConfigs = $this->getSharedGridColumnConfigs($this->getAdminUser(), $classId, $searchType); - - $settings = $this->getShareSettings($gridConfig->getId()); - $settings['gridConfigId'] = (int)$gridConfig->getId(); - $settings['gridConfigName'] = $gridConfig->getName(); - $settings['gridConfigDescription'] = $gridConfig->getDescription(); - $settings['shareGlobally'] = $gridConfig->isShareGlobally(); - $settings['setAsFavourite'] = $gridConfig->isSetAsFavourite(); - $settings['isShared'] = $gridConfig->getOwnerId() !== $this->getAdminUser()->getId(); - - return $this->adminJson([ - 'success' => true, - 'settings' => $settings, - 'availableConfigs' => $availableConfigs, - 'sharedConfigs' => $sharedConfigs, - ]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - throw $this->createAccessDeniedHttpException(); - } - - /** - * @throws Exception - */ - protected function updateGridConfigShares(?GridConfig $gridConfig, array $metadata): void - { - $user = $this->getAdminUser(); - if (!$gridConfig || !$user->isAllowed('share_configurations')) { - // nothing to do - return; - } - - if ($gridConfig->getOwnerId() !== $this->getAdminUser()->getId()) { - throw new Exception("don't mess with someone elses grid config"); - } - $combinedShares = []; - $sharedUserIds = $metadata['sharedUserIds']; - $sharedRoleIds = $metadata['sharedRoleIds']; - - if ($sharedUserIds) { - $combinedShares = explode(',', $sharedUserIds); - } - - if ($sharedRoleIds) { - $sharedRoleIds = explode(',', $sharedRoleIds); - $combinedShares = [...$combinedShares, ...$sharedRoleIds]; - } - - $db = Db::get(); - $db->delete('gridconfig_shares', ['gridConfigId' => $gridConfig->getId()]); - - foreach ($combinedShares as $id) { - $share = new GridConfigShare(); - $share->setGridConfigId($gridConfig->getId()); - $share->setSharedWithUserId((int) $id); - $share->save(); - } - } - - /** - * @throws Exception - */ - protected function updateGridConfigFavourites(?GridConfig $gridConfig, array $metadata): void - { - $currentUser = $this->getAdminUser(); - - if (!$gridConfig || $currentUser === null || !$currentUser->isAllowed('share_configurations')) { - // nothing to do - return; - } - - if (!$currentUser->isAdmin() && (int) $gridConfig->getOwnerId() !== $currentUser->getId()) { - throw new Exception("don't mess with someone elses grid config"); - } - - $sharedUsers = []; - - if ($metadata['shareGlobally'] === false) { - $sharedUserIds = $metadata['sharedUserIds']; - - if ($sharedUserIds) { - $sharedUsers = array_map(intval(...), explode(',', $sharedUserIds)); - } - } - - if ($metadata['shareGlobally'] === true) { - $users = new User\Listing(); - $users->setCondition('id = ?', $currentUser->getId()); - - foreach ($users as $user) { - $sharedUsers[] = $user->getId(); - } - } - - foreach ($sharedUsers as $id) { - // Check if the user has already a favourite - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId( - $id, - $gridConfig->getClassId(), - 0, - $gridConfig->getSearchType() - ); - - if ($favourite instanceof GridConfigFavourite) { - $favouriteGridConfig = GridConfig::getById($favourite->getGridConfigId()); - - if ($favouriteGridConfig instanceof GridConfig) { - // Check if the grid config was shared globally if that is *not* the case we also not update - if ($favouriteGridConfig->isShareGlobally() === false) { - continue; - } - - // Check if the user is the owner. If that is the case we do not update the favourite - if ($favouriteGridConfig->getOwnerId() === $id) { - continue; - } - } - } - - $favourite = new GridConfigFavourite(); - $favourite->setGridConfigId($gridConfig->getId()); - $favourite->setClassId($gridConfig->getClassId()); - $favourite->setObjectId(0); - $favourite->setOwnerId($id); - $favourite->setType($gridConfig->getType()); - $favourite->setSearchType($gridConfig->getSearchType()); - $favourite->save(); - } + return $this->adminJson(ApiResponse::ok([ + 'settings' => $result->settings, + 'availableConfigs' => $result->availableConfigs, + 'sharedConfigs' => $result->sharedConfigs, + ])); } #[Route('/get-export-jobs', name: 'opendxp_admin_asset_assethelper_getexportjobs', methods: ['POST'])] - public function getExportJobsAction(Request $request, GridHelperService $gridHelperService): JsonResponse + public function getExportJobsAction(GetExportJobsPayload $payload, GetExportJobsHandler $getExportJobs): JsonResponse { - $allParams = [...$request->request->all(), ...$request->query->all()]; - $list = $gridHelperService->prepareAssetListingForGrid($allParams, $this->getAdminUser()); + $result = $getExportJobs($payload); - if (empty($ids = $allParams['ids'] ?? '')) { - $ids = $list->loadIdList(); - } - - $jobs = array_chunk($ids, 20); - - $fileHandle = uniqid('asset-export-', false); - $storage = Storage::get('temp'); - $storage->write($this->getCsvFile($fileHandle), ''); - - return $this->adminJson(['success' => true, 'jobs' => $jobs, 'fileHandle' => $fileHandle]); + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs, 'fileHandle' => $result->fileHandle])); } - /** - * @throws FilesystemException - */ #[Route('/do-export', name: 'opendxp_admin_asset_assethelper_doexport', methods: ['POST'])] - public function doExportAction(Request $request): JsonResponse - { - $fileHandle = File::getValidFilename($request->request->get('fileHandle')); - $ids = $request->request->all('ids'); - $settings = json_decode($request->request->get('settings'), true); - $delimiter = $settings['delimiter'] ?? ';'; - $language = str_replace('default', '', $request->request->get('language')); - $header = $settings['header'] ?? 'title'; - - $list = new Asset\Listing(); - - $quotedIds = []; - foreach ($ids as $id) { - $quotedIds[] = $list->quote($id); - } - - $list->setCondition('id IN (' . implode(',', $quotedIds) . ')'); - $list->setOrderKey(' FIELD(id, ' . implode(',', $quotedIds) . ')', false); - - $fields = json_decode($request->request->all('fields')[0], true); - - $addTitles = (bool) $request->request->get('initial'); - - $csv = $this->getCsvData($language, $list, $fields, $header, $addTitles); - - $temp = tmpfile(); - - try { - $storage = Storage::get('temp'); - $csvFile = $this->getCsvFile($fileHandle); - - $fileStream = $storage->readStream($csvFile); - - stream_copy_to_stream($fileStream, $temp, null, 0); - - $firstLine = true; - if ($request->request->get('initial') && $header === 'no_header') { - $firstLine = false; - } - - foreach ($csv as $line) { - if ($addTitles && $firstLine) { - $firstLine = false; - $line = implode($delimiter, $line) . "\r\n"; - fwrite($temp, $line); - } else { - fwrite($temp, implode($delimiter, array_map($this->encodeFunc(...), $line)) . "\r\n"); - } - } - $storage->writeStream($csvFile, $temp); - } catch (UnableToReadFile $exception) { - Logger::err($exception->getMessage()); - - return $this->adminJson( - [ - 'success' => false, - 'message' => sprintf('export file not found: %s', $fileHandle), - ] - ); - } finally { - if (is_resource($temp)) { - fclose($temp); - } - } - - return $this->adminJson(['success' => true]); - } - - public function encodeFunc(null|string|array $value): string + public function doExportAction(DoAssetExportPayload $payload, DoAssetExportHandler $doExport): JsonResponse { - if (is_array($value)) { - $value = implode(',', $value); - } - $value = str_replace('"', '""', $value ?? ''); + $doExport($payload); - //force wrap value in quotes and return - return '"' . $value . '"'; - } - - protected function getCsvData( - string $language, - Asset\Listing $list, - array $fields, - string $header, - bool $addTitles = true - ): array { - //create csv - $csv = []; - - $unsupportedFields = [0 => 'preview~system', 1 => 'size~system']; - $fields = array_filter($fields, fn ($field) => !in_array($field['key'], $unsupportedFields)); - - if ($addTitles && $header !== 'no_header') { - $columns = $fields; - $titleIdx = $header === 'name' ? 'key' : 'label'; - foreach ($columns as $columnIdx => $columnKeys) { - $columns[$columnIdx] = '"' . $columnKeys[$titleIdx] . '"'; - } - $csv[] = $columns; - } - - foreach ($list->load() as $asset) { - if ($fields) { - $dataRows = []; - foreach ($fields as $field) { - $fieldDef = explode('~', $field['key']); - $getter = 'get' . ucfirst($fieldDef[0]); - - if (isset($fieldDef[1])) { - if ($fieldDef[1] === 'system' && method_exists($asset, $getter)) { - $data = $asset->$getter($language); - } else { - $fieldDef[1] = str_replace('none', '', $fieldDef[1]); - $data = $asset->getMetadata($fieldDef[0], $fieldDef[1], true); - } - } else { - $data = $asset->getMetadata($field['key'], $language, true); - } - - if ($data instanceof Element\ElementInterface) { - $data = $data->getRealFullPath(); - } - $dataRows[] = $data; - } - $dataRows = Element\Service::escapeCsvRecord($dataRows); - $csv[] = $dataRows; - } - } - - return $csv; - } - - protected function getCsvFile(string $fileHandle): string - { - return $fileHandle . '.csv'; + return $this->adminJson(ApiResponse::ok()); } #[Route('/download-csv-file', name: 'opendxp_admin_asset_assethelper_downloadcsvfile', methods: ['GET'])] - public function downloadCsvFileAction(Request $request): Response - { - $storage = Storage::get('temp'); - $fileHandle = File::getValidFilename($request->query->get('fileHandle')); - $csvFile = $this->getCsvFile($fileHandle); - + public function downloadCsvFileAction( + GridExportService $gridExportService, + #[MapQueryParameter] ?string $fileHandle = null, + ): Response { try { - $csvData = $storage->read($csvFile); - $response = new Response($csvData); - $response->headers->set('Content-Type', 'application/csv'); - $disposition = HeaderUtils::makeDisposition( - HeaderUtils::DISPOSITION_ATTACHMENT, - 'export.csv' - ); - - $response->headers->set('Content-Disposition', $disposition); - $storage->delete($csvFile); - - return $response; - } catch (FilesystemException | UnableToReadFile) { - // handle the error + return $gridExportService->downloadCsvFile($fileHandle); + } catch (\RuntimeException) { throw $this->createNotFoundException('CSV file not found'); } } #[Route('/download-xlsx-file', name: 'opendxp_admin_asset_assethelper_downloadxlsxfile', methods: ['GET'])] - public function downloadXlsxFileAction(Request $request, GridHelperService $gridHelperService): BinaryFileResponse - { - $storage = Storage::get('temp'); - $fileHandle = File::getValidFilename($request->query->get('fileHandle')); - $csvFile = $this->getCsvFile($fileHandle); - + public function downloadXlsxFileAction( + GridExportService $gridExportService, + #[MapQueryParameter] ?string $fileHandle = null, + ): BinaryFileResponse { try { - return $gridHelperService->createXlsxExportFile($storage, $fileHandle, $csvFile); - } catch (Exception | FilesystemException | UnableToReadFile) { - // handle the error + return $gridExportService->downloadXlsxFile($fileHandle); + } catch (\RuntimeException) { throw $this->createNotFoundException('XLSX file not found'); } } #[Route('/get-metadata-for-column-config', name: 'opendxp_admin_asset_assethelper_getmetadataforcolumnconfig', methods: ['GET'])] - public function getMetadataForColumnConfigAction(Request $request): JsonResponse + public function getMetadataForColumnConfigAction(GetAssetMetadataForColumnConfigHandler $getMetadata): JsonResponse { - $result = []; - - //default metadata - $defaultMetadataNames = ['copyright', 'alt', 'title']; - foreach ($defaultMetadataNames as $defaultMetadata) { - $defaultColumns[] = ['title' => $defaultMetadata, 'name' => $defaultMetadata, 'datatype' => 'data', 'fieldtype' => 'input']; - } - $result['defaultColumns']['nodeLabel'] = 'default_metadata'; - $result['defaultColumns']['nodeType'] = 'image'; - $result['defaultColumns']['children'] = $defaultColumns; - - //predefined metadata - $list = Metadata\Predefined\Listing::getByTargetType('asset'); - $metadataItems = []; - $tmp = []; - foreach ($list as $item) { - //only allow unique metadata columns with subtypes - $uniqueKey = $item->getName().'_'.$item->getTargetSubtype(); - if (!in_array($uniqueKey, $tmp) && !in_array($item->getName(), $defaultMetadataNames)) { - $tmp[] = $uniqueKey; - $item->expand(); - $name = SecurityHelper::convertHtmlSpecialChars($item->getName()); - $metadataItems[] = [ - 'title' => $name, - 'name' => $name, - 'subtype' => $item->getTargetSubtype(), - 'datatype' => 'data', - 'fieldtype' => $item->getType(), - 'config' => $item->getConfig(), - ]; - } - } - - $result['metadataColumns']['children'] = $metadataItems; - $result['metadataColumns']['nodeLabel'] = 'predefined_metadata'; - $result['metadataColumns']['nodeType'] = 'metadata'; - - //system columns - $systemColumnNames = Asset\Service::GRID_SYSTEM_COLUMNS; - $systemColumns = []; - foreach ($systemColumnNames as $systemColumn) { - $systemColumns[] = ['title' => $systemColumn, 'name' => $systemColumn, 'datatype' => 'data', 'fieldtype' => 'system']; - } - $result['systemColumns']['nodeLabel'] = 'system_columns'; - $result['systemColumns']['nodeType'] = 'system'; - $result['systemColumns']['children'] = $systemColumns; + $result = $getMetadata(); - return $this->adminJson($result); + return $this->adminJson($result->data); } #[Route('/get-batch-jobs', name: 'opendxp_admin_asset_assethelper_getbatchjobs', methods: ['POST'])] - public function getBatchJobsAction(Request $request, GridHelperService $gridHelperService): JsonResponse + public function getBatchJobsAction(GetAssetBatchJobsPayload $payload, GetAssetBatchJobsHandler $getAssetBatchJobs, Request $request): JsonResponse { - if ($request->request->get('language')) { - $request->setLocale($request->request->get('language')); + if ($payload->language) { + $request->setLocale($payload->language); } - $allParams = [...$request->request->all(), ...$request->query->all()]; - $list = $gridHelperService->prepareAssetListingForGrid($allParams, $this->getAdminUser()); - - $jobs = $list->loadIdList(); + $result = $getAssetBatchJobs($payload); - return $this->adminJson(['success' => true, 'jobs' => $jobs]); + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs])); } #[Route('/batch', name: 'opendxp_admin_asset_assethelper_batch', methods: ['PUT'])] - public function batchAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function batchAction(ExecuteAssetBatchPayload $payload, ExecuteAssetBatchHandler $executeAssetBatch): JsonResponse { - try { - if ($request->request->has('data')) { - $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); - - $data = $this->decodeJson($request->request->get('data'), true); - - $updateEvent = new GenericEvent($this, [ - 'data' => $data, - 'processed' => false, - ]); - - $eventDispatcher->dispatch($updateEvent, AdminEvents::ASSET_LIST_BEFORE_BATCH_UPDATE); - - $processed = $updateEvent->getArgument('processed'); - - if ($processed) { - return $this->adminJson(['success' => true]); - } - - $language = null; - if (isset($data['language'])) { - $language = $data['language'] !== 'default' ? $data['language'] : null; - } - - $asset = Asset::getById((int) $data['job']); - - if ($asset) { - if (!$asset->isAllowed('publish')) { - throw new Exception("Permission denied. You don't have the rights to save this asset."); - } - - $metadata = $asset->getMetadata(null, null, false, true); - $dirty = false; - - $name = $data['name']; - $value = $data['value']; - - if ($data['valueType'] === 'object') { - $value = $this->decodeJson($value); - } - - $fieldDef = explode('~', $name); - $name = $fieldDef[0]; - if (count($fieldDef) > 1) { - $language = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); - } - - foreach ($metadata as &$em) { - if ($em['name'] == $name && $em['language'] == $language) { - try { - $dataImpl = $loader->build($em['type']); - $value = $dataImpl->getDataFromListfolderGrid($value, $em); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $em['type']); - } - $em['data'] = $value; - $dirty = true; - - break; - } - } - - if (!$dirty) { - $defaulMetadata = ['title', 'alt', 'copyright']; - if (in_array($name, $defaulMetadata)) { - $newEm = [ - 'name' => $name, - 'language' => $language, - 'type' => 'input', - 'data' => $value, - ]; - - try { - $dataImpl = $loader->build($newEm['type']); - $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $newEm['type']); - } - - $metadata[] = $newEm; - $dirty = true; - } else { - $predefined = Metadata\Predefined::getByName($name); - if ($predefined && (empty($predefined->getTargetSubtype()) - || $predefined->getTargetSubtype() === $asset->getType())) { - $newEm = [ - 'name' => $name, - 'language' => $language, - 'type' => $predefined->getType(), - 'data' => $value, - ]; - - try { - $dataImpl = $loader->build($newEm['type']); - $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); - } catch (UnsupportedException) { - Logger::error('could not resolve metadata implementation for ' . $newEm['type']); - } - - $metadata[] = $newEm; - - $dirty = true; - } - } - } - - try { - if ($dirty) { - $metadataEvent = new GenericEvent($this, [ - 'id' => $asset->getId(), - 'metadata' => $metadata, - ]); - - $eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); - - $asset->setMetadataRaw($metadata); - $asset->save(); - - return $this->adminJson(['success' => true]); - } - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - Logger::debug('AssetHelperController::batchAction => There is no asset left to update.'); - - return $this->adminJson(['success' => false, 'message' => 'AssetHelperController::batchAction => There is no asset left to update.']); - } - } - } catch (Exception $e) { - Logger::err((string)$e); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); + if ($payload->data !== null) { + $executeAssetBatch($payload); } - return $this->adminJson(['success' => false, 'message' => 'something went wrong.']); + return $this->adminJson(ApiResponse::ok()); } } diff --git a/src/Controller/Admin/Asset/AssetMediaController.php b/src/Controller/Admin/Asset/AssetMediaController.php new file mode 100644 index 00000000..fba9c7ff --- /dev/null +++ b/src/Controller/Admin/Asset/AssetMediaController.php @@ -0,0 +1,159 @@ +value)] +class AssetMediaController extends AdminAbstractController +{ + #[Route('/get-asset', name: 'opendxp_admin_asset_getasset', methods: ['GET'])] + public function getAssetAction(DownloadAssetPayload $payload, DownloadAssetHandler $downloadAsset): StreamedResponse + { + $result = $downloadAsset($payload); + $asset = $result->asset; + $stream = $asset->getStream(); + + if (!is_resource($stream)) { + throw $this->createNotFoundException('Unable to get resource for asset ' . $asset->getId()); + } + + $response = new StreamedResponse(static function () use ($stream): void { + fpassthru($stream); + }, 200, [ + 'Content-Type' => $asset->getMimeType(), + 'Access-Control-Allow-Origin' => '*', + ]); + $this->addThumbnailCacheHeaders($response); + + return $response; + } + + #[Route('/get-preview-document', name: 'opendxp_admin_asset_getpreviewdocument', methods: ['GET'])] + public function getPreviewDocumentAction(GetDocumentPreviewPayload $payload, GetDocumentPreviewHandler $getDocumentPreview): StreamedResponse|Response + { + $result = $getDocumentPreview($payload); + $asset = $result->asset; + + if ($result->thumbnailPath !== null) { + return $this->render('@OpenDxpAdmin/admin/asset/get_preview_pdf_open_in_new_tab.html.twig', [ + 'thumbnailPath' => $result->thumbnailPath, + 'assetPath' => $result->assetPath, + ]); + } + + if ($result->scanStatus === PdfScanStatus::IN_PROGRESS) { + return $this->render('@OpenDxpAdmin/admin/asset/get_preview_pdf_in_progress.html.twig'); + } + + if ($result->scanStatus === PdfScanStatus::UNSAFE) { + return $this->render('@OpenDxpAdmin/admin/asset/get_preview_pdf_unsafe.html.twig'); + } + + if ($result->stream) { + return new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); + }, 200, [ + 'Content-Type' => 'application/pdf', + ]); + } + + throw $this->createNotFoundException('Unable to get preview for asset ' . $asset->getId()); + } + + #[Route('/get-preview-video', name: 'opendxp_admin_asset_getpreviewvideo', methods: ['GET'])] + public function getPreviewVideoAction( + GetVideoPreviewPayload $payload, + GetVideoPreviewHandler $getVideoPreview, + ): Response { + $result = $getVideoPreview($payload); + $previewData = [ + 'asset' => $result->asset, + 'thumbnail' => $result->thumbnail, + 'config' => $result->configName, + ]; + + if ($result->thumbnail && $result->isFinished) { + return $this->render('@OpenDxpAdmin/admin/asset/get_preview_video_display.html.twig', $previewData); + } + + return $this->render('@OpenDxpAdmin/admin/asset/get_preview_video_error.html.twig', $previewData); + } + + #[Route('/serve-video-preview', name: 'opendxp_admin_asset_servevideopreview', methods: ['GET'])] + public function serveVideoPreviewAction( + ServeVideoPreviewPayload $payload, + ServeVideoPreviewHandler $serveVideoPreview, + ): StreamedResponse { + $result = $serveVideoPreview($payload); + + return new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); + }, 200, [ + 'Content-Type' => 'video/mp4', + 'Content-Length' => $result->fileSize, + 'Accept-Ranges' => 'bytes', + ]); + } + + #[Route('/get-text', name: 'opendxp_admin_asset_gettext', methods: ['GET'])] + public function getTextAction( + GetAssetTextPayload $payload, + GetAssetTextHandler $getAssetText, + ): JsonResponse { + $result = $getAssetText($payload); + + return $this->adminJson(ApiResponse::ok(['text' => $result->text])); + } + + private function addThumbnailCacheHeaders(Response $response): void + { + $lifetime = 300; + $date = new DateTime('now'); + $date->add(new DateInterval('PT' . $lifetime . 'S')); + + $response->setMaxAge($lifetime); + $response->setPublic(); + $response->setExpires($date); + $response->headers->set('Pragma', ''); + } +} diff --git a/src/Controller/Admin/Asset/AssetThumbnailController.php b/src/Controller/Admin/Asset/AssetThumbnailController.php new file mode 100644 index 00000000..a979324f --- /dev/null +++ b/src/Controller/Admin/Asset/AssetThumbnailController.php @@ -0,0 +1,166 @@ +returnLoadingGif) { + $response = new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/video-loading.gif'); + $response->headers->set('Cache-Control', 'no-store'); + + return $response; + } + + if ($result->thumbnailResult === null) { + throw $this->createNotFoundException(sprintf('Tree preview thumbnail not available for asset %s', $payload->id)); + } + + if ($result->returnFileinfo) { + return $this->adminJson([ + 'width' => $result->thumbnailResult->getWidth(), + 'height' => $result->thumbnailResult->getHeight(), + ]); + } + + $stream = $result->thumbnailResult->getStream(); + if (!$stream) { + return new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/filetype-not-supported.svg'); + } + + $response = new StreamedResponse(static function () use ($stream): void { + fpassthru($stream); + }, 200, [ + 'Content-Type' => $result->thumbnailResult->getMimeType(), + 'Access-Control-Allow-Origin' => '*', + ]); + $this->addThumbnailCacheHeaders($response); + + return $response; + } + + #[Route('/get-folder-thumbnail', name: 'opendxp_admin_asset_getfolderthumbnail', methods: ['GET'])] + #[IsGranted(CorePermission::Assets->value)] + public function getFolderThumbnailAction(GetFolderThumbnailPayload $payload, GetFolderThumbnailHandler $getFolderThumbnail): StreamedResponse + { + $result = $getFolderThumbnail($payload); + + $response = new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); + }, 200, [ + 'Content-Type' => 'image/jpg', + ]); + $this->addThumbnailCacheHeaders($response); + + return $response; + } + + #[Route('/get-video-thumbnail', name: 'opendxp_admin_asset_getvideothumbnail', methods: ['GET'])] + public function getVideoThumbnailAction( + GetVideoThumbnailPayload $payload, + GetVideoThumbnailHandler $getVideoThumbnail, + ): StreamedResponse { + $result = $getVideoThumbnail($payload); + + $response = new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); + }, 200, [ + 'Content-Type' => 'image/' . $result->fileExtension, + ]); + $this->addThumbnailCacheHeaders($response); + + return $response; + } + + #[Route('/get-document-thumbnail', name: 'opendxp_admin_asset_getdocumentthumbnail', methods: ['GET'])] + public function getDocumentThumbnailAction( + GetDocumentThumbnailPayload $payload, + GetDocumentThumbnailHandler $getDocumentThumbnail, + ): BinaryFileResponse|StreamedResponse { + $result = $getDocumentThumbnail($payload); + + if ($result->stream) { + $response = new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); + }, 200, [ + 'Content-Type' => 'image/' . $result->fileExtension, + ]); + } else { + $response = new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/filetype-not-supported.svg'); + } + + $this->addThumbnailCacheHeaders($response); + + return $response; + } + + #[Route('/get-folder-content-preview', name: 'opendxp_admin_asset_getfoldercontentpreview', methods: ['GET'])] + #[IsGranted(CorePermission::Assets->value)] + public function getFolderContentPreviewAction( + GetFolderContentPreviewPayload $payload, + GetFolderContentPreviewHandler $getFolderContentPreview, + ): JsonResponse { + $result = $getFolderContentPreview($payload); + + return $this->adminJson(ApiResponse::ok(['assets' => $result->assets, 'total' => $result->total])); + } + + private function addThumbnailCacheHeaders(Response $response): void + { + $lifetime = 300; + $date = new DateTime('now'); + $date->add(new DateInterval('PT' . $lifetime . 'S')); + + $response->setMaxAge($lifetime); + $response->setPublic(); + $response->setExpires($date); + $response->headers->set('Pragma', ''); + } +} diff --git a/src/Controller/Admin/Asset/AssetUploadController.php b/src/Controller/Admin/Asset/AssetUploadController.php new file mode 100644 index 00000000..ba233e6e --- /dev/null +++ b/src/Controller/Admin/Asset/AssetUploadController.php @@ -0,0 +1,127 @@ +value)] +class AssetUploadController extends AdminAbstractController +{ + public function __construct( + private readonly AssetUploadService $assetUploadService, + ) {} + + #[Route('/add-asset', name: 'opendxp_admin_asset_addasset', methods: ['POST'])] + public function addAssetAction(Request $request): JsonResponse + { + $res = $this->assetUploadService->addAsset($request); + + if ($res['success']) { + return $this->adminJson(ApiResponse::ok([ + 'asset' => [ + 'id' => $res['asset']->getId(), + 'path' => $res['asset']->getFullPath(), + 'type' => $res['asset']->getType(), + ], + ])); + } + + throw new BadRequestHttpException(); + } + + #[Route('/add-asset-compatibility', name: 'opendxp_admin_asset_addassetcompatibility', methods: ['POST'])] + public function addAssetCompatibilityAction(Request $request): JsonResponse + { + $res = $this->assetUploadService->addAsset($request); + + $response = $this->adminJson(ApiResponse::fromBool($res['success'], [ + 'msg' => $res['success'] ? 'Success' : 'Error', + 'id' => $res['asset'] ? $res['asset']->getId() : null, + 'fullpath' => $res['asset'] ? $res['asset']->getRealFullPath() : null, + 'type' => $res['asset'] ? $res['asset']->getType() : null, + ])); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/exists', name: 'opendxp_admin_asset_exists', methods: ['GET'])] + public function existsAction( + CheckAssetExistsPayload $payload, + CheckAssetExistsHandler $checkAssetExists, + ): JsonResponse { + return new JsonResponse([ + 'exists' => $checkAssetExists($payload), + ]); + } + + #[Route('/replace-asset', name: 'opendxp_admin_asset_replaceasset', methods: ['POST', 'PUT'])] + public function replaceAssetAction(ReplaceAssetPayload $payload, ReplaceAssetHandler $replaceAsset): JsonResponse + { + $asset = $replaceAsset($payload); + + $response = $this->adminJson(ApiResponse::ok(['id' => $asset->getId(), 'path' => $asset->getRealFullPath()])); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/import-zip', name: 'opendxp_admin_asset_importzip', methods: ['POST'])] + public function importZipAction( + ImportZipPayload $payload, + ImportZipHandler $importZip, + Request $request, + ): Response { + if (!$request->files->has('Filedata')) { + throw new BadRequestHttpException('Something went wrong, please check upload_max_filesize and post_max_size in your php.ini as well as the write permissions on the file system'); + } + + $importResult = $importZip($payload); + + return new Response($this->encodeJson(ApiResponse::ok(['jobs' => $importResult->jobs, 'jobId' => $importResult->jobId]))); + } + + #[Route('/import-zip-files', name: 'opendxp_admin_asset_importzipfiles', methods: ['POST'])] + public function importZipFilesAction(ImportZipFilesPayload $payload, ImportZipFilesHandler $importZipFiles): JsonResponse + { + $importZipFiles($payload); + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/Asset/AssetVersionController.php b/src/Controller/Admin/Asset/AssetVersionController.php new file mode 100644 index 00000000..20bac3be --- /dev/null +++ b/src/Controller/Admin/Asset/AssetVersionController.php @@ -0,0 +1,92 @@ +value)] +class AssetVersionController extends AdminAbstractController +{ + #[Route('/publish-version', name: 'opendxp_admin_asset_publishversion', methods: ['POST'])] + public function publishVersionAction( + PublishVersionPayload $payload, + PublishVersionHandler $publishVersion, + ElementServiceInterface $elementService, + ): JsonResponse { + + $result = $publishVersion($payload); + + return $this->adminJson(ApiResponse::ok([ + 'treeData' => $elementService->getElementTreeNodeConfig($result->asset), + ])); + } + + #[Route('/show-version', name: 'opendxp_admin_asset_showversion', methods: ['GET'])] + public function showVersionAction( + Environment $twig, + ShowVersionPayload $payload, + ShowVersionHandler $showVersion, + ): Response { + $result = $showVersion($payload); + + if ($result->isPdf) { + return $this->render( + '@OpenDxpAdmin/admin/asset/get_preview_pdf_open_in_new_tab.html.twig', + [ + 'thumbnailPath' => '', + 'assetPath' => $result->pdfPath + ], + ); + } + + Tool\UserTimezone::setUserTimezone($payload->userTimezone); + if ($timezone = Tool\UserTimezone::getUserTimezone()) { + $twig->getExtension(CoreExtension::class)->setTimezone($timezone); + } + + $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); + + return $this->render( + '@OpenDxpAdmin/admin/asset/show_version_' . strtolower($result->asset->getType()) . '.html.twig', + [ + 'asset' => $result->asset, + 'version' => $result->version, + 'loader' => $loader, + ], + ); + } +} diff --git a/src/Controller/Admin/DataObject/ClassController.php b/src/Controller/Admin/DataObject/ClassController.php index c73ae76a..ab7ec09a 100644 --- a/src/Controller/Admin/DataObject/ClassController.php +++ b/src/Controller/Admin/DataObject/ClassController.php @@ -16,491 +16,131 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Db; -use OpenDxp\Helper\FileSystemHelper; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\AddClass\AddClassHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\AddClass\AddClassPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\BulkCommit\BulkCommitHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\BulkCommit\BulkCommitPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\BulkExportPrepare\BulkExportPreparePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\BulkImport\BulkImportHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\BulkImport\BulkImportPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\DeleteClass\DeleteClassHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\DeleteClass\DeleteClassPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\DeleteSelectOptions\DeleteSelectOptionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\DeleteSelectOptions\DeleteSelectOptionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\DoBulkExport\DoBulkExportHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\ExportClass\ExportClassHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\ExportClass\ExportClassPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetAssetTypes\GetAssetTypesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassBulkExportList\GetClassBulkExportListHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassDefinitionForColumnConfig\GetClassDefinitionForColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassDefinitionForColumnConfig\GetClassDefinitionForColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClass\GetClassHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassIcons\GetClassIconsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassIcons\GetClassIconsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClass\GetClassPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassTree\GetClassTreeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetClassTree\GetClassTreePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetDocumentTypes\GetDocumentTypesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptions\GetSelectOptionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptions\GetSelectOptionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptionsTree\GetSelectOptionsTreeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptionsTree\GetSelectOptionsTreePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptionsUsages\GetSelectOptionsUsagesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetSelectOptionsUsages\GetSelectOptionsUsagesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetTextLayoutPreview\GetTextLayoutPreviewHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetTextLayoutPreview\GetTextLayoutPreviewPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\GetVideoAllowedTypes\GetVideoAllowedTypesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\ImportClass\ImportClassHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\ImportClass\ImportClassPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\SaveClassDefinition\SaveClassDefinitionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\SaveClassDefinition\SaveClassDefinitionPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\SaveSelectOptions\SaveSelectOptionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\SaveSelectOptions\SaveSelectOptionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ClassDef\SuggestClassIdentifier\SuggestClassIdentifierHandler; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use OpenDxp\Logger; -use OpenDxp\Model\Asset; -use OpenDxp\Model\DataObject; -use OpenDxp\Model\Document; -use OpenDxp\Model\Exception\ConfigWriteException; -use OpenDxp\Model\Translation; use OpenDxp\Tool\Session; -use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/class', name: 'opendxp_admin_dataobject_class_')] -class ClassController extends AdminAbstractController implements KernelControllerEventInterface +class ClassController extends AdminAbstractController { + #[IsGranted(CorePermission::Classes->value)] #[Route('/get-document-types', name: 'getdocumenttypes', methods: ['GET'])] - public function getDocumentTypesAction(Request $request): JsonResponse + public function getDocumentTypesAction(GetDocumentTypesHandler $handler): JsonResponse { - $documentTypes = Document::getTypes(); - $typeItems = []; - foreach ($documentTypes as $documentType) { - $typeItems[] = [ - 'text' => $documentType, - ]; - } - - return $this->adminJson($typeItems); + return $this->adminJson($handler()->types); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/get-asset-types', name: 'getassettypes', methods: ['GET'])] - public function getAssetTypesAction(Request $request): JsonResponse + public function getAssetTypesAction(GetAssetTypesHandler $handler): JsonResponse { - $assetTypes = Asset::getTypes(); - $typeItems = []; - foreach ($assetTypes as $assetType) { - $typeItems[] = [ - 'text' => $assetType, - ]; - } - - return $this->adminJson($typeItems); + return $this->adminJson($handler()->types); } #[Route('/get-tree', name: 'gettree', methods: ['GET', 'POST'])] - public function getTreeAction(Request $request): JsonResponse + public function getTreeAction(GetClassTreePayload $payload, GetClassTreeHandler $handler): JsonResponse { try { - // we need to check objects permission for listing in opendxp.model.objecttypes ext model $this->checkPermission('objects'); } catch (AccessDeniedHttpException) { Logger::log('[Startup] Object types are not loaded as "objects" permission is missing'); - //return empty string to avoid error on startup return $this->adminJson([]); } - $defaultIcon = '/bundles/opendxpadmin/img/flat-color-icons/class.svg'; - - $classesList = new DataObject\ClassDefinition\Listing(); - $classesList->setOrderKey('name'); - $classesList->setOrder('asc'); - $classes = $classesList->load(); - - // filter classes - if ($request->query->get('createAllowed')) { - $tmpClasses = []; - foreach ($classes as $class) { - if ($this->getAdminUser()->isAllowed($class->getId(), 'class')) { - $tmpClasses[] = $class; - } - } - $classes = $tmpClasses; - } - - $withId = $request->query->get('withId'); - $useTitle = $request->query->get('useTitle'); - $getClassConfig = static function ($class) use ($defaultIcon, $withId, $useTitle) { - $text = $class->getName(); - if ($useTitle) { - $text = $class->getTitle() ?: $class->getName(); - } - if ($withId) { - $text .= ' (' . $class->getId() . ')'; - } - - $hasBrickField = false; - foreach ($class->getFieldDefinitions() as $fieldDefinition) { - if ($fieldDefinition instanceof DataObject\ClassDefinition\Data\Objectbricks) { - $hasBrickField = true; - - break; - } - } - - return [ - 'id' => $class->getId(), - 'text' => $text, - 'leaf' => true, - 'icon' => $class->getIcon() ? htmlspecialchars($class->getIcon()) : $defaultIcon, - 'cls' => 'opendxp_class_icon', - 'propertyVisibility' => $class->getPropertyVisibility(), - 'enableGridLocking' => $class->isEnableGridLocking(), - 'hasBrickField' => $hasBrickField, - ]; - }; - - // build groups - $groups = []; - foreach ($classes as $class) { - $groupName = null; - - if ($class->getGroup()) { - $type = 'manual'; - $groupName = $class->getGroup(); - } else { - $type = 'auto'; - if (preg_match('@^([A-Za-z])([^A-Z]+)@', $class->getName(), $matches)) { - $groupName = $matches[0]; - } - - if (!$groupName) { - // this is eg. the case when class name uses only capital letters - $groupName = $class->getName(); - } - } - - $groupName = Translation::getByKeyLocalized($groupName, Translation::DOMAIN_ADMIN, true, true); - - if (!isset($groups[$groupName])) { - $groups[$groupName] = [ - 'classes' => [], - 'type' => $type, - ]; - } - $groups[$groupName]['classes'][] = $class; - } - - $treeNodes = []; - if ($groups !== []) { - $types = array_column($groups, 'type'); - array_multisort($types, SORT_ASC, array_keys($groups), SORT_ASC, $groups); - } - - if (!$request->query->get('grouped')) { - // list output - foreach ($groups as $groupName => $groupData) { - foreach ($groupData['classes'] as $class) { - $node = $getClassConfig($class); - if (count($groupData['classes']) > 1 || $groupData['type'] === 'manual') { - $node['group'] = $groupName; - } - $treeNodes[] = $node; - } - } - } else { - // create json output - foreach ($groups as $groupName => $groupData) { - if (count($groupData['classes']) === 1 && $groupData['type'] === 'auto') { - // no group, only one child - $node = $getClassConfig($groupData['classes'][0]); - } else { - // group classes - $node = [ - 'id' => 'folder_' . $groupName, - 'text' => $groupName, - 'leaf' => false, - 'expandable' => true, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'children' => [], - ]; - - foreach ($groupData['classes'] as $class) { - $node['children'][] = $getClassConfig($class); - } - } - - $treeNodes[] = $node; - } - } - - return $this->adminJson($treeNodes); + return $this->adminJson($handler($payload)->nodes); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/get', name: 'get', methods: ['GET'])] - public function getAction(Request $request): JsonResponse + public function getAction(GetClassPayload $payload, GetClassHandler $handler): JsonResponse { - $class = DataObject\ClassDefinition::getById($request->query->get('id')); - if (!$class) { - throw $this->createNotFoundException(); - } - $class->setFieldDefinitions([]); - $isWriteable = $class->isWritable(); - $class = $class->getObjectVars(); - $class['isWriteable'] = $isWriteable; - - return $this->adminJson($class); - } - - #[Route('/get-custom-layout', name: 'getcustomlayout', methods: ['GET'])] - public function getCustomLayoutAction(Request $request): JsonResponse - { - $customLayout = DataObject\ClassDefinition\CustomLayout::getById($request->query->get('id')); - if (!$customLayout) { - $brickLayoutSeparator = strpos($request->query->get('id'), '.brick.'); - if ($brickLayoutSeparator !== false) { - $customLayout = DataObject\ClassDefinition\CustomLayout::getById(substr($request->query->get('id'), 0, $brickLayoutSeparator)); - if ($customLayout instanceof DataObject\ClassDefinition\CustomLayout) { - $customLayout = DataObject\ClassDefinition\CustomLayout::create( - [ - 'name' => $customLayout->getName().' '.substr($request->query->get('id'), $brickLayoutSeparator+strlen('.brick.')), - 'userOwner' => $this->getAdminUser()->getId(), - 'classId' => $customLayout->getClassId(), - ] - ); - - $customLayout->setId($request->query->get('id')); - if (!$customLayout->isWriteable()) { - throw new ConfigWriteException(); - } - $customLayout->save(); - } - } - - if (!$customLayout) { - throw $this->createNotFoundException(); - } - } - $isWriteable = $customLayout->isWriteable(); - $customLayout = $customLayout->getObjectVars(); - $customLayout['isWriteable'] = $isWriteable; - - return $this->adminJson(['success' => true, 'data' => $customLayout]); + return $this->adminJson($handler($payload)->classData); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/add', name: 'add', methods: ['POST'])] - public function addAction(Request $request): JsonResponse + public function addAction(AddClassPayload $payload, AddClassHandler $handler): JsonResponse { - $className = $request->request->get('className'); - $className = $this->correctClassname($className); - - $classId = $request->request->get('classIdentifier'); - $existingClass = DataObject\ClassDefinition::getById($classId); - if ($existingClass) { - throw new Exception('Class identifier already exists'); - } - - $class = DataObject\ClassDefinition::create( - ['name' => $className, - 'userOwner' => $this->getAdminUser()->getId(), ] - ); - - $class->setId($classId); - - $class->save(true); - - return $this->adminJson(['success' => true, 'id' => $class->getId()]); - } - - #[Route('/add-custom-layout', name: 'addcustomlayout', methods: ['POST'])] - public function addCustomLayoutAction(Request $request): JsonResponse - { - $layoutId = $request->request->get('layoutIdentifier'); - $existingLayout = DataObject\ClassDefinition\CustomLayout::getById($layoutId); - - if ($existingLayout) { - throw new Exception('Custom Layout identifier already exists'); - } - - $customLayout = DataObject\ClassDefinition\CustomLayout::create( - [ - 'name' => $request->request->get('layoutName'), - 'userOwner' => $this->getAdminUser()->getId(), - 'classId' => $request->request->get('classId'), - ] - ); - - $customLayout->setId($layoutId); - if (!$customLayout->isWriteable()) { - throw new ConfigWriteException(); - } - - $customLayout->save(); - - $isWriteable = $customLayout->isWriteable(); - $data = $customLayout->getObjectVars(); - $data['isWriteable'] = $isWriteable; - - return $this->adminJson([ - 'success' => true, - 'id' => $customLayout->getId(), - 'name' => $customLayout->getName(), - 'data' => $data, - ]); + return $this->adminJson(ApiResponse::ok(['id' => $handler($payload)->id])); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/delete', name: 'delete', methods: ['DELETE'])] - public function deleteAction(Request $request): Response + public function deleteAction(DeleteClassPayload $payload, DeleteClassHandler $handler): Response { - $class = DataObject\ClassDefinition::getById($request->request->get('id')); - if ($class) { - $class->delete(); - } + $handler($payload); return new Response(); } - #[Route('/delete-custom-layout', name: 'deletecustomlayout', methods: ['DELETE'])] - public function deleteCustomLayoutAction(Request $request): JsonResponse - { - $customLayouts = new DataObject\ClassDefinition\CustomLayout\Listing(); - $id = $request->request->get('id'); - $customLayouts->setFilter(function (DataObject\ClassDefinition\CustomLayout $layout) use ($id) { - $currentLayoutId = $layout->getId(); - - return $currentLayoutId === $id || str_starts_with($currentLayoutId, $id . '.brick.'); - }); - - foreach ($customLayouts->getLayoutDefinitions() as $customLayout) { - $customLayout->delete(); - } - - return $this->adminJson(['success' => true]); - } - - #[Route('/save-custom-layout', name: 'savecustomlayout', methods: ['PUT'])] - public function saveCustomLayoutAction(Request $request): JsonResponse - { - $customLayout = DataObject\ClassDefinition\CustomLayout::getById($request->request->get('id')); - if (!$customLayout) { - throw $this->createNotFoundException(); - } - - $configuration = $this->decodeJson($request->request->get('configuration')); - $values = $this->decodeJson($request->request->get('values')); - - $modificationDate = (int)$values['modificationDate']; - if ($modificationDate < $customLayout->getModificationDate()) { - return $this->adminJson(['success' => false, 'msg' => 'custom_layout_changed']); - } - - $configuration['datatype'] = 'layout'; - $configuration['fieldtype'] = 'panel'; - $configuration['name'] = 'opendxp_root'; - - try { - $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); - $customLayout->setLayoutDefinitions($layout); - $customLayout->setName($values['name']); - $customLayout->setDescription($values['description']); - $customLayout->setDefault($values['default']); - if (!$customLayout->isWriteable()) { - throw new ConfigWriteException(); - } - $customLayout->save(); - - return $this->adminJson(['success' => true, 'id' => $customLayout->getId(), 'data' => $customLayout->getObjectVars()]); - } catch (Exception $e) { - Logger::error($e->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classes->value)] #[Route('/save', name: 'save', methods: ['PUT'])] - public function saveAction(Request $request): JsonResponse - { - $class = DataObject\ClassDefinition::getById($request->request->get('id')); - if (!$class) { - throw $this->createNotFoundException(); - } - - $configuration = $this->decodeJson($request->request->get('configuration')); - $values = $this->decodeJson($request->request->get('values')); - - // check if the class was changed during editing in the frontend - if ($class->getModificationDate() != $values['modificationDate']) { - throw new Exception('The class was modified during editing, please reload the class and make your changes again'); - } - - if ($values['name'] != $class->getName()) { - $classByName = DataObject\ClassDefinition::getByName($values['name']); - if ($classByName && $classByName->getId() !== $class->getId()) { - throw new Exception('Class name already exists'); - } - - $values['name'] = $this->correctClassname($values['name']); - $class->rename($values['name']); - } - - if ($values['compositeIndices']) { - foreach ($values['compositeIndices'] as $index => $compositeIndex) { - if ($compositeIndex['index_key'] !== ($sanitizedKey = preg_replace('/[^a-za-z0-9_\-+]/', '', $compositeIndex['index_key']))) { - $values['compositeIndices'][$index]['index_key'] = $sanitizedKey; - } - } - } - - unset($values['creationDate'], $values['userOwner'], $values['layoutDefinitions'], $values['fieldDefinitions']); - - $configuration['datatype'] = 'layout'; - $configuration['fieldtype'] = 'panel'; - $configuration['name'] = 'opendxp_root'; - - $class->setValues($values); - - try { - $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); - - $class->setLayoutDefinitions($layout); - - $class->setUserModification($this->getAdminUser()->getId()); - $class->setModificationDate(time()); - - $propertyVisibility = []; - foreach ($values as $key => $value) { - if (false !== stripos($key, 'propertyVisibility')) { - if (preg_match("/\.grid\./i", $key)) { - $propertyVisibility['grid'][preg_replace("/propertyVisibility\.grid\./i", '', $key)] = (bool) $value; - } elseif (preg_match("/\.search\./i", $key)) { - $propertyVisibility['search'][preg_replace("/propertyVisibility\.search\./i", '', $key)] = (bool) $value; - } - } - } - if (!empty($propertyVisibility)) { - $class->setPropertyVisibility($propertyVisibility); - } - - $class->save(); - - // set the fielddefinitions to [] because we don't need them in the response - $class->setFieldDefinitions([]); - - return $this->adminJson(['success' => true, 'class' => $class]); - } catch (Exception $e) { - Logger::error($e->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - protected function correctClassname(string $name): string + public function saveAction(SaveClassDefinitionPayload $payload, SaveClassDefinitionHandler $handler): JsonResponse { - $name = preg_replace('/[^a-zA-Z0-9_]+/', '', $name); - - return preg_replace('/^\d+/', '', $name); + return $this->adminJson(ApiResponse::ok(['class' => $handler($payload)->class])); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/import-class', name: 'importclass', methods: ['POST', 'PUT'])] - public function importClassAction(Request $request): Response + public function importClassAction(ImportClassPayload $payload, ImportClassHandler $handler): Response { - $class = DataObject\ClassDefinition::getById($request->query->get('id')); - if (!$class) { - throw $this->createNotFoundException(); - } + $handler($payload); - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $json = file_get_contents($file->getPathname()); - - $success = DataObject\ClassDefinition\Service::importClassDefinitionFromJson($class, $json, false, true); - - $response = $this->adminJson([ - 'success' => $success, - ]); + $response = $this->adminJson(ApiResponse::ok()); // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in // Ext.form.Action.Submit and mark the submission as failed @@ -509,906 +149,41 @@ public function importClassAction(Request $request): Response return $response; } - #[Route('/import-custom-layout-definition', name: 'importcustomlayoutdefinition', methods: ['POST', 'PUT'])] - public function importCustomLayoutDefinitionAction(Request $request): Response - { - $success = false; - $responseContent = []; - - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $json = file_get_contents($file->getPathname()); - - $importData = $this->decodeJson($json); - - $existingLayout = null; - if (isset($importData['name'])) { - $existingLayout = DataObject\ClassDefinition\CustomLayout::getByName($importData['name']); - - if ($existingLayout instanceof DataObject\ClassDefinition\CustomLayout) { - $responseContent['nameAlreadyInUse'] = true; - } - } - - if (!$existingLayout instanceof DataObject\ClassDefinition\CustomLayout) { - $customLayoutId = $request->query->get('id'); - $customLayout = DataObject\ClassDefinition\CustomLayout::getById($customLayoutId); - if ($customLayout) { - try { - $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($importData['layoutDefinitions'], true); - $customLayout->setLayoutDefinitions($layout); - if (isset($importData['name'])) { - $customLayout->setName($importData['name']); - } - $customLayout->setDescription($importData['description']); - if (!$customLayout->isWriteable()) { - throw new ConfigWriteException(); - } - $customLayout->save(); - $success = true; - } catch (Exception $e) { - Logger::error($e->getMessage()); - } - } - - $responseContent['success'] = $success; - } - - $response = $this->adminJson($responseContent); - - // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in - // Ext.form.Action.Submit and mark the submission as failed - $response->headers->set('Content-Type', 'text/html'); - - return $response; - } - - #[Route('/get-custom-layout-definitions', name: 'getcustomlayoutdefinitions', methods: ['GET'])] - public function getCustomLayoutDefinitionsAction(Request $request): JsonResponse - { - $classIds = explode(',', $request->query->get('classId')); - $list = new DataObject\ClassDefinition\CustomLayout\Listing(); - - $list->setFilter(fn (DataObject\ClassDefinition\CustomLayout $layout) => in_array($layout->getClassId(), $classIds) && !str_contains($layout->getId(), '.brick.')); - $list = $list->load(); - $result = []; - foreach ($list as $item) { - $result[] = [ - 'id' => $item->getId(), - 'name' => $item->getName(), - 'default' => $item->getDefault(), - ]; - } - - return $this->adminJson(['success' => true, 'data' => $result]); - } - - #[Route('/get-all-layouts', name: 'getalllayouts', methods: ['GET'])] - public function getAllLayoutsAction(Request $request): JsonResponse - { - // get all classes - $resultList = []; - $mapping = []; - - $customLayouts = new DataObject\ClassDefinition\CustomLayout\Listing(); - $customLayouts->setFilter(fn (DataObject\ClassDefinition\CustomLayout $layout) => !str_contains($layout->getId(), '.brick.')); - $customLayouts->setOrder(fn (DataObject\ClassDefinition\CustomLayout $a, DataObject\ClassDefinition\CustomLayout $b) => strcmp($a->getName(), $b->getName())); - - $customLayouts = $customLayouts->load(); - foreach ($customLayouts as $layout) { - $mapping[$layout->getClassId()][] = $layout; - } - - $classList = new DataObject\ClassDefinition\Listing(); - $classList->setOrder('ASC'); - $classList->setOrderKey('name'); - $classList = $classList->load(); - - foreach ($classList as $class) { - if (isset($mapping[$class->getId()])) { - $classMapping = $mapping[$class->getId()]; - $resultList[] = [ - 'type' => 'main', - 'id' => $class->getId() . '_' . 0, - 'name' => $class->getName(), - ]; - - foreach ($classMapping as $layout) { - $resultList[] = [ - 'type' => 'custom', - 'id' => $class->getId() . '_' . $layout->getId(), - 'name' => $class->getName() . ' - ' . $layout->getName(), - ]; - } - } - } - - return $this->adminJson(['data' => $resultList]); - } - + #[IsGranted(CorePermission::Classes->value)] #[Route('/export-class', name: 'exportclass', methods: ['GET'])] - public function exportClassAction(Request $request): Response + public function exportClassAction(ExportClassPayload $payload, ExportClassHandler $handler): Response { - $id = $request->query->get('id'); - $class = DataObject\ClassDefinition::getById($id); - - if (!$class instanceof DataObject\ClassDefinition) { - $errorMessage = ': Class with id [ ' . $id . ' not found. ]'; - Logger::error($errorMessage); + $result = $handler($payload); - throw $this->createNotFoundException($errorMessage); - } - - $json = DataObject\ClassDefinition\Service::generateClassDefinitionJson($class); - - $response = new Response($json); + $response = new Response($result->json); $response->headers->set('Content-type', 'application/json'); - $response->headers->set('Content-Disposition', 'attachment; filename: "class_' . $class->getName() . '_export.json"'); + $response->headers->set('Content-Disposition', 'attachment; filename: "class_' . $result->className . '_export.json"'); return $response; } - #[Route('/export-custom-layout-definition', name: 'exportcustomlayoutdefinition', methods: ['GET'])] - public function exportCustomLayoutDefinitionAction(Request $request): Response - { - $id = $request->query->get('id'); - - if ($id) { - $customLayout = DataObject\ClassDefinition\CustomLayout::getById($id); - if ($customLayout) { - $name = $customLayout->getName(); - $json = DataObject\ClassDefinition\Service::generateCustomLayoutJson($customLayout); - - $response = new Response($json); - $response->headers->set('Content-type', 'application/json'); - $response->headers->set('Content-Disposition', 'attachment; filename: "custom_definition_' . $name . '_export.json"'); - - return $response; - } - } - - $errorMessage = ': Custom Layout with id [ ' . $id . ' not found. ]'; - Logger::error($errorMessage); - - throw $this->createNotFoundException($errorMessage); - } - - /** - * FIELDCOLLECTIONS - */ - #[Route('/fieldcollection-get', name: 'fieldcollectionget', methods: ['GET'])] - public function fieldcollectionGetAction(Request $request): JsonResponse - { - $fc = DataObject\Fieldcollection\Definition::getByKey($request->query->get('id')); - - $isWriteable = $fc->isWritable(); - $fc = $fc->getObjectVars(); - $fc['isWriteable'] = $isWriteable; - - return $this->adminJson($fc); - } - - #[Route('/fieldcollection-update', name: 'fieldcollectionupdate', methods: ['PUT', 'POST'])] - public function fieldcollectionUpdateAction(Request $request): JsonResponse - { - try { - $key = $request->request->get('key'); - $title = $request->request->get('title'); - $group = $request->request->get('group'); - - if ($request->request->get('task') === 'add') { - // check for existing fieldcollection with same name with different lower/upper cases - $list = new DataObject\Fieldcollection\Definition\Listing(); - $list = $list->loadNames(); - - foreach ($list as $fcName) { - if (strtolower($key) === strtolower($fcName)) { - throw new Exception('FieldCollection with the same name already exists (lower/upper cases may be different)'); - } - } - } - - $fcDef = new DataObject\Fieldcollection\Definition(); - $fcDef->setKey($key); - $fcDef->setTitle($title); - $fcDef->setGroup($group); - - if ($request->request->has('values')) { - $values = $this->decodeJson($request->request->get('values')); - $fcDef->setParentClass($values['parentClass']); - $fcDef->setImplementsInterfaces($values['implementsInterfaces']); - } - - if ($request->request->has('configuration')) { - $configuration = $this->decodeJson($request->request->get('configuration')); - - $configuration['datatype'] = 'layout'; - $configuration['fieldtype'] = 'panel'; - - $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); - $fcDef->setLayoutDefinitions($layout); - } - - $fcDef->save(); - - return $this->adminJson(['success' => true, 'id' => $fcDef->getKey()]); - } catch (Exception $e) { - Logger::error($e->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - #[Route('/import-fieldcollection', name: 'importfieldcollection', methods: ['POST'])] - public function importFieldcollectionAction(Request $request): Response - { - $this->checkPermission('fieldcollections'); - - $fieldCollection = DataObject\Fieldcollection\Definition::getByKey($request->query->get('id')); - - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $data = file_get_contents($file->getPathname()); - - $success = DataObject\ClassDefinition\Service::importFieldCollectionFromJson($fieldCollection, $data); - - $response = $this->adminJson([ - 'success' => $success, - ]); - - // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in - // Ext.form.Action.Submit and mark the submission as failed - $response->headers->set('Content-Type', 'text/html'); - - return $response; - } - - #[Route('/export-fieldcollection', name: 'exportfieldcollection', methods: ['GET'])] - public function exportFieldcollectionAction(Request $request): Response - { - $this->checkPermission('fieldcollections'); - - $fieldCollection = DataObject\Fieldcollection\Definition::getByKey($request->query->get('id')); - - if (!$fieldCollection instanceof DataObject\Fieldcollection\Definition) { - $errorMessage = ': Field-Collection with id [ ' . $request->query->get('id') . ' not found. ]'; - Logger::error($errorMessage); - - throw $this->createNotFoundException($errorMessage); - } - - $json = DataObject\ClassDefinition\Service::generateFieldCollectionJson($fieldCollection); - $response = new Response($json); - $response->headers->set('Content-type', 'application/json'); - $response->headers->set('Content-Disposition', 'attachment; filename="fieldcollection_' . $fieldCollection->getKey() . '_export.json"'); - - return $response; - } - - #[Route('/fieldcollection-delete', name: 'fieldcollectiondelete', methods: ['DELETE'])] - public function fieldcollectionDeleteAction(Request $request): JsonResponse - { - $this->checkPermission('fieldcollections'); - - $fc = DataObject\Fieldcollection\Definition::getByKey($request->request->get('id')); - $fc->delete(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/fieldcollection-tree', name: 'fieldcollectiontree', methods: ['GET', 'POST'])] - public function fieldcollectionTreeAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $list = new DataObject\Fieldcollection\Definition\Listing(); - $list = $list->load(); - - $forObjectEditor = $request->query->get('forObjectEditor'); - - $layoutDefinitions = []; - - $definitions = []; - - $allowedTypes = null; - if ($request->query->has('allowedTypes')) { - $allowedTypes = explode(',', $request->query->get('allowedTypes')); - } - - $object = $request->query->has('object_id') - ? DataObject\Concrete::getById((int) $request->query->get('object_id')) - : null; - - $currentLayoutId = $request->query->get('layoutId'); - $user = \OpenDxp\Tool\Admin::getCurrentUser(); - - $groups = []; - foreach ($list as $item) { - if ($allowedTypes && !in_array($item->getKey(), $allowedTypes)) { - continue; - } - - if ($item->getGroup()) { - if (!isset($groups[$item->getGroup()])) { - $groups[$item->getGroup()] = [ - 'id' => 'group_' . $item->getKey(), - 'text' => htmlspecialchars($item->getGroup()), - 'expandable' => true, - 'leaf' => false, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'group' => $item->getGroup(), - 'children' => [], - ]; - } - if ($forObjectEditor) { - $itemLayoutDefinitions = $item->getLayoutDefinitions(); - DataObject\Service::enrichLayoutDefinition($itemLayoutDefinitions, $object); - - if ($currentLayoutId == -1 && $user->isAdmin()) { - DataObject\Service::createSuperLayout($itemLayoutDefinitions); - } - $layoutDefinitions[$item->getKey()] = $itemLayoutDefinitions; - } - $groups[$item->getGroup()]['children'][] = - [ - 'id' => $item->getKey(), - 'text' => $item->getKey(), - 'title' => $item->getTitle(), - 'key' => $item->getKey(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_fieldcollection', - ]; - } else { - if ($forObjectEditor) { - $itemLayoutDefinitions = $item->getLayoutDefinitions(); - DataObject\Service::enrichLayoutDefinition($itemLayoutDefinitions, $object); - - if ($currentLayoutId == -1 && $user->isAdmin()) { - DataObject\Service::createSuperLayout($itemLayoutDefinitions); - } - - $layoutDefinitions[$item->getKey()] = $itemLayoutDefinitions; - } - $definitions[] = [ - 'id' => $item->getKey(), - 'text' => $item->getKey(), - 'title' => $item->getTitle(), - 'key' => $item->getKey(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_fieldcollection', - ]; - } - } - - foreach ($groups as $group) { - $definitions[] = $group; - } - - $event = new GenericEvent($this, [ - 'list' => $definitions, - 'objectId' => $request->query->get('object_id'), - 'layoutDefinitions' => $layoutDefinitions, - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_FIELDCOLLECTION_LIST_PRE_SEND_DATA); - $definitions = $event->getArgument('list'); - $layoutDefinitions = $event->getArgument('layoutDefinitions'); - - if ($forObjectEditor) { - return $this->adminJson(['fieldcollections' => $definitions, 'layoutDefinitions' => $layoutDefinitions]); - } - - return $this->adminJson($definitions); - } - - #[Route('/fieldcollection-list', name: 'fieldcollectionlist', methods: ['GET'])] - public function fieldcollectionListAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $user = \OpenDxp\Tool\Admin::getCurrentUser(); - $currentLayoutId = $request->query->get('layoutId'); - - $list = new DataObject\Fieldcollection\Definition\Listing(); - $list = $list->load(); - - if ($request->query->has('allowedTypes')) { - $filteredList = []; - $allowedTypes = explode(',', $request->query->get('allowedTypes')); - foreach ($list as $type) { - if (in_array($type->getKey(), $allowedTypes)) { - $filteredList[] = $type; - - // mainly for objects-meta data-type - $layoutDefinitions = $type->getLayoutDefinitions(); - $context = [ - 'containerType' => 'fieldcollection', - 'containerKey' => $type->getKey(), - 'outerFieldname' => $request->query->get('field_name'), - ]; - - $object = DataObject\Concrete::getById((int) $request->query->get('object_id')); - - DataObject\Service::enrichLayoutDefinition($layoutDefinitions, $object, $context); - - if ($currentLayoutId == -1 && $user->isAdmin()) { - DataObject\Service::createSuperLayout($layoutDefinitions); - } - } - } - - $list = $filteredList; - } - - $event = new GenericEvent($this, [ - 'list' => $list, - 'objectId' => $request->query->get('object_id'), - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_FIELDCOLLECTION_LIST_PRE_SEND_DATA); - $list = $event->getArgument('list'); - - return $this->adminJson(['fieldcollections' => $list]); - } - #[Route('/get-class-definition-for-column-config', name: 'getclassdefinitionforcolumnconfig', methods: ['GET'])] - public function getClassDefinitionForColumnConfigAction(Request $request): JsonResponse - { - $class = DataObject\ClassDefinition::getById($request->query->get('id')); - if (!$class) { - throw $this->createNotFoundException(); - } - $objectId = (int)$request->query->get('oid'); - - $filteredDefinitions = DataObject\Service::getCustomLayoutDefinitionForGridColumnConfig($class, $objectId); - - /** @var DataObject\ClassDefinition\Layout $layoutDefinitions */ - $layoutDefinitions = $filteredDefinitions['layoutDefinition'] ?? false; - $filteredFieldDefinition = $filteredDefinitions['fieldDefinition'] ?? false; - - $class->setFieldDefinitions([]); - - $result = []; - - DataObject\Service::enrichLayoutDefinition($layoutDefinitions); - - $result['objectColumns']['children'] = $layoutDefinitions->getChildren(); - $result['objectColumns']['nodeLabel'] = 'object_columns'; - $result['objectColumns']['nodeType'] = 'object'; - - // array("id", "fullpath", "published", "creationDate", "modificationDate", "filename", "classname"); - $systemColumnNames = DataObject\Concrete::SYSTEM_COLUMN_NAMES; - $systemColumns = []; - foreach ($systemColumnNames as $systemColumn) { - $systemColumns[] = ['title' => $systemColumn, 'name' => $systemColumn, 'datatype' => 'data', 'fieldtype' => 'system']; - } - $result['systemColumns']['nodeLabel'] = 'system_columns'; - $result['systemColumns']['nodeType'] = 'system'; - $result['systemColumns']['children'] = $systemColumns; - - $list = new DataObject\Objectbrick\Definition\Listing(); - $list = $list->load(); - - foreach ($list as $brickDefinition) { - $classDefs = $brickDefinition->getClassDefinitions(); - if (!empty($classDefs)) { - foreach ($classDefs as $classDef) { - if ($classDef['classname'] == $class->getName()) { - $fieldName = $classDef['fieldname']; - if (isset($filteredFieldDefinition[$fieldName]) && !$filteredFieldDefinition[$fieldName]) { - continue; - } - - $key = $brickDefinition->getKey(); - - $brickLayoutDefinitions = $brickDefinition->getLayoutDefinitions(); - $context = [ - 'containerType' => 'objectbrick', - 'containerKey' => $key, - 'outerFieldname' => $fieldName, - ]; - DataObject\Service::enrichLayoutDefinition($brickLayoutDefinitions, null, $context); - - $result[$key]['nodeLabel'] = $key; - $result[$key]['brickField'] = $fieldName; - $result[$key]['nodeType'] = 'objectbricks'; - $result[$key]['children'] = $brickLayoutDefinitions->getChildren(); - - break; - } - } - } - } - - return $this->adminJson($result); - } - - /** - * OBJECT BRICKS - */ - #[Route('/objectbrick-get', name: 'objectbrickget', methods: ['GET'])] - public function objectbrickGetAction(Request $request): JsonResponse - { - $fc = DataObject\Objectbrick\Definition::getByKey($request->query->get('id')); - - $isWriteable = $fc->isWritable(); - $fc = $fc->getObjectVars(); - $fc['isWriteable'] = $isWriteable; - - return $this->adminJson($fc); - } - - #[Route('/objectbrick-update', name: 'objectbrickupdate', methods: ['PUT', 'POST'])] - public function objectbrickUpdateAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - try { - $key = $request->request->get('key'); - $title = $request->request->get('title'); - $group = $request->request->get('group'); - - if ($request->request->get('task') === 'add') { - // check for existing brick with same name with different lower/upper cases - $list = new DataObject\Objectbrick\Definition\Listing(); - $list = $list->loadNames(); - - foreach ($list as $brickName) { - if (strtolower($key) === strtolower($brickName)) { - throw new Exception('Brick with the same name already exists (lower/upper cases may be different)'); - } - } - } - - // now we create a new definition - $brickDef = new DataObject\Objectbrick\Definition(); - $brickDef->setKey($key); - $brickDef->setTitle($title); - $brickDef->setGroup($group); - - if ($request->request->has('values')) { - $values = $this->decodeJson($request->request->get('values')); - - $brickDef->setParentClass($values['parentClass']); - $brickDef->setImplementsInterfaces($values['implementsInterfaces']); - $brickDef->setClassDefinitions($values['classDefinitions']); - } - - if ($request->request->has('configuration')) { - $configuration = $this->decodeJson($request->request->get('configuration')); - - $configuration['datatype'] = 'layout'; - $configuration['fieldtype'] = 'panel'; - - $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); - $brickDef->setLayoutDefinitions($layout); - } - - $event = new GenericEvent($this, [ - 'brickDefinition' => $brickDef, - ]); - - $eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_UPDATE_DEFINITION); - $brickDef = $event->getArgument('brickDefinition'); - - $brickDef->save(); - - return $this->adminJson(['success' => true, 'id' => $brickDef->getKey()]); - } catch (Exception $e) { - Logger::error($e->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - #[Route('/import-objectbrick', name: 'importobjectbrick', methods: ['POST'])] - public function importObjectbrickAction(Request $request): JsonResponse - { - $this->checkPermission('objectbricks'); - - $objectBrick = DataObject\Objectbrick\Definition::getByKey($request->query->get('id')); - - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - $data = file_get_contents($file->getPathname()); - $success = DataObject\ClassDefinition\Service::importObjectBrickFromJson($objectBrick, $data); - - $response = $this->adminJson([ - 'success' => $success, - ]); - - // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in - // Ext.form.Action.Submit and mark the submission as failed - $response->headers->set('Content-Type', 'text/html'); - - return $response; - } - - #[Route('/export-objectbrick', name: 'exportobjectbrick', methods: ['GET'])] - public function exportObjectbrickAction(Request $request): Response - { - $this->checkPermission('objectbricks'); - - $objectBrick = DataObject\Objectbrick\Definition::getByKey($request->query->get('id')); - - if (!$objectBrick instanceof DataObject\Objectbrick\Definition) { - $errorMessage = ': Object-Brick with id [ ' . $request->query->get('id') . ' not found. ]'; - Logger::error($errorMessage); - - throw $this->createNotFoundException($errorMessage); - } - - $xml = DataObject\ClassDefinition\Service::generateObjectBrickJson($objectBrick); - $response = new Response($xml); - $response->headers->set('Content-type', 'application/json'); - $response->headers->set('Content-Disposition', 'attachment; filename="objectbrick_' . $objectBrick->getKey() . '_export.json"'); - - return $response; - } - - #[Route('/objectbrick-delete', name: 'objectbrickdelete', methods: ['DELETE'])] - public function objectbrickDeleteAction(Request $request): JsonResponse - { - $this->checkPermission('objectbricks'); - - $fc = DataObject\Objectbrick\Definition::getByKey($request->request->get('id')); - $fc->delete(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/objectbrick-tree', name: 'objectbricktree', methods: ['GET', 'POST'])] - public function objectbrickTreeAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $list = new DataObject\Objectbrick\Definition\Listing(); - $list = $list->load(); - - $forObjectEditor = $request->query->get('forObjectEditor'); - - $context = []; - $layoutDefinitions = []; - $groups = []; - $definitions = []; - $fieldname = null; - $className = null; - - $object = $request->query->has('object_id') - ? DataObject\Concrete::getById((int) $request->query->get('object_id')) - : null; - - if ($request->query->has('class_id') && $request->query->has('field_name')) { - $classId = $request->query->get('class_id'); - $fieldname = $request->query->get('field_name'); - $classDefinition = DataObject\ClassDefinition::getById($classId); - $className = $classDefinition->getName(); - } - - foreach ($list as $item) { - if ($forObjectEditor) { - $context = [ - 'containerType' => 'objectbrick', - 'containerKey' => $item->getKey(), - 'outerFieldname' => $fieldname, - ]; - } - if ($request->query->has('class_id') && $request->query->has('field_name')) { - $keep = false; - $clsDefs = $item->getClassDefinitions(); - if (!empty($clsDefs)) { - foreach ($clsDefs as $cd) { - if ($cd['classname'] == $className && $cd['fieldname'] == $fieldname) { - $keep = true; - - continue; - } - } - } - if (!$keep) { - continue; - } - } - - if ($item->getGroup()) { - if (!isset($groups[$item->getGroup()])) { - $groups[$item->getGroup()] = [ - 'id' => 'group_' . $item->getKey(), - 'text' => htmlspecialchars($item->getGroup()), - 'expandable' => true, - 'leaf' => false, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'group' => $item->getGroup(), - 'children' => [], - ]; - } - if ($forObjectEditor) { - $layoutId = $request->query->get('layoutId'); - $itemLayoutDefinitions = null; - if ($layoutId) { - $layout = DataObject\ClassDefinition\CustomLayout::getById($layoutId.'.brick.'.$item->getKey()); - if ($layout instanceof DataObject\ClassDefinition\CustomLayout) { - $itemLayoutDefinitions = $layout->getLayoutDefinitions(); - } - } - - if (!$itemLayoutDefinitions instanceof \OpenDxp\Model\DataObject\ClassDefinition\Layout) { - $itemLayoutDefinitions = $item->getLayoutDefinitions(); - } - - DataObject\Service::enrichLayoutDefinition($itemLayoutDefinitions, $object, $context); - - $layoutDefinitions[$item->getKey()] = $itemLayoutDefinitions; - } - $groups[$item->getGroup()]['children'][] = - [ - 'id' => $item->getKey(), - 'text' => $item->getKey(), - 'title' => $item->getTitle(), - 'key' => $item->getKey(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_objectbricks', - ]; - } else { - if ($forObjectEditor) { - $layout = $item->getLayoutDefinitions(); - - $currentLayoutId = $request->query->get('layoutId'); - - $user = $this->getAdminUser(); - if ($currentLayoutId == -1 && $user->isAdmin()) { - DataObject\Service::createSuperLayout($layout); - } elseif ($currentLayoutId) { - $customLayout = DataObject\ClassDefinition\CustomLayout::getById($currentLayoutId.'.brick.'.$item->getKey()); - if ($customLayout instanceof DataObject\ClassDefinition\CustomLayout) { - $layout = $customLayout->getLayoutDefinitions(); - } - } - - DataObject\Service::enrichLayoutDefinition($layout, $object, $context); - - $layoutDefinitions[$item->getKey()] = $layout; - } - $definitions[] = [ - 'id' => $item->getKey(), - 'text' => $item->getKey(), - 'title' => $item->getTitle(), - 'key' => $item->getKey(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_objectbricks', - ]; - } - } - - foreach ($groups as $group) { - $definitions[] = $group; - } - - $event = new GenericEvent($this, [ - 'list' => $definitions, - 'objectId' => $request->query->get('object_id'), - 'forObjectEditor' => $forObjectEditor, - 'layoutDefinitions' => $layoutDefinitions, - 'fieldName' => $request->query->get('field_name'), - 'object' => $object, - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_LIST_PRE_SEND_DATA); - $definitions = $event->getArgument('list'); - $layoutDefinitions = $event->getArgument('layoutDefinitions'); - - if ($forObjectEditor) { - return $this->adminJson(['objectbricks' => $definitions, 'layoutDefinitions' => $layoutDefinitions]); - } - - return $this->adminJson($definitions); - } - - #[Route('/objectbrick-list', name: 'objectbricklist', methods: ['GET'])] - public function objectbrickListAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $list = new DataObject\Objectbrick\Definition\Listing(); - $list = $list->load(); - - if ($request->query->has('class_id') && $request->query->has('field_name')) { - $filteredList = []; - $classId = $request->query->get('class_id'); - $fieldname = $request->query->get('field_name'); - $classDefinition = DataObject\ClassDefinition::getById($classId); - $className = $classDefinition->getName(); - - foreach ($list as $type) { - $clsDefs = $type->getClassDefinitions(); - if (!empty($clsDefs)) { - foreach ($clsDefs as $cd) { - if ($cd['classname'] == $className && $cd['fieldname'] == $fieldname) { - $filteredList[] = $type; - - continue; - } - } - } - - $layout = $type->getLayoutDefinitions(); - - $currentLayoutId = $request->query->get('layoutId'); - - $user = $this->getAdminUser(); - if ($currentLayoutId == -1 && $user->isAdmin()) { - DataObject\Service::createSuperLayout($layout); - $objectData['layout'] = $layout; - } - - $context = [ - 'containerType' => 'objectbrick', - 'containerKey' => $type->getKey(), - 'outerFieldname' => $request->query->get('field_name'), - ]; - - $object = DataObject\Concrete::getById((int) $request->query->get('object_id')); - - DataObject\Service::enrichLayoutDefinition($layout, $object, $context); - $type->setLayoutDefinitions($layout); - } - - $list = $filteredList; - } - - $event = new GenericEvent($this, [ - 'list' => $list, - 'objectId' => $request->query->get('object_id'), - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_LIST_PRE_SEND_DATA); - $list = $event->getArgument('list'); - - return $this->adminJson(['objectbricks' => $list]); + public function getClassDefinitionForColumnConfigAction( + GetClassDefinitionForColumnConfigPayload $payload, + GetClassDefinitionForColumnConfigHandler $handler, + ): JsonResponse { + return $this->adminJson($handler($payload)->config); } /** * Add option to export/import all class definitions/brick definitions etc. at once */ + #[IsGranted(CorePermission::Classes->value)] #[Route('/bulk-import', name: 'bulkimport', methods: ['POST'])] - public function bulkImportAction(Request $request): JsonResponse + public function bulkImportAction(BulkImportPayload $payload, BulkImportHandler $handler, Request $request): JsonResponse { - $result = []; - - /** @var UploadedFile $uploadFile */ - $uploadFile = $request->files->get('Filedata'); - - $json = file_get_contents($uploadFile->getPathname()); - - $tmpName = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/bulk-import-' . uniqid('', false) . '.tmp'; - file_put_contents($tmpName, $json); + $result = $handler($payload); - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($tmpName): void { - $session->set('class_bulk_import_file', $tmpName); + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { + $session->set('class_bulk_import_file', $result->tmpFile); }, 'opendxp_objects'); - $json = json_decode($json, true); - - foreach ($json as $groupName => $group) { - foreach ($group as $groupItem) { - $displayName = null; - $icon = null; - - if ($groupName === 'class') { - $name = $groupItem['name']; - $icon = 'class'; - } elseif ($groupName === 'customlayout') { - $className = $groupItem['className']; - - $layoutData = ['className' => $className, 'name' => $groupItem['name']]; - $name = base64_encode(json_encode($layoutData)); - $displayName = $className . ' / ' . $groupItem['name']; - $icon = 'custom_views'; - } else { - if ($groupName === 'objectbrick') { - $icon = 'objectbricks'; - } elseif ($groupName === 'fieldcollection') { - $icon = 'fieldcollection'; - } - $name = $groupItem['key']; - } - - if (!$displayName) { - $displayName = $name; - } - $result[] = ['icon' => $icon, 'checked' => true, 'type' => $groupName, 'name' => $name, 'displayName' => $displayName]; - } - } - - $response = $this->adminJson(['success' => true, 'data' => $result]); + $response = $this->adminJson(ApiResponse::ok(['data' => $result->items])); $response->headers->set('Content-Type', 'text/html'); return $response; @@ -1416,657 +191,118 @@ public function bulkImportAction(Request $request): JsonResponse /** * Add option to export/import all class definitions/brick definitions etc. at once - * - * @throws Exception */ #[Route('/bulk-commit', name: 'bulkcommit', methods: ['POST'])] - public function bulkCommitAction(Request $request): JsonResponse + public function bulkCommitAction(BulkCommitPayload $payload, BulkCommitHandler $handler): JsonResponse { - $data = json_decode($request->request->get('data'), true); - - $session = Session::getSessionBag($request->getSession(), 'opendxp_objects'); - $filename = $session->get('class_bulk_import_file'); - $json = @file_get_contents($filename); - $json = json_decode($json, true); - - $type = $data['type']; - $name = $data['name']; - $list = $json[$type]; - - foreach ($list as $item) { - - unset($item['creationDate'], $item['modificationDate'], $item['userOwner'], $item['userModification']); - - if ($type === 'class' && $item['name'] == $name) { - $this->checkPermission('classes'); - $class = DataObject\ClassDefinition::getByName($name); - if (!$class) { - $class = new DataObject\ClassDefinition(); - $class->setName($name); - } - $success = DataObject\ClassDefinition\Service::importClassDefinitionFromJson($class, json_encode($item), true); - - return $this->adminJson(['success' => $success]); - } - - if ($type === 'objectbrick' && $item['key'] == $name) { - $this->checkPermission('objectbricks'); - if (!$brick = DataObject\Objectbrick\Definition::getByKey($name)) { - $brick = new DataObject\Objectbrick\Definition(); - $brick->setKey($name); - } - - $success = DataObject\ClassDefinition\Service::importObjectBrickFromJson($brick, json_encode($item), true); - - return $this->adminJson(['success' => $success]); - } + $handler($payload); - if ($type === 'fieldcollection' && $item['key'] == $name) { - $this->checkPermission('fieldcollections'); - if (!$fieldCollection = DataObject\Fieldcollection\Definition::getByKey($name)) { - $fieldCollection = new DataObject\Fieldcollection\Definition(); - $fieldCollection->setKey($name); - } - - $success = DataObject\ClassDefinition\Service::importFieldCollectionFromJson($fieldCollection, json_encode($item), true); - - return $this->adminJson(['success' => $success]); - } - - if ($type === 'customlayout') { - $this->checkPermission('classes'); - $layoutData = json_decode(base64_decode($data['name']), true); - $className = $layoutData['className']; - $layoutName = $layoutData['name']; - - if ($item['name'] == $layoutName && $item['className'] == $className) { - $class = DataObject\ClassDefinition::getByName($className); - if (!$class) { - throw new Exception('Class does not exist'); - } - - $classId = $class->getId(); - - $layoutList = new DataObject\ClassDefinition\CustomLayout\Listing(); - $layoutList->setFilter(fn (DataObject\ClassDefinition\CustomLayout $layout) => $layout->getName() === $layoutName && $layout->getClassId() === $classId); - $layoutList = $layoutList->load(); - - $layoutDefinition = null; - if ($layoutList) { - $layoutDefinition = array_values($layoutList)[0]; - } - - if (!$layoutDefinition) { - $layoutDefinition = new DataObject\ClassDefinition\CustomLayout(); - $layoutDefinition->setName($layoutName); - $layoutDefinition->setClassId($classId); - } - - try { - $layoutDefinition->setDescription($item['description']); - $layoutDef = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($item['layoutDefinitions'], true); - $layoutDefinition->setLayoutDefinitions($layoutDef); - $layoutDefinition->save(); - } catch (Exception $e) { - Logger::error($e->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - } - } - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } /** * Add option to export/import all class definitions/brick definitions etc. at once */ + #[IsGranted(CorePermission::Classes->value)] #[Route('/bulk-export-prepare', name: 'bulkexportprepare', methods: ['POST'])] - public function bulkExportPrepareAction(Request $request): Response + public function bulkExportPrepareAction(BulkExportPreparePayload $payload, Request $request): Response { - $data = $request->request->get('data'); - - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($data): void { - $session->set('class_bulk_export_settings', $data); + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($payload): void { + $session->set('class_bulk_export_settings', $payload->data); }, 'opendxp_objects'); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/bulk-export', name: 'bulkexport', methods: ['GET'])] - public function bulkExportAction(Request $request): JsonResponse + public function bulkExportAction(GetClassBulkExportListHandler $handler): JsonResponse { - $result = []; - - if ($this->getAdminUser()->isAllowed('fieldcollections')) { - $fieldCollections = new DataObject\Fieldcollection\Definition\Listing(); - $fieldCollections = $fieldCollections->load(); - - foreach ($fieldCollections as $fieldCollection) { - $result[] = [ - 'icon' => 'fieldcollection', - 'checked' => true, - 'type' => 'fieldcollection', - 'name' => $fieldCollection->getKey(), - 'displayName' => $fieldCollection->getKey(), - ]; - } - } - - if ($this->getAdminUser()->isAllowed('classes')) { - $classes = new DataObject\ClassDefinition\Listing(); - $classes->setOrder('ASC'); - $classes->setOrderKey('id'); - $classes = $classes->load(); - - foreach ($classes as $class) { - $result[] = [ - 'icon' => 'class', - 'checked' => true, - 'type' => 'class', - 'name' => $class->getName(), - 'displayName' => $class->getName(), - ]; - } - } - - if ($this->getAdminUser()->isAllowed('objectbricks')) { - $objectBricks = new DataObject\Objectbrick\Definition\Listing(); - $objectBricks = $objectBricks->loadNames(); - - foreach ($objectBricks as $brickName) { - $result[] = [ - 'icon' => 'objectbricks', - 'checked' => true, - 'type' => 'objectbrick', - 'name' => $brickName, - 'displayName' => $brickName, - ]; - } - } - - if ($this->getAdminUser()->isAllowed('classes')) { - $customLayouts = new DataObject\ClassDefinition\CustomLayout\Listing(); - $customLayouts = $customLayouts->load(); - foreach ($customLayouts as $customLayout) { - $class = DataObject\ClassDefinition::getById($customLayout->getClassId()); - $displayName = $class->getName().' / '.$customLayout->getName(); - - $result[] = [ - 'icon' => 'custom_views', - 'checked' => true, - 'type' => 'customlayout', - 'name' => $customLayout->getId(), - 'displayName' => $displayName, - ]; - } - } - - return new JsonResponse(['success' => true, 'data' => $result]); + return $this->adminJson(ApiResponse::ok(['data' => $handler()->data])); } #[Route('/do-bulk-export', name: 'dobulkexport', methods: ['GET'])] - public function doBulkExportAction(Request $request): Response + public function doBulkExportAction(DoBulkExportHandler $handler): Response { - $session = Session::getSessionBag($request->getSession(), 'opendxp_objects'); - $list = $session->get('class_bulk_export_settings'); - $list = json_decode($list, true); - $result = []; - - foreach ($list as $item) { - if ($item['type'] === 'fieldcollection' && $this->getAdminUser()->isAllowed('fieldcollections')) { - if ($fieldCollection = DataObject\Fieldcollection\Definition::getByKey($item['name'])) { - $fieldCollectionJson = json_decode(DataObject\ClassDefinition\Service::generateFieldCollectionJson($fieldCollection)); - $fieldCollectionJson->key = $item['name']; - $result['fieldcollection'][] = $fieldCollectionJson; - } - } elseif ($item['type'] === 'class' && $this->getAdminUser()->isAllowed('classes')) { - if ($class = DataObject\ClassDefinition::getByName($item['name'])) { - $data = json_decode(DataObject\ClassDefinition\Service::generateClassDefinitionJson($class)); - $data->name = $item['name']; - $result['class'][] = $data; - } - } elseif ($item['type'] === 'objectbrick' && $this->getAdminUser()->isAllowed('objectbricks')) { - if ($objectBrick = DataObject\Objectbrick\Definition::getByKey($item['name'])) { - $objectBrickJson = json_decode(DataObject\ClassDefinition\Service::generateObjectBrickJson($objectBrick)); - $objectBrickJson->key = $item['name']; - $result['objectbrick'][] = $objectBrickJson; - } - } elseif ($item['type'] === 'customlayout' && $this->getAdminUser()->isAllowed('classes')) { - if ($customLayout = DataObject\ClassDefinition\CustomLayout::getById($item['name'])) { - $classId = $customLayout->getClassId(); - $class = DataObject\ClassDefinition::getById($classId); - $customLayoutJson = json_decode(DataObject\ClassDefinition\Service::generateCustomLayoutJson($customLayout)); - $customLayoutJson->name = $customLayout->getName(); - $customLayoutJson->className = $class->getName(); - $result['customlayout'][] = $customLayoutJson; - } - } - } + $result = $handler(); - $result = json_encode($result, JSON_PRETTY_PRINT); - $response = new Response($result); + $response = new Response($result->json); $response->headers->set('Content-type', 'application/json'); $response->headers->set('Content-Disposition', 'attachment; filename="bulk_export.json"'); return $response; } - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - // check permissions - $unrestrictedActions = [ - 'getTreeAction', 'fieldcollectionListAction', 'fieldcollectionTreeAction', 'fieldcollectionGetAction', - 'getClassDefinitionForColumnConfigAction', 'objectbrickListAction', 'objectbrickTreeAction', 'objectbrickGetAction', - 'objectbrickDeleteAction', 'objectbrickUpdateAction', 'importObjectbrickAction', 'exportObjectbrickAction', 'bulkCommitAction', 'doBulkExportAction', 'bulkExportAction', 'importFieldcollectionAction', 'exportFieldcollectionAction', // permissions for listed write operations handled separately in action methods - 'selectOptionsGetAction', 'selectOptionsTreeAction', 'selectOptionsUpdateAction', 'getSelectOptionsUsagesAction', 'selectOptionsDeleteAction', - ]; - - $this->checkActionPermission($event, 'classes', $unrestrictedActions); - } - - #[Route('/get-fieldcollection-usages', name: 'getfieldcollectionusages', methods: ['GET'])] - public function getFieldcollectionUsagesAction(Request $request): Response - { - $key = $request->query->get('key'); - $result = []; - - $classes = new DataObject\ClassDefinition\Listing(); - $classes = $classes->load(); - foreach ($classes as $class) { - $fieldDefs = $class->getFieldDefinitions(); - foreach ($fieldDefs as $fieldDef) { - if ($fieldDef instanceof DataObject\ClassDefinition\Data\Fieldcollections) { - $allowedKeys = $fieldDef->getAllowedTypes(); - if (in_array($key, $allowedKeys)) { - $result[] = [ - 'class' => $class->getName(), - 'field' => $fieldDef->getName(), - ]; - } - } - } - } - - return $this->adminJson($result); - } - - #[Route('/get-bricks-usages', name: 'getbrickusages', methods: ['GET'])] - public function getBrickUsagesAction(Request $request): Response - { - $classId = $request->query->get('classId'); - $myclass = DataObject\ClassDefinition::getById($classId); - - $result = []; - - $brickDefinitions = new DataObject\Objectbrick\Definition\Listing(); - $brickDefinitions = $brickDefinitions->load(); - foreach ($brickDefinitions as $brickDefinition) { - $classes = $brickDefinition->getClassDefinitions(); - foreach ($classes as $class) { - if ($myclass->getName() == $class['classname']) { - $result[] = [ - 'objectbrick' => $brickDefinition->getKey(), - 'field' => $class['fieldname'], - ]; - } - } - } - - return $this->adminJson($result); - } - - #[Route('/get-select-options-usages', name: 'getselectoptionsusages', methods: [Request::METHOD_GET])] - public function getSelectOptionsUsagesAction(Request $request): Response - { - $usages = []; - $id = $request->query->get(DataObject\SelectOptions\Config::PROPERTY_ID); - $selectOptionsConfiguration = $this->getSelectOptionsConfig($id); - foreach ($selectOptionsConfiguration->getFieldsUsedIn() as $className => $fieldNames) { - foreach ($fieldNames as $fieldName) { - $usages[] = [ - 'class' => $className, - 'field' => $fieldName, - ]; - } - } - - return $this->adminJson($usages); + #[Route('/get-select-options-usages', name: 'getselectoptionsusages', methods: ['GET'])] + public function getSelectOptionsUsagesAction( + GetSelectOptionsUsagesPayload $payload, + GetSelectOptionsUsagesHandler $handler, + ): Response { + return $this->adminJson($handler($payload)->usages); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/get-icons', name: 'geticons', methods: ['GET'])] - public function getIconsAction(Request $request, EventDispatcherInterface $eventDispatcher): Response + public function getIconsAction(GetClassIconsPayload $payload, GetClassIconsHandler $handler): Response { - $classId = $request->query->get('classId'); - $type = $request->query->has('type') ? $request->query->getString('type') : null; - - $iconDir = OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img'; - if ($type === '') { - return $this->adminJson([]); - } - - if ($type === null) { - $classIcons = FileSystemHelper::scanDirectory($iconDir . '/object-icons/'); - $colorIcons = FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'); - $twemoji = FileSystemHelper::scanDirectory($iconDir . '/twemoji/'); - $icons = [...$classIcons, ...$colorIcons, ...$twemoji]; - } else { - $icons = match($type) { - 'color' => FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'), - 'white' => FileSystemHelper::scanDirectory($iconDir . '/flat-white-icons/'), - 'twemoji-1', 'twemoji-2', 'twemoji-3', - 'twemoji_variants-1', 'twemoji_variants-2', 'twemoji_variants-3' - => FileSystemHelper::scanDirectory($iconDir . '/twemoji/'), - default => [], - }; - } - - $style = ''; - if ($type === 'white') { - $style = 'background-color:#000'; - } - - foreach ($icons as &$icon) { - $icon = str_replace(OPENDXP_WEB_ROOT, '', $icon); - } - - $event = new GenericEvent($this, [ - 'icons' => $icons, - 'classId' => $classId, - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECT_ICONS_PRE_SEND_DATA); - $icons = $event->getArgument('icons'); - - $startIndex = 0; - $result = []; - - if ($type !== null && str_starts_with($type, 'twemoji')) { - foreach ($icons as $index => $twemojiIcon) { - $iconBase = basename($twemojiIcon); - - // All the variants (like skin color) have a hyphen in their base name - // Here we remove/unset wheter if the selected icon type is the variant list - $explodeByHyphen = explode('-', $iconBase); - if ( - (!str_starts_with($type, 'twemoji_variants') && isset($explodeByHyphen[1])) || - (str_starts_with($type, 'twemoji_variants') && !isset($explodeByHyphen[1])) - ) { - unset($icons[$index]); - } - } - - $icons = array_values($icons); - $limit = count($icons); - - if (str_ends_with($type, '-1')) { - $limit = floor($limit / 3); - } - if (str_ends_with($type, '-2')) { - $startIndex = floor($limit / 3); - $limit = floor($limit / 3 * 2); - } - if (str_ends_with($type, '-3')) { - $startIndex = floor($limit / 3 * 2); - } - } else { - $limit = count($icons); - } - - for ($i = $startIndex; $i < $limit; $i++) { - $icon = $icons[$i]; - $content = file_get_contents(OPENDXP_WEB_ROOT . $icon); - $result[] = [ - 'text' => sprintf( - '', - $style, - mime_content_type(OPENDXP_WEB_ROOT . $icon), - base64_encode($content) - ), - 'value' => $icon, - ]; - } - - return $this->adminJson($result); + return $this->adminJson($handler($payload)->icons); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/suggest-class-identifier', name: 'suggestclassidentifier')] - public function suggestClassIdentifierAction(): Response - { - $db = Db::get(); - $maxId = $db->fetchOne('SELECT MAX(CAST(id AS SIGNED)) FROM classes'); - - $existingIds = $db->fetchFirstColumn('SELECT LOWER(id) FROM classes'); - - $result = [ - 'suggestedIdentifier' => $maxId ? $maxId + 1 : 1, - 'existingIds' => $existingIds, - ]; - - return $this->adminJson($result); - } - - #[Route('/suggest-custom-layout-identifier', name: 'suggestcustomlayoutidentifier', methods: ['GET'])] - public function suggestCustomLayoutIdentifierAction(Request $request): Response + public function suggestClassIdentifierAction(SuggestClassIdentifierHandler $handler): Response { - $classId = $request->query->get('classId'); - - $identifier = DataObject\ClassDefinition\CustomLayout::getIdentifier($classId); - - $list = new DataObject\ClassDefinition\CustomLayout\Listing(); - - $list = $list->load(); - $existingIds = []; - $existingNames = []; + $result = $handler(); - foreach ($list as $item) { - $existingIds[] = $item->getId(); - if ($item->getClassId() == $classId) { - $existingNames[] = $item->getName(); - } - } - - $result = [ - 'suggestedIdentifier' => $identifier, - 'existingIds' => $existingIds, - 'existingNames' => $existingNames, - ]; - - return $this->adminJson($result); + return $this->adminJson([ + 'suggestedIdentifier' => $result->suggestedIdentifier, + 'existingIds' => $result->existingIds, + ]); } + #[IsGranted(CorePermission::Classes->value)] #[Route('/text-layout-preview', name: 'textlayoutpreview', methods: ['GET'])] - public function textLayoutPreviewAction(Request $request): Response + public function textLayoutPreviewAction(GetTextLayoutPreviewPayload $payload, GetTextLayoutPreviewHandler $handler): Response { - $objPath = $request->query->get('previewObject', ''); - $className = '\\OpenDxp\\Model\\DataObject\\' . $request->query->get('className'); - $obj = DataObject::getByPath($objPath) ?? new $className(); - - $textLayout = new DataObject\ClassDefinition\Layout\Text(); - $textLayout->setName('textLayoutPreview' . $className); - - $context = [ - 'data' => $request->query->get('renderingData'), - ]; - - if ($renderingClass = $request->query->get('renderingClass')) { - $textLayout->setRenderingClass($renderingClass); - $textLayout->setRenderingData($request->query->get('renderingData', '')); - } - - if ($staticHtml = $request->query->get('html')) { - $textLayout->setHtml($staticHtml); - } - - $html = $textLayout->enrichLayoutDefinition($obj, $context)->getHtml(); - - $content = - "\n" . - "\n" . - '\n" . - "\n\n" . - "\n" . - $html . - "\n\n\n" . - "\n"; - - $response = new Response($content); + $response = new Response($handler($payload)->content); $response->headers->set('Content-Type', 'text/html'); return $response; } + #[IsGranted(CorePermission::Classes->value)] #[Route('/video-supported-types', name: 'videosupportedTypestypes', methods: ['GET'])] - public function videoAllowedTypesAction(Request $request, TranslatorInterface $translator): Response - { - $videoDef = new DataObject\ClassDefinition\Data\Video(); - $res = []; - - foreach ($videoDef->getSupportedTypes() as $type) { - $res[] = [ - 'key' => $type, - 'value' => $translator->trans($type, [], 'admin'), - ]; - } - - return $this->adminJson($res); - } - - #[Route('/select-options-get', name: 'selectoptionsget', methods: [Request::METHOD_GET])] - public function selectOptionsGetAction(Request $request): JsonResponse + public function videoAllowedTypesAction(GetVideoAllowedTypesHandler $handler): Response { - $this->checkPermission('selectoptions'); - $id = $request->query->get(DataObject\SelectOptions\Config::PROPERTY_ID); - $selectOptionsConfiguration = $this->getSelectOptionsConfig($id); - - $data = $selectOptionsConfiguration->getObjectVars(); - $data['isWriteable'] = $selectOptionsConfiguration->isWriteable(); - $data['enumName'] = $selectOptionsConfiguration->getEnumName(true); - - return $this->adminJson($data); + return $this->adminJson($handler()->types); } - #[Route('/select-options-update', name: 'selectoptionsupdate', methods: [Request::METHOD_PUT, Request::METHOD_POST])] - public function selectOptionsUpdateAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + #[IsGranted(CorePermission::Selectoptions->value)] + #[Route('/select-options-get', name: 'selectoptionsget', methods: ['GET'])] + public function selectOptionsGetAction(GetSelectOptionsPayload $payload, GetSelectOptionsHandler $handler): JsonResponse { - $this->checkPermission('selectoptions'); - - try { - $id = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_ID); - - if ($request->request->get('task') === 'add' && (new DataObject\SelectOptions\Config\Listing())->hasConfig($id)) { - throw new Exception('Select options with the same ID already exists (lower/upper cases may be different)'); - } - - $group = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_GROUP); - $useTraits = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_USE_TRAITS, ''); - $implementsInterfaces = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_IMPLEMENTS_INTERFACES, ''); - $selectOptionsData = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_SELECT_OPTIONS, 'null'); - $selectOptionsConfiguration = DataObject\SelectOptions\Config::createFromData( - [ - DataObject\SelectOptions\Config::PROPERTY_ID => $id, - DataObject\SelectOptions\Config::PROPERTY_GROUP => $group, - DataObject\SelectOptions\Config::PROPERTY_USE_TRAITS => $useTraits, - DataObject\SelectOptions\Config::PROPERTY_IMPLEMENTS_INTERFACES => $implementsInterfaces, - DataObject\SelectOptions\Config::PROPERTY_SELECT_OPTIONS => $this->decodeJson($selectOptionsData), - ] - ); - - $event = new GenericEvent($this, [ - 'selectOptionsConfiguration' => $selectOptionsConfiguration, - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_SELECTOPTIONS_UPDATE_CONFIGURATION); - /** @var DataObject\SelectOptions\Config $selectOptionsConfiguration */ - $selectOptionsConfiguration = $event->getArgument('selectOptionsConfiguration'); - - $selectOptionsConfiguration->save(); - - return $this->adminJson(['success' => true, 'id' => $selectOptionsConfiguration->getId()]); - } catch (Exception $exception) { - Logger::error($exception->getMessage()); - - return $this->adminJson(['success' => false, 'message' => $exception->getMessage()]); - } + return $this->adminJson($handler($payload)->data); } - #[Route('/select-options-tree', name: 'selectoptionstree', methods: [Request::METHOD_GET, Request::METHOD_POST])] - public function selectOptionsTreeAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + #[IsGranted(CorePermission::Selectoptions->value)] + #[Route('/select-options-update', name: 'selectoptionsupdate', methods: ['PUT', 'POST'])] + public function selectOptionsUpdateAction(SaveSelectOptionsPayload $payload, SaveSelectOptionsHandler $handler): JsonResponse { - $this->checkPermission('selectoptions'); - $configurations = $groups = []; - - $selectOptionConfigs = new DataObject\SelectOptions\Config\Listing(); - foreach ($selectOptionConfigs as $selectOptionConfig) { - $id = $selectOptionConfig->getId(); - $configurationData = [ - 'id' => $id, - 'text' => $id, - 'leaf' => true, - 'iconCls' => 'opendxp_icon_select', - ]; - - if ((int)$request->query->get('grouped', '0') === 0 || !$selectOptionConfig->hasGroup()) { - $configurations[] = $configurationData; - - continue; - } - - $group = $selectOptionConfig->getGroup(); - if (!isset($groups[$group])) { - $groups[$group] = [ - 'id' => 'group_' . $id, - 'text' => htmlspecialchars($group ?? ''), - 'expandable' => true, - 'leaf' => false, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'group' => $group, - 'children' => [], - ]; - } - $groups[$group]['children'][] = $configurationData; - } - - foreach ($groups as $group) { - $configurations[] = $group; - } - - $event = new GenericEvent($this, [ - 'list' => $configurations, - ]); - $eventDispatcher->dispatch($event, AdminEvents::CLASS_SELECTOPTIONS_LIST_PRE_SEND_DATA); - - return $this->adminJson($configurations); + return $this->adminJson(ApiResponse::ok(['id' => $handler($payload)->id])); } - #[Route('/select-options-delete', name: 'selectoptionsdelete', methods: [Request::METHOD_DELETE])] - public function selectOptionsDeleteAction(Request $request): JsonResponse + #[IsGranted(CorePermission::Selectoptions->value)] + #[Route('/select-options-tree', name: 'selectoptionstree', methods: ['GET', 'POST'])] + public function selectOptionsTreeAction(GetSelectOptionsTreePayload $payload, GetSelectOptionsTreeHandler $handler): JsonResponse { - $this->checkPermission('selectoptions'); - - try { - $id = $request->request->get(DataObject\SelectOptions\Config::PROPERTY_ID); - $this->getSelectOptionsConfig($id)->delete(); - - return $this->adminJson(['success' => true]); - } catch (Exception $exception) { - return $this->adminJson(['success' => false, 'message' => $exception->getMessage()]); - } + return $this->adminJson($handler($payload)->configurations); } - protected function getSelectOptionsConfig(string $id): DataObject\SelectOptions\Config + #[IsGranted(CorePermission::Selectoptions->value)] + #[Route('/select-options-delete', name: 'selectoptionsdelete', methods: ['DELETE'])] + public function selectOptionsDeleteAction(DeleteSelectOptionsPayload $payload, DeleteSelectOptionsHandler $handler): JsonResponse { - $selectOptions = DataObject\SelectOptions\Config::getById($id); - if (!$selectOptions instanceof \OpenDxp\Model\DataObject\SelectOptions\Config) { - throw new NotFoundHttpException('Not Found', code: 1677133720896); - } + $handler($payload); - return $selectOptions; + return $this->adminJson(ApiResponse::ok()); } } diff --git a/src/Controller/Admin/DataObject/ClassificationstoreController.php b/src/Controller/Admin/DataObject/ClassificationstoreController.php index b092e927..5812383f 100644 --- a/src/Controller/Admin/DataObject/ClassificationstoreController.php +++ b/src/Controller/Admin/DataObject/ClassificationstoreController.php @@ -16,1481 +16,297 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Doctrine\DBAL\ArrayParameterType; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Db; -use OpenDxp\Helper\ArrayHelper; -use OpenDxp\Model\DataObject; -use OpenDxp\Model\DataObject\ClassDefinition\Data\LayoutDefinitionEnrichmentInterface; -use OpenDxp\Model\DataObject\Classificationstore; -use OpenDxp\Model\Translation; -use OpenDxp\Model\Translation\Listing; -use OpenDxp\Model\User; -use OpenDxp\Security\SecurityHelper; -use OpenDxp\Tool\Admin; -use stdClass; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddCollections\AddCollectionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddCollections\AddCollectionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddGroups\AddGroupsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddGroups\AddGroupsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddProperty\AddPropertyHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\AddProperty\AddPropertyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateCollection\CreateCollectionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateCollection\CreateCollectionPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateGroup\CreateGroupHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateGroup\CreateGroupPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateStore\CreateStoreHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\CreateStore\CreateStorePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteCollection\DeleteCollectionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteCollection\DeleteCollectionPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteCollectionRelation\DeleteCollectionRelationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteCollectionRelation\DeleteCollectionRelationPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteGroup\DeleteGroupHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteGroup\DeleteGroupPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteProperty\DeletePropertyHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteProperty\DeletePropertyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteRelation\DeleteRelationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\DeleteRelation\DeleteRelationPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\EditStore\EditStoreHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\EditStore\EditStorePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetCollectionRelations\GetCollectionRelationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetCollectionRelations\GetCollectionRelationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetCollections\GetCollectionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetCollections\GetCollectionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetGroups\GetGroupsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetGroups\GetGroupsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetPage\GetPageHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetPage\GetPagePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetProperties\GetPropertiesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetProperties\GetPropertiesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetRelations\GetRelationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetRelations\GetRelationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\GetStoreTree\GetStoreTreeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\ListStores\ListStoresHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SaveCollectionRelations\SaveCollectionRelationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SaveCollectionRelations\SaveCollectionRelationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SaveRelation\SaveRelationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SaveRelation\SaveRelationPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SearchRelations\SearchRelationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\SearchRelations\SearchRelationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateCollection\UpdateCollectionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateCollection\UpdateCollectionPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateGroup\UpdateGroupHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateGroup\UpdateGroupPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateProperty\UpdatePropertyHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Classificationstore\UpdateProperty\UpdatePropertyPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/classificationstore', name: 'opendxp_admin_dataobject_classificationstore_')] -class ClassificationstoreController extends AdminAbstractController implements KernelControllerEventInterface +class ClassificationstoreController extends AdminAbstractController { + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/delete-collection', name: 'deletecollection', methods: ['DELETE'])] - public function deleteCollectionAction(Request $request): JsonResponse + public function deleteCollectionAction(DeleteCollectionPayload $payload, DeleteCollectionHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); + $handler($payload); - $id = $request->request->getInt('id'); - - $configRelations = new Classificationstore\CollectionGroupRelation\Listing(); - $configRelations->setCondition('colId = ?', $id); - $list = $configRelations->load(); - foreach ($list as $item) { - $item->delete(); - } - - $config = Classificationstore\CollectionConfig::getById($id); - $config->delete(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/delete-collection-relation', name: 'deletecollectionrelation', methods: ['DELETE'])] - public function deleteCollectionRelationAction(Request $request): JsonResponse + public function deleteCollectionRelationAction(DeleteCollectionRelationPayload $payload, DeleteCollectionRelationHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $colId = $request->request->getInt('colId'); - $groupId = $request->request->getInt('groupId'); - - $config = new Classificationstore\CollectionGroupRelation(); - $config->setColId($colId); - $config->setGroupId($groupId); + $handler($payload); - $config->delete(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/delete-relation', name: 'deleterelation', methods: ['DELETE'])] - public function deleteRelationAction(Request $request): JsonResponse + public function deleteRelationAction(DeleteRelationPayload $payload, DeleteRelationHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $keyId = $request->request->getInt('keyId'); - $groupId = $request->request->getInt('groupId'); - - $config = new Classificationstore\KeyGroupRelation(); - $config->setKeyId($keyId); - $config->setGroupId($groupId); + $handler($payload); - $config->delete(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/delete-group', name: 'deletegroup', methods: ['DELETE'])] - public function deleteGroupAction(Request $request): JsonResponse + public function deleteGroupAction(DeleteGroupPayload $payload, DeleteGroupHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $id = $request->request->getInt('id'); + $handler($payload); - $config = Classificationstore\GroupConfig::getById($id); - $config->delete(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/create-group', name: 'creategroup', methods: ['POST'])] - public function createGroupAction(Request $request): JsonResponse + public function createGroupAction(CreateGroupPayload $payload, CreateGroupHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $name = SecurityHelper::convertHtmlSpecialChars($request->request->get('name')); - $storeId = $request->request->getInt('storeId'); - $config = Classificationstore\GroupConfig::getByName($name, $storeId); - - if (!$config) { - $config = new Classificationstore\GroupConfig(); - $config->setStoreId($storeId); - $config->setName($name); - $config->save(); + $result = $handler($payload); - return $this->adminJson(['success' => true, 'id' => $config->getName()]); + if ($result->alreadyExists) { + throw new BadRequestHttpException('classificationstore_error_group_exists_msg'); } - return $this->adminJson(['success' => false, 'id' => $config->getName(), 'message' => 'classificationstore_error_group_exists_msg']); + return $this->adminJson(ApiResponse::ok(['id' => $result->name])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/create-store', name: 'createstore', methods: ['POST'])] - public function createStoreAction(Request $request): JsonResponse + public function createStoreAction(CreateStorePayload $payload, CreateStoreHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $name = SecurityHelper::convertHtmlSpecialChars($request->request->get('name')); - - $config = Classificationstore\StoreConfig::getByName($name); - - if (!$config) { - $config = new Classificationstore\StoreConfig(); - $config->setName($name); - $config->save(); - } else { - throw new Exception('Store with the given name exists'); - } - - return $this->adminJson(['success' => true, 'storeId' => $config->getId()]); + return $this->adminJson(ApiResponse::ok(['storeId' => $handler($payload)->storeId])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/create-collection', name: 'createcollection', methods: ['POST'])] - public function createCollectionAction(Request $request): JsonResponse + public function createCollectionAction(CreateCollectionPayload $payload, CreateCollectionHandler $handler): JsonResponse { - $this->checkPermission('classificationstore'); - - $name = SecurityHelper::convertHtmlSpecialChars($request->request->get('name')); - $storeId = $request->request->getInt('storeId'); - $config = Classificationstore\CollectionConfig::getByName($name, $storeId); - - if (!$config) { - $config = new Classificationstore\CollectionConfig(); - $config->setName($name); - $config->setStoreId($storeId); - $config->save(); - } - - return $this->adminJson(['success' => true, 'id' => $config->getName()]); + return $this->adminJson(ApiResponse::ok(['id' => $handler($payload)->name])); } + #[IsGranted(CorePermission::Objects->value)] #[Route('/collections', name: 'collectionsactionget', methods: ['GET'])] - public function collectionsActionGet(Request $request): JsonResponse + public function collectionsActionGet(GetCollectionsPayload $payload, GetCollectionsHandler $handler): JsonResponse { - $this->checkPermission('objects'); - - $start = 0; - $limit = $request->query->get('limit') ? (int) $request->query->get('limit') : 15; - - $orderKey = 'name'; - $order = 'ASC'; - - if ($request->query->has('dir')) { - $order = $request->query->get('dir'); - } - - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $sortingSettings['orderKey']; - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - $storeIdFromDefinition = 0; - $allowedCollectionIds = []; - if ($oid = $request->query->get('oid')) { - $object = DataObject\Concrete::getById((int) $oid); - $class = $object->getClass(); - /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ - $fd = $class->getFieldDefinition($request->query->get('fieldname')); - $allowedGroupIds = $fd->getAllowedGroupIds(); - - if ($allowedGroupIds) { - $db = \OpenDxp\Db::get(); - $relationList = $db->fetchAllAssociative( - 'SELECT * FROM classificationstore_collectionrelations WHERE groupId IN (?)', - [$allowedGroupIds], - [ArrayParameterType::INTEGER] - ); - - foreach ($relationList as $item) { - $allowedCollectionIds[] = $item['colId']; - } - } - - $storeIdFromDefinition = $fd->getStoreId(); - } - - $list = new Classificationstore\CollectionConfig\Listing(); - - $list->setLimit($limit); - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($orderKey); - - $conditionParts = []; - $db = Db::get(); - - $searchfilter = $request->query->get('searchfilter'); - if ($searchfilter) { - $searchFilterConditions = []; + $result = $handler($payload); - $searchTerms = [$searchfilter, ...$this->getTranslatedSearchFilterTerms($searchfilter)]; - foreach ($searchTerms as $searchFilterTerm) { - $searchFilterConditions[] = 'name LIKE '.$db->quote('%'.$searchFilterTerm.'%').' OR description LIKE '.$db->quote('%'.$searchFilterTerm.'%'); - } - - $conditionParts[] = '('.implode(' OR ', $searchFilterConditions).')'; - } - - $storeId = $request->query->get('storeId'); - $storeId = $storeId ? (int) $storeId : $storeIdFromDefinition; - - $conditionParts[] = ' (storeId = ' . $db->quote($storeId) . ')'; - - if ($request->query->has('filter')) { - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - - if ($allowedCollectionIds) { - $conditionParts[] = ' id in (' . implode(',', $allowedCollectionIds) . ')'; - } - - $condition = implode(' AND ', $conditionParts); - - $list->setCondition($condition); - - $list->load(); - $configList = $list->getList(); - - $rootElement = []; - - $data = []; - foreach ($configList as $config) { - $name = $config->getName(); - if (!$name) { - $name = 'EMPTY'; - } - $item = [ - 'storeId' => $config->getStoreId(), - 'id' => $config->getId(), - 'name' => $name, - 'description' => $config->getDescription(), - ]; - if ($config->getCreationDate()) { - $item['creationDate'] = $config->getCreationDate(); - } - - if ($config->getModificationDate()) { - $item['modificationDate'] = $config->getModificationDate(); - } - - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); - - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/collections', name: 'collections', methods: ['POST', 'PUT'])] - public function collectionsAction(Request $request): JsonResponse + public function collectionsAction(UpdateCollectionPayload $payload, UpdateCollectionHandler $handler): JsonResponse { - if ($request->request->has('data')) { - $dataParam = $request->request->get('data'); - $data = $this->decodeJson($dataParam); - - $id = $data['id']; - $config = Classificationstore\CollectionConfig::getById($id); - - foreach ($data as $key => $value) { - if ($key !== 'id') { - $setter = 'set' . $key; - $config->$setter($value); - } - } - - $config->save(); - - return $this->adminJson(['success' => true, 'data' => $this->getConfigItem($config)]); + if (!$payload->hasData) { + throw new BadRequestHttpException(); } - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['data' => $handler($payload)->item])); } + #[IsGranted(CorePermission::Objects->value)] #[Route('/groups', name: 'groupsactionget', methods: ['GET'])] - public function groupsActionGet(Request $request): JsonResponse + public function groupsActionGet(GetGroupsPayload $payload, GetGroupsHandler $handler): JsonResponse { - $this->checkPermission('objects'); - - $start = 0; - $limit = 15; - $orderKey = 'name'; - $order = 'ASC'; - - if ($request->query->has('dir')) { - $order = $request->query->get('dir'); - } - - if ($request->query->has('sort')) { - $orderKey = $request->query->get('sort'); - } - - if ($request->query->has('limit')) { - $limit = (int) $request->query->get('limit'); - } - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $sortingSettings['orderKey']; - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - $list = new Classificationstore\GroupConfig\Listing(); - - $list->setLimit($limit); - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($orderKey); - - $conditionParts = []; - $db = Db::get(); - - if ($request->query->has('searchfilter')) { - $searchfilter = $request->query->get('searchfilter'); - $searchFilterConditions = []; - - $searchTerms = [$searchfilter, ...$this->getTranslatedSearchFilterTerms($searchfilter)]; - foreach ($searchTerms as $searchFilterTerm) { - $searchFilterConditions[] = 'name LIKE '.$db->quote('%'.$searchFilterTerm.'%').' OR description LIKE '.$db->quote('%'.$searchFilterTerm.'%'); - } - - $conditionParts[] = '('.implode(' OR ', $searchFilterConditions).')'; - } - - if ($storeId = $request->query->getInt('storeId')) { - $conditionParts[] = '(storeId = ' . $db->quote($storeId) . ')'; - } - - if ($request->query->has('filter')) { - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - - if ($request->query->has('oid')) { - $oid = $request->query->get('oid'); - $object = DataObject\Concrete::getById((int) $oid); - $class = $object->getClass(); - /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ - $fd = $class->getFieldDefinition($request->query->get('fieldname')); - $allowedGroupIds = $fd->getAllowedGroupIds(); - - if ($allowedGroupIds) { - $conditionParts[] = 'ID in (' . implode(',', $allowedGroupIds) . ')'; - } - } - - $condition = implode(' AND ', $conditionParts); - $list->setCondition($condition); - - $list->load(); - $configList = $list->getList(); - - $rootElement = []; - - $data = []; - foreach ($configList as $config) { - $name = $config->getName(); - if (!$name) { - $name = 'EMPTY'; - } - $item = [ - 'storeId' => $config->getStoreId(), - 'id' => $config->getId(), - 'name' => $name, - 'description' => $config->getDescription(), - ]; - if ($config->getCreationDate()) { - $item['creationDate'] = $config->getCreationDate(); - } + $result = $handler($payload); - if ($config->getModificationDate()) { - $item['modificationDate'] = $config->getModificationDate(); - } - - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); - - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/groups', name: 'groupsaction', methods: ['POST', 'PUT'])] - public function groupsAction(Request $request): JsonResponse + public function groupsAction(UpdateGroupPayload $payload, UpdateGroupHandler $handler): JsonResponse { - if ($request->request->has('data')) { - $dataParam = $request->request->get('data'); - $data = $this->decodeJson($dataParam); - - $id = $data['id']; - $config = Classificationstore\GroupConfig::getById($id); - - foreach ($data as $key => $value) { - if ($key !== 'id') { - $setter = 'set' . $key; - $config->$setter($value); - } - } - - $config->save(); - - return $this->adminJson(['success' => true, 'data' => $this->getConfigItem($config)]); + if (!$payload->hasData) { + throw new BadRequestHttpException(); } - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['data' => $handler($payload)->item])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/collection-relations', name: 'collectionrelationsget', methods: ['GET'])] - public function collectionRelationsGetAction(Request $request): JsonResponse + public function collectionRelationsGetAction(GetCollectionRelationsPayload $payload, GetCollectionRelationsHandler $handler): JsonResponse { - $mapping = ['groupName' => 'name', 'groupDescription' => 'description']; - - $start = 0; - $limit = 15; - $orderKey = 'sorter'; - $order = 'ASC'; - - if ($request->query->has('dir')) { - $order = $request->query->get('dir'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $sortingSettings['orderKey']; - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - if ($request->query->has('limit')) { - $limit = (int) $request->query->get('limit'); - } - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } - - $list = new Classificationstore\CollectionGroupRelation\Listing(); - - if ($limit > 0) { - $list->setLimit($limit); - } - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($mapping[$orderKey] ?? $orderKey); - $condition = ''; - - if ($request->query->has('filter')) { - $db = Db::get(); - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - - $count = 0; - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - if ($count > 0) { - $condition .= ' AND '; - } - $count++; - $fieldname = $mapping[$f->field]; - $condition .= $db->quoteIdentifier($fieldname) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - - $colId = $request->query->getInt('colId'); - if ($condition) { - $condition = '( ' . $condition . ' ) AND'; - } - $condition .= ' colId = ' . $list->quote($colId); - - $list->setCondition($condition); - - $listItems = $list->load(); - - $rootElement = []; - - $data = []; - foreach ($listItems as $config) { - $item = [ - 'colId' => $config->getColId(), - 'groupId' => $config->getGroupId(), - 'groupName' => $config->getName(), - 'groupDescription' => $config->getDescription(), - 'id' => $config->getColId() . '-' . $config->getGroupId(), - 'sorter' => $config->getSorter(), - ]; - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); + $result = $handler($payload); - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/collection-relations', name: 'collectionrelations', methods: ['POST', 'PUT'])] - public function collectionRelationsAction(Request $request): JsonResponse + public function collectionRelationsAction(SaveCollectionRelationsPayload $payload, SaveCollectionRelationsHandler $handler): JsonResponse { - if ($request->request->has('data')) { - $dataParam = $request->request->get('data'); - $data = $this->decodeJson($dataParam); - - if (count($data) === count($data, 1)) { - $data = [$data]; - } - - foreach ($data as &$row) { - $colId = $row['colId']; - $groupId = $row['groupId']; - $sorter = $row['sorter']; - - $config = new Classificationstore\CollectionGroupRelation(); - $config->setGroupId($groupId); - $config->setColId($colId); - $config->setSorter((int) $sorter); - - $config->save(); - - $row['id'] = $config->getColId() . '-' . $config->getGroupId(); - } - - return $this->adminJson(['success' => true, 'data' => $data]); + if (!$payload->hasData) { + throw new BadRequestHttpException(); } - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['data' => $handler($payload)->data])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/list-stores', name: 'liststores', methods: ['GET'])] - public function listStoresAction(): JsonResponse + public function listStoresAction(ListStoresHandler $handler): JsonResponse { - $storeConfigs = []; - $storeConfigListing = new Classificationstore\StoreConfig\Listing(); - $storeConfigListing->load(); - - foreach ($storeConfigListing as $storeConfig) { - $storeConfigs[] = $storeConfig->getObjectVars(); - } - - return $this->adminJson($storeConfigs); + return $this->adminJson($handler()->storeConfigs); } #[Route('/search-relations', name: 'searchrelations', methods: ['GET'])] - public function searchRelationsAction(Request $request): JsonResponse + public function searchRelationsAction(SearchRelationsPayload $payload, SearchRelationsHandler $handler): JsonResponse { - $db = Db::get(); - - $storeId = $request->query->get('storeId'); - - $mapping = [ - 'groupName' => DataObject\Classificationstore\GroupConfig\Dao::TABLE_NAME_GROUPS .'.name', - 'keyName' => DataObject\Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS .'.name', - 'keyDescription' => DataObject\Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS. '.description', - ]; - - $start = 0; - $limit = 15; - $orderKey = 'name'; - $order = 'ASC'; - - if ($request->query->get('dir')) { - $order = $request->query->get('dir'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $sortingSettings['orderKey']; - if ($orderKey === 'keyName') { - $orderKey = 'name'; - } - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - if ($request->query->has('limit')) { - $limit = (int) $request->query->get('limit'); - } - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } + $result = $handler($payload); - $list = new Classificationstore\KeyGroupRelation\Listing(); - - if ($limit > 0) { - $list->setLimit($limit); - } - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($orderKey); - - $conditionParts = []; - - if ($request->query->has('filter')) { - $db = Db::get(); - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - $fieldname = $mapping[$f->property]; - $conditionParts[] = $fieldname . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - - $conditionParts[] = ' groupId IN (select id from classificationstore_groups where storeId = ' . $db->quote($storeId) . ')'; - - $searchfilter = $request->query->get('searchfilter'); - if ($searchfilter) { - $searchFilterConditions = []; - - $searchTerms = [$searchfilter, ...$this->getTranslatedSearchFilterTerms($searchfilter)]; - foreach ($searchTerms as $searchFilterTerm) { - $searchFilterConditions[] = Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS.'.name LIKE '.$db->quote('%'.$searchFilterTerm.'%') - .' OR '.Classificationstore\GroupConfig\Dao::TABLE_NAME_GROUPS.'.name LIKE '.$db->quote('%'.$searchFilterTerm.'%') - .' OR '.Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS.'.description LIKE '.$db->quote('%'.$searchFilterTerm.'%'); - } - - $conditionParts[] = '('.implode(' OR ', $searchFilterConditions).')'; - } - - $condition = implode(' AND ', $conditionParts); - $list->setCondition($condition); - $list->setResolveGroupName(true); - - $rootElement = []; - - $data = []; - foreach ($list->getList() as $config) { - $item = [ - 'keyId' => $config->getKeyId(), - 'groupId' => $config->getGroupId(), - 'keyName' => $config->getName(), - 'keyDescription' => $config->getDescription(), - 'id' => $config->getGroupId() . '-' . $config->getKeyId(), - 'sorter' => $config->getSorter(), - ]; - - $groupConfig = Classificationstore\GroupConfig::getById($config->getGroupId()); - if ($groupConfig) { - $item['groupName'] = $groupConfig->getName(); - } - - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); - - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } #[Route('/relations', name: 'relationsactionget', methods: ['GET'])] - public function relationsActionGet(Request $request): JsonResponse + public function relationsActionGet(GetRelationsPayload $payload, GetRelationsHandler $handler): JsonResponse { - $mapping = ['keyName' => 'name', 'keyDescription' => 'description']; - - $start = 0; - $limit = 15; - $orderKey = 'name'; - $order = 'ASC'; - $relationIds = $request->query->get('relationIds'); - - if ($relationIds) { - $relationIds = json_decode($relationIds, true); - } + $result = $handler($payload); - if ($request->query->has('dir')) { - $order = $request->query->get('dir'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $mapping[$sortingSettings['orderKey']] ?? $sortingSettings['orderKey']; - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - if ($request->query->has('limit')) { - $limit = (int) $request->query->get('limit'); - } elseif (is_array($relationIds)) { - $limit = count($relationIds); - } - - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } - - $list = new Classificationstore\KeyGroupRelation\Listing(); - - if ($limit > 0) { - $list->setLimit($limit); - } - - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($orderKey); - $conditionParts = []; - - if ($request->query->has('filter')) { - $db = Db::get(); - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - $fieldname = $mapping[$f->field]; - $conditionParts[] = $db->quoteIdentifier($fieldname) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - - if (!$request->query->has('relationIds')) { - $groupId = $request->query->get('groupId'); - $conditionParts[] = ' groupId = ' . $list->quote($groupId); - } - - if ($relationIds) { - $relationParts = []; - - foreach ($relationIds as $relationId) { - $keyId = $relationId['keyId']; - $groupId = $relationId['groupId']; - $relationParts[] = '(keyId = ' . $list->quote($keyId) . ' AND groupId = ' . $list->quote($groupId) . ')'; - } - - $conditionParts[] = '(' . implode(' OR ', $relationParts) . ')'; - } - - $condition = implode(' AND ', $conditionParts); - - $list->setCondition($condition); - - $listItems = $list->load(); - - $rootElement = []; - - $data = []; - foreach ($listItems as $config) { - $type = $config->getType(); - $definition = json_decode($config->getDefinition(), true); - $definition = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); - DataObject\Service::enrichLayoutDefinition($definition); - - $item = [ - 'keyId' => $config->getKeyId(), - 'groupId' => $config->getGroupId(), - 'keyName' => $config->getName(), - 'keyDescription' => $config->getDescription(), - 'id' => $config->getGroupId() . '-' . $config->getKeyId(), - 'sorter' => $config->getSorter(), - 'layout' => $definition, - 'mandatory' => $config->isMandatory(), - ]; - - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); - - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/relations', name: 'relations', methods: ['POST', 'PUT'])] - public function relationsAction(Request $request): JsonResponse + public function relationsAction(SaveRelationPayload $payload, SaveRelationHandler $handler): JsonResponse { - if ($request->request->has('data')) { - $dataParam = $request->request->get('data'); - $data = $this->decodeJson($dataParam); - - $keyId = $data['keyId']; - $groupId = $data['groupId']; - $sorter = $data['sorter']; - $mandatory = $data['mandatory']; - - $config = new Classificationstore\KeyGroupRelation(); - $config->setGroupId((int) $groupId); - $config->setKeyId((int) $keyId); - $config->setSorter($sorter); - $config->setMandatory($mandatory); - - $config->save(); - $data['id'] = $config->getGroupId() . '-' . $config->getKeyId(); - - return $this->adminJson(['success' => true, 'data' => $data]); + if (!$payload->hasData) { + throw new BadRequestHttpException(); } - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['data' => $handler($payload)->data])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Objects->value)] #[Route('/add-collections', name: 'addcollections', methods: ['POST'])] - public function addCollectionsAction(Request $request): JsonResponse + public function addCollectionsAction(AddCollectionsPayload $payload, AddCollectionsHandler $handler): JsonResponse { - $this->checkPermission('objects'); - - $ids = $this->decodeJson($request->request->get('collectionIds')); - $data = []; - - if ($ids) { - $db = \OpenDxp\Db::get(); - $mappedData = []; - $groupsData = $db->fetchAllAssociative( - 'SELECT * FROM classificationstore_groups g, classificationstore_collectionrelations c - WHERE colId IN (?) AND g.id = c.groupId', - [array_values(array_filter($ids, is_numeric(...)))], - [ArrayParameterType::INTEGER] - ); - - foreach ($groupsData as $groupData) { - $mappedData[$groupData['id']] = $groupData; - } - - $groupIdList = []; - $groupId = null; - - $allowedGroupIds = null; - - $oid = $request->request->getInt('oid'); - $object = DataObject\Concrete::getById($oid); - if ($object) { - $class = $object->getClass(); - /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ - $fd = $class->getFieldDefinition($request->request->get('fieldname')); - $allowedGroupIds = $fd->getAllowedGroupIds(); - } - - foreach ($groupsData as $groupItem) { - $groupId = $groupItem['groupId']; - if (!$allowedGroupIds || in_array($groupId, $allowedGroupIds)) { - $groupIdList[] = $groupId; - } - } - - if ($groupIdList) { - $fieldname = $request->request->get('fieldname'); - $groupList = new Classificationstore\GroupConfig\Listing(); - $groupCondition = 'id in (' . implode(',', $groupIdList) . ')'; - $groupList->setCondition($groupCondition); - - $groupList = $groupList->load(); - - $keyCondition = 'groupId in (' . implode(',', $groupIdList) . ')'; - - $keyList = new Classificationstore\KeyGroupRelation\Listing(); - $keyList->setCondition($keyCondition); - $keyList->setOrderKey(['sorter', 'id']); - $keyList->setOrder(['ASC', 'ASC']); - $keyList = $keyList->load(); - - foreach ($groupList as $groupData) { - $data[$groupData->getId()] = [ - 'name' => $groupData->getName(), - 'id' => $groupData->getId(), - 'description' => $groupData->getDescription(), - 'keys' => [], - 'sorter' => (int) $mappedData[$groupData->getId()]['sorter'], - 'collectionId' => $mappedData[$groupId]['colId'], - ]; - } - - foreach ($keyList as $keyData) { - $groupId = $keyData->getGroupId(); - - $keyList = $data[$groupId]['keys']; - $type = $keyData->getType(); - $definition = json_decode($keyData->getDefinition(), true); - $definition = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); - - if (method_exists($definition, '__wakeup')) { - $definition->__wakeup(); - } - - $context['object'] = $object; - $context['class'] = $object ? $object->getClass() : null; - $context['ownerType'] = 'classificationstore'; - $context['ownerName'] = $fieldname; - $context['keyId'] = $keyData->getKeyId(); - $context['groupId'] = $groupId; - $context['keyDefinition'] = $definition; - - if ($definition instanceof LayoutDefinitionEnrichmentInterface) { - $definition = $definition->enrichLayoutDefinition($object, $context); - } - - $keyList[] = [ - 'name' => $keyData->getName(), - 'id' => $keyData->getKeyId(), - 'description' => $keyData->getDescription(), - 'definition' => $definition, - ]; - $data[$groupId]['keys'] = $keyList; - } - } - } - - return $this->adminJson($data); + return $this->adminJson($handler($payload)->data); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Objects->value)] #[Route('/add-groups', name: 'addgroups', methods: ['POST'])] - public function addGroupsAction(Request $request): JsonResponse + public function addGroupsAction(AddGroupsPayload $payload, AddGroupsHandler $handler): JsonResponse { - $this->checkPermission('objects'); - - $ids = $this->decodeJson($request->request->get('groupIds')); - $oid = $request->request->getInt('oid'); - $object = $oid === 0 ? null : DataObject\Concrete::getById($oid); - $fieldname = $request->request->get('fieldname'); - - $keyCondition = 'groupId in (' . implode(',', array_fill(0, count($ids), '?')) . ')'; - - $keyList = new Classificationstore\KeyGroupRelation\Listing(); - $keyList->setCondition($keyCondition, $ids); - $keyList->setOrderKey(['sorter', 'id']); - $keyList->setOrder(['ASC', 'ASC']); - $keyList = $keyList->load(); - - $groupCondition = 'id in (' . implode(',', array_fill(0, count($ids), '?')) . ')'; - - $groupList = new Classificationstore\GroupConfig\Listing(); - $groupList->setCondition($groupCondition, $ids); - $groupList->setOrder('ASC'); - $groupList->setOrderKey('id'); - $groupList = $groupList->load(); - - $data = []; - - foreach ($groupList as $groupData) { - $data[$groupData->getId()] = [ - 'name' => $groupData->getName(), - 'id' => $groupData->getId(), - 'description' => $groupData->getDescription(), - 'keys' => [], - ]; - } - - foreach ($keyList as $keyData) { - $groupId = $keyData->getGroupId(); - - $keyList = $data[$groupId]['keys']; - $type = $keyData->getType(); - $definition = json_decode($keyData->getDefinition(), true); - $definition = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); - - if (method_exists($definition, '__wakeup')) { - $definition->__wakeup(); - } - - $context['object'] = $object; - $context['class'] = $object ? $object->getClass() : null; - $context['ownerType'] = 'classificationstore'; - $context['ownerName'] = $fieldname; - $context['keyId'] = $keyData->getKeyId(); - $context['groupId'] = $groupId; - $context['keyDefinition'] = $definition; - - if ($definition instanceof LayoutDefinitionEnrichmentInterface) { - $definition = $definition->enrichLayoutDefinition($object, $context); - } - - $keyList[] = [ - 'name' => $keyData->getName(), - 'id' => $keyData->getKeyId(), - 'description' => $keyData->getDescription(), - 'definition' => $definition, - ]; - $data[$groupId]['keys'] = $keyList; - } - - return $this->adminJson($data); + return $this->adminJson($handler($payload)->data); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/properties', name: 'propertiesget', methods: ['GET'])] - public function propertiesGetAction(Request $request): JsonResponse + public function propertiesGetAction(GetPropertiesPayload $payload, GetPropertiesHandler $handler): JsonResponse { - $storeId = (int) $request->query->get('storeId'); - $frameName = $request->query->get('frameName'); - $db = \OpenDxp\Db::get(); - - $conditionParts = []; - - if ($frameName) { - $keyCriteria = ' FALSE '; - $frameConfig = Classificationstore\CollectionConfig::getByName($frameName, $storeId); - if ($frameConfig) { - // get all keys within that collection / frame - $frameId = $frameConfig->getId(); - $groupList = new Classificationstore\CollectionGroupRelation\Listing(); - $groupList->setCondition('colId = ' . $db->quote($frameId)); - $groupList = $groupList->load(); - $groupIdList = []; - foreach ($groupList as $groupEntry) { - $groupIdList[] = $groupEntry->getGroupId(); - } - - if ($groupIdList) { - $keyIdList = new Classificationstore\KeyGroupRelation\Listing(); - $keyIdList->setCondition('groupId in (' . implode(',', $groupIdList) . ')'); - $keyIdList = $keyIdList->load(); - if ($keyIdList) { - $keyIds = []; - foreach ($keyIdList as $keyEntry) { - $keyIds[] = $keyEntry->getKeyId(); - } - - $keyCriteria = ' id in (' . implode(',', $keyIds) . ')'; - } - } - } - - $conditionParts[] = $keyCriteria; - } - - $start = 0; - $limit = 15; - $orderKey = 'name'; - $order = 'ASC'; - - if ($request->query->has('dir')) { - $order = $request->query->get('dir'); - } - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $orderKey = $sortingSettings['orderKey']; - $order = $sortingSettings['order']; - } - - if ($request->query->getBoolean('overrideSort')) { - $orderKey = 'id'; - $order = 'DESC'; - } - - if ($request->query->has('limit')) { - $limit = (int) $request->query->get('limit'); - } - if ($request->query->has('start')) { - $start = (int) $request->query->get('start'); - } - - $list = new Classificationstore\KeyConfig\Listing(); - - if ($limit > 0 && !$request->query->get('groupIds') && !$request->query->get('keyIds')) { - $list->setLimit($limit); - } - $list->setOffset($start); - $list->setOrder($order); - $list->setOrderKey($orderKey); - - $searchfilter = $request->query->get('searchfilter'); - if ($searchfilter) { - $conditionParts[] = '(name LIKE ' . $db->quote('%' . $searchfilter . '%') . ' OR description LIKE ' . $db->quote('%'. $searchfilter . '%') . ')'; - } - - if ($storeId) { - $conditionParts[] = '(storeId = '. $db->quote($storeId) . ')'; - } - - if ($request->query->has('filter')) { - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - /** @var stdClass $f */ - foreach ($filters as $f) { - if (!isset($f->value)) { - continue; - } - - $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } - } - $condition = implode(' AND ', $conditionParts); - $list->setCondition($condition); - - if ($request->query->get('groupIds') || $request->query->get('keyIds')) { - $db = Db::get(); - - if ($request->query->get('groupIds')) { - $ids = $this->decodeJson($request->query->get('groupIds')); - $col = 'group'; - } else { - $ids = $this->decodeJson($request->query->get('keyIds')); - $col = 'id'; - } - - $condition = $db->quoteIdentifier($col) . ' IN ('; - $count = 0; - foreach ($ids as $theId) { - if ($count > 0) { - $condition .= ','; - } - $condition .= $theId; - $count++; - } - - $condition .= ')'; - $list->setCondition($condition); - } - - $list->load(); - $configList = $list->getList(); - - $rootElement = []; + $result = $handler($payload); - $data = []; - foreach ($configList as $config) { - $item = $this->getKeyConfigItem($config); - $data[] = $item; - } - $rootElement['data'] = $data; - $rootElement['success'] = true; - $rootElement['total'] = $list->getTotalCount(); - - return $this->adminJson($rootElement); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/properties', name: 'properties', methods: ['POST', 'PUT'])] - public function propertiesAction(Request $request): JsonResponse - { - if ($request->request->has('data')) { - $dataParam = $request->request->get('data'); - $data = $this->decodeJson($dataParam); - - $id = $data['id']; - $config = Classificationstore\KeyConfig::getById($id); - - foreach ($data as $key => $value) { - if ($key !== 'id') { - $setter = 'set' . $key; - if (method_exists($config, $setter)) { - $config->$setter($value); - } - } - } - - $config->save(); - $item = $this->getKeyConfigItem($config); - - return $this->adminJson(['success' => true, 'data' => $item]); - } - - return $this->adminJson(['success' => false]); - } - - protected function getConfigItem(Classificationstore\KeyConfig|Classificationstore\CollectionConfig|Classificationstore\GroupConfig $config): array + public function propertiesAction(UpdatePropertyPayload $payload, UpdatePropertyHandler $handler): JsonResponse { - $name = $config->getName(); - - $item = [ - 'storeId' => $config->getStoreId(), - 'id' => $config->getId(), - 'name' => $name, - 'description' => $config->getDescription(), - ]; - - if ($config->getCreationDate()) { - $item['creationDate'] = $config->getCreationDate(); - } - - if ($config->getModificationDate()) { - $item['modificationDate'] = $config->getModificationDate(); - } - - return $item; - } - - protected function getKeyConfigItem(Classificationstore\KeyConfig $config): array - { - $item = $this->getConfigItem($config); - $item['type'] = $config->getType() ?: 'input'; - $definition = $config->getDefinition(); - $item['definition'] = $definition; - - if ($definition) { - $definition = json_decode($definition, true); - if ($definition) { - $item['title'] = $definition['title']; - } + if (!$payload->hasData) { + throw new BadRequestHttpException(); } - return $item; + return $this->adminJson(ApiResponse::ok(['data' => $handler($payload)->item])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/add-property', name: 'addproperty', methods: ['POST'])] - public function addPropertyAction(Request $request): JsonResponse + public function addPropertyAction(AddPropertyPayload $payload, AddPropertyHandler $handler): JsonResponse { - $name = $request->request->get('name'); - $storeId = $request->request->getInt('storeId'); - - $definition = [ - 'fieldtype' => 'input', - 'name' => $name, - 'title' => $name, - 'datatype' => 'data', - ]; - - $config = new Classificationstore\KeyConfig(); - $config->setName($name); - $config->setTitle($name); - $config->setType('input'); - $config->setStoreId($storeId); - $config->setEnabled(true); - $config->setDefinition(json_encode($definition)); - $config->save(); - - return $this->adminJson(['success' => true, 'id' => $config->getName()]); + return $this->adminJson(ApiResponse::ok(['id' => $handler($payload)->name])); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/delete-property', name: 'deleteproperty', methods: ['DELETE'])] - public function deletePropertyAction(Request $request): JsonResponse + public function deletePropertyAction(DeletePropertyPayload $payload, DeletePropertyHandler $handler): JsonResponse { - $id = $request->request->getInt('id'); + $handler($payload); - $config = Classificationstore\KeyConfig::getById($id); - // $config->delete(); - $config->setEnabled(false); - $config->save(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/edit-store', name: 'editstore', methods: ['PUT'])] - public function editStoreAction(Request $request): JsonResponse + public function editStoreAction(EditStorePayload $payload, EditStoreHandler $handler): JsonResponse { - $id = $request->request->getInt('id'); - $data = json_decode($request->request->get('data'), true); - $name = $data['name']; - if (!$name) { - throw new Exception('Name must not be empty'); - } - - $description = $data['description']; - - $config = Classificationstore\StoreConfig::getByName($name); - if ($config && $config->getId() != $id) { - throw new Exception('There is already a config with the same name'); - } - - $config = Classificationstore\StoreConfig::getById($id); - - if (!$config) { - throw new Exception('Configuration does not exist'); - } + $handler($payload); - $config->setName($name); - $config->setDescription($description); - $config->save(); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/storetree', name: 'storetree', methods: ['GET'])] - public function storetreeAction(Request $request): JsonResponse + public function storetreeAction(GetStoreTreeHandler $handler): JsonResponse { - $result = []; - $list = new Classificationstore\StoreConfig\Listing(); - $list = $list->load(); - foreach ($list as $item) { - $resultItem = [ - 'id' => $item->getId(), - 'text' => htmlspecialchars($item->getName() ?? '', ENT_QUOTES), - 'expandable' => false, - 'leaf' => true, - 'expanded' => true, - 'description' => htmlspecialchars($item->getDescription() ?? '', ENT_QUOTES), - 'iconCls' => 'opendxp_icon_classificationstore', - ]; - - $resultItem['qtitle'] = 'ID: ' . $item->getId(); - - if ($item->getDescription()) { - } - $resultItem['qtip'] = $item->getDescription() ? htmlspecialchars($item->getDescription(), ENT_QUOTES) : ' '; - $result[] = $resultItem; - } - - return $this->adminJson($result); + return $this->adminJson($handler()->items); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/get-page', name: 'getpage', methods: ['GET'])] - public function getPageAction(Request $request): JsonResponse - { - $tableSuffix = $request->query->get('table'); - if (!ArrayHelper::inArrayCaseInsensitive($tableSuffix, ['keys', 'groups'])) { - $tableSuffix = 'keys'; - } - - $table = 'classificationstore_' . $tableSuffix; - $db = \OpenDxp\Db::get(); - $id = (int) $request->query->get('id'); - $storeId = (int) $request->query->get('storeId'); - $pageSize = (int) $request->query->get('pageSize'); - - if ($request->query->get('sortKey')) { - $sortKey = $request->query->get('sortKey'); - $sortDir = $request->query->get('sortDir'); - } else { - $sortKey = 'name'; - $sortDir = 'ASC'; - } - - if (!ArrayHelper::inArrayCaseInsensitive($sortDir, ['DESC', 'ASC'])) { - $sortDir = 'DESC'; - } - - if (!ArrayHelper::inArrayCaseInsensitive($sortKey, ['name', 'title', 'description', 'id', 'type', 'creationDate', 'modificationDate', 'enabled', 'parentId', 'storeId'])) { - $sortKey = 'name'; - } - - $sorter = ' order by `' . $sortKey . '` ' . $sortDir; - - if ($table === 'keys') { - $query = ' - select *, (item.pos - 1)/ ' . $pageSize . ' + 1 as page from ( - select * from ( - select @rownum := @rownum + 1 as pos, id, name, `type` - from `' . $table . '` - where enabled = 1 and storeId = ' . $storeId . $sorter . ' - ) all_rows) item where id = ' . $id . ';'; - } else { - $query = ' - select *, (item.pos - 1)/ ' . $pageSize . ' + 1 as page from ( - select * from ( - select @rownum := @rownum + 1 as pos, id, name - from `' . $table . '` - where storeId = ' . $storeId . $sorter . ' - ) all_rows) item where id = ' . $id . ';'; - } - - $db->executeStatement('SET @rownum = 0'); - $result = $db->fetchAllAssociative($query); - - $page = (int) $result[0]['page'] ; - - return $this->adminJson(['success' => true, 'page' => $page]); - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $unrestrictedActions = [ - 'collectionsActionGet', - 'groupsActionGet', - 'relationsActionGet', - 'addGroupsAction', - 'addCollectionsAction', - 'searchRelationsAction', - ]; - $this->checkActionPermission($event, 'classificationstore', $unrestrictedActions); - } - - /** - * @return string[] - */ - private function getTranslatedSearchFilterTerms(string $searchTerm): array + public function getPageAction(GetPagePayload $payload, GetPageHandler $handler): JsonResponse { - $terms = []; - - $user = Admin::getCurrentUser(); - if ($user instanceof User) { - $translationListing = new Listing(); - $translationListing->setDomain(Translation::DOMAIN_ADMIN); - $translationListing->setCondition( - $translationListing->quoteIdentifier('language') . ' = ? AND ' . - $translationListing->quoteIdentifier('text') . ' LIKE ?', - [ - $user->getLanguage(), - '%' . $searchTerm . '%', - ] - ); - - foreach ($translationListing as $translation) { - $terms[] = $translation->getKey(); - } - } - - return $terms; + return $this->adminJson(ApiResponse::ok(['page' => $handler($payload)->page])); } } diff --git a/src/Controller/Admin/DataObject/CustomLayoutController.php b/src/Controller/Admin/DataObject/CustomLayoutController.php new file mode 100644 index 00000000..389a1f1b --- /dev/null +++ b/src/Controller/Admin/DataObject/CustomLayoutController.php @@ -0,0 +1,144 @@ +value)] +class CustomLayoutController extends AdminAbstractController +{ + #[Route('/get-custom-layout', name: 'getcustomlayout', methods: ['GET'])] + public function getCustomLayoutAction(GetCustomLayoutHandler $getCustomLayout, GetCustomLayoutPayload $payload): JsonResponse + { + $result = $getCustomLayout($payload); + $data = $result->data; + $data['isWriteable'] = $result->isWriteable; + + return $this->adminJson(ApiResponse::ok(['data' => $data])); + } + + #[Route('/add-custom-layout', name: 'addcustomlayout', methods: ['POST'])] + public function addCustomLayoutAction(AddCustomLayoutHandler $addCustomLayout, AddCustomLayoutPayload $payload): JsonResponse + { + $customLayout = $addCustomLayout($payload); + + $data = $customLayout->getObjectVars(); + $data['isWriteable'] = $customLayout->isWriteable(); + + return $this->adminJson(ApiResponse::ok(['id' => $customLayout->getId(), 'name' => $customLayout->getName(), 'data' => $data])); + } + + #[Route('/save-custom-layout', name: 'savecustomlayout', methods: ['PUT'])] + public function saveCustomLayoutAction(SaveCustomLayoutHandler $saveCustomLayout, SaveCustomLayoutPayload $payload): JsonResponse + { + $customLayout = $saveCustomLayout($payload); + + return $this->adminJson(ApiResponse::ok(['id' => $customLayout->getId(), 'data' => $customLayout->getObjectVars()])); + } + + #[Route('/delete-custom-layout', name: 'deletecustomlayout', methods: ['DELETE'])] + public function deleteCustomLayoutAction(DeleteCustomLayoutHandler $deleteCustomLayout, StringIdBodyPayload $payload): JsonResponse + { + $deleteCustomLayout($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/import-custom-layout-definition', name: 'importcustomlayoutdefinition', methods: ['POST', 'PUT'])] + public function importCustomLayoutDefinitionAction( + ImportCustomLayoutHandler $importCustomLayout, + ImportCustomLayoutPayload $payload, + ): Response { + if ($payload->nameAlreadyInUse) { + $response = $this->adminJson(ApiResponse::error(null, ['nameAlreadyInUse' => true])); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + $importCustomLayout($payload); + + $response = $this->adminJson(ApiResponse::ok()); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/export-custom-layout-definition', name: 'exportcustomlayoutdefinition', methods: ['GET'])] + public function exportCustomLayoutDefinitionAction(ExportCustomLayoutHandler $exportCustomLayout, ExportCustomLayoutPayload $payload): Response + { + $result = $exportCustomLayout($payload); + + $response = new Response($result->json); + $response->headers->set('Content-type', 'application/json'); + $response->headers->set('Content-Disposition', 'attachment; filename: "custom_definition_' . $result->name . '_export.json"'); + + return $response; + } + + #[Route('/get-custom-layout-definitions', name: 'getcustomlayoutdefinitions', methods: ['GET'])] + public function getCustomLayoutDefinitionsAction(GetCustomLayoutDefinitionsHandler $getDefinitions, GetCustomLayoutDefinitionsPayload $payload): JsonResponse + { + return $this->adminJson(ApiResponse::ok(['data' => $getDefinitions($payload)->definitions])); + } + + #[Route('/get-all-layouts', name: 'getalllayouts', methods: ['GET'])] + public function getAllLayoutsAction(GetAllLayoutsHandler $getAllLayouts): JsonResponse + { + return $this->adminJson(['data' => $getAllLayouts()->layouts]); + } + + #[Route('/suggest-custom-layout-identifier', name: 'suggestcustomlayoutidentifier', methods: ['GET'])] + public function suggestCustomLayoutIdentifierAction(SuggestCustomLayoutIdentifierHandler $suggestIdentifier, SuggestCustomLayoutIdentifierPayload $payload): Response + { + $result = $suggestIdentifier($payload); + + return $this->adminJson([ + 'suggestedIdentifier' => $result->suggestedIdentifier, + 'existingIds' => $result->existingIds, + 'existingNames' => $result->existingNames, + ]); + } +} diff --git a/src/Controller/Admin/DataObject/DataObjectController.php b/src/Controller/Admin/DataObject/DataObjectController.php index 313c2749..f05eb46f 100644 --- a/src/Controller/Admin/DataObject/DataObjectController.php +++ b/src/Controller/Admin/DataObject/DataObjectController.php @@ -13,60 +13,63 @@ * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) */ +declare(strict_types=1); + namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\Admin\ElementControllerBase; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\AdminStyleTrait; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\ApplySchedulerDataTrait; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\UserNameTrait; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Event\ElementAdminStyleEvent; -use OpenDxp\Bundle\AdminBundle\Helper\GridHelperService; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\AddObjectFolder\AddObjectFolderHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\AddObjectFolder\AddObjectFolderPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\AddObject\AddObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\AddObject\AddObjectPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ChangeChildrenSortBy\ChangeChildrenSortByHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\ChangeChildrenSortBy\ChangeChildrenSortByPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\DataObjectGridProxy\DataObjectGridProxyHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\DataObjectGridProxy\DataObjectGridProxyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\DeleteDataObject\DeleteDataObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\DeleteDataObject\DeleteDataObjectPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObject\GetDataObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObject\GetDataObjectPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetSelectOptions\GetSelectOptionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetSelectOptions\GetSelectOptionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\SaveDataObjectFolder\SaveDataObjectFolderHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\SaveDataObjectFolder\SaveDataObjectFolderPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetChildrenById\TreeGetChildrenByIdHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetChildrenById\TreeGetChildrenByIdPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\UpdateDataObject\UpdateDataObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\UpdateDataObject\UpdateDataObjectPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObjectFolder\GetDataObjectFolderHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetIdPathPagingInfo\GetIdPathPagingInfoHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetIdPathPagingInfo\GetIdPathPagingInfoPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObjectPreviewUrl\GetDataObjectPreviewUrlHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObjectPreviewUrl\GetDataObjectPreviewUrlPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\SaveDataObject\SaveDataObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\SaveDataObject\SaveDataObjectPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Service\Element\SessionService; +use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; use OpenDxp\Bundle\AdminBundle\Security\CsrfProtectionHandler; -use OpenDxp\Controller\KernelControllerEventInterface; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Exception\ElementLockedException; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; +use Symfony\Component\Security\Http\Attribute\IsGranted; use OpenDxp\Controller\Traits\ElementEditLockHelperTrait; -use OpenDxp\Db; -use OpenDxp\Localization\LocaleServiceInterface; -use OpenDxp\Logger; -use OpenDxp\Model; -use OpenDxp\Model\DataObject; -use OpenDxp\Model\DataObject\ClassDefinition\Data\ManyToManyObjectRelation; -use OpenDxp\Model\DataObject\ClassDefinition\Data\Relations\AbstractRelations; -use OpenDxp\Model\DataObject\ClassDefinition\Data\ReverseObjectRelation; -use OpenDxp\Model\DataObject\ClassDefinition\Helper\OptionsProviderResolver; -use OpenDxp\Model\DataObject\ClassDefinition\PreviewGeneratorInterface; -use OpenDxp\Model\Element; use OpenDxp\Model\Element\ElementInterface; -use OpenDxp\Model\Element\Service; -use OpenDxp\Model\Schedule\Task; -use OpenDxp\Tool; use Override; -use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Throwable; -use Twig\Environment; -use Twig\Extension\CoreExtension; /** * @internal */ #[Route('/object', name: 'opendxp_admin_dataobject_dataobject_')] -class DataObjectController extends ElementControllerBase implements KernelControllerEventInterface +#[IsGranted(CorePermission::Objects->value)] +class DataObjectController extends ElementControllerBase { - use AdminStyleTrait; use ElementEditLockHelperTrait; - use ApplySchedulerDataTrait; - use DataObjectActionsTrait; - use UserNameTrait; /** On active edit lock answer with editlock response */ public const string TASK_RESPONSE = 'response'; @@ -77,163 +80,36 @@ class DataObjectController extends ElementControllerBase implements KernelContro /** On active edit lock keep existing entry */ public const string TASK_KEEP = 'keep'; - protected DataObject\Service $_objectService; - - private array $objectData = []; - - private array $metaData = []; - - private array $classFieldDefinitions = []; + public function __construct( + ElementServiceInterface $elementService, + private readonly SessionService $sessionService, + ) { + parent::__construct($elementService); + } #[Route('/tree-get-children-by-id', name: 'treegetchildrenbyid', methods: ['GET'])] - public function treeGetChildrenByIdAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $filter = $request->query->get('filter'); - $object = DataObject::getById((int) $request->query->get('node')); - $objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER]; - $objects = []; - $cv = []; - $offset = $total = $limit = $filteredTotalCount = 0; - - if ($object instanceof DataObject\Concrete) { - $class = $object->getClass(); - if ($class->getShowVariants()) { - $objectTypes = DataObject::$types; - } - } - - if ($object->hasChildren($objectTypes)) { - $offset = (int)$request->query->get('start'); - $limit = (int)$request->query->get('limit', '100000000'); - if ($view = $request->query->get('view', '')) { - $cv = $this->elementService->getCustomViewById($view) ?? []; - } - - if (!is_null($filter)) { - if (!str_ends_with($filter, '*')) { - $filter .= '*'; - } - $filter = str_replace('*', '%', $filter); - $limit = 100; - } - - $childrenList = new DataObject\Listing(); - $childrenList->setCondition($this->buildChildrenCondition($object, $filter, (string)$view)); - $childrenList->setLimit($limit); - $childrenList->setOffset($offset); - - if ($object->getChildrenSortBy() === 'index') { - $childrenList->setOrderKey('objects.index ASC', false); - } else { - $childrenList->setOrderKey( - sprintf( - 'CAST(objects.%s AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci %s', - $object->getChildrenSortBy(), $object->getChildrenSortOrder() - ), - false - ); - } - $childrenList->setObjectTypes($objectTypes); - - Element\Service::addTreeFilterJoins($cv, $childrenList); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => $request->query->all(), - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); - - /** @var DataObject\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - $children = $childrenList->load(); - $filteredTotalCount = $childrenList->getTotalCount(); - - foreach ($children as $child) { - $objectTreeNode = $this->getTreeNodeConfig($child); - // this if is obsolete since as long as the change with #11714 about list on line 175-179 are working fine, we already filter the list=1 there - if ($objectTreeNode['permissions']['list'] == 1) { - $objects[] = $objectTreeNode; - } - } - - //pagination for custom view - $total = $cv - ? $filteredTotalCount - : $object->getChildAmount(null, $this->getAdminUser()); - } - - //Hook for modifying return value - e.g. for changing permissions based on object data - //data need to wrapped into a container in order to pass parameter to event listeners by reference so that they can change the values - $event = new GenericEvent($this, [ - 'objects' => $objects, - ]); - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); - - $objects = $event->getArgument('objects'); + public function treeGetChildrenByIdAction( + TreeGetChildrenByIdPayload $payload, + TreeGetChildrenByIdHandler $handler, + ): JsonResponse { + $result = $handler($payload); - if ($limit) { + if ($result->limit) { return $this->adminJson([ - 'offset' => $offset, - 'limit' => $limit, - 'total' => $total, - 'overflow' => !is_null($filter) && ($filteredTotalCount > $limit), - 'nodes' => $objects, - 'fromPaging' => (int)$request->query->get('fromPaging'), - 'filter' => $request->query->get('filter') ?: '', - 'inSearch' => (int)$request->query->get('inSearch'), + 'offset' => $result->offset, + 'limit' => $result->limit, + 'total' => $result->total, + 'overflow' => !is_null($result->filter) && ($result->filteredTotalCount > $result->limit), + 'nodes' => $result->objects, + 'fromPaging' => $result->fromPaging, + 'filter' => $result->filter ?: '', + 'inSearch' => $payload->inSearch, ]); } - return $this->adminJson($objects); + return $this->adminJson($result->objects); } - private function buildChildrenCondition(DataObject\AbstractObject $object, ?string $filter, ?string $view): string - { - $condition = "objects.parentId = '" . $object->getId() . "'"; - - // custom views start - if ($view) { - $cv = $this->elementService->getCustomViewById($view); - - if (!empty($cv['classes'])) { - $cvConditions = []; - $cvClasses = $cv['classes']; - foreach ($cvClasses as $key => $cvClass) { - $cvConditions[] = "objects.classId = '" . $key . "'"; - } - - $cvConditions[] = "objects.type = 'folder'"; - $condition .= ' AND (' . implode(' OR ', $cvConditions) . ')'; - } - } - // custom views end - - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $currentUserId = $this->getAdminUser()->getId(); - $userIds[] = $currentUserId; - - $inheritedPermission = $object->getDao()->isInheritingPermission('list', $userIds); - - $anyAllowedRowOrChildren = 'EXISTS(SELECT list FROM users_workspaces_object uwo WHERE userId IN (' . implode(',', $userIds) . ') AND list=1 AND LOCATE(CONCAT(objects.path,objects.key),cpath)=1 AND - NOT EXISTS(SELECT list FROM users_workspaces_object WHERE userId =' . $currentUserId . ' AND list=0 AND cpath = uwo.cpath))'; - $isDisallowedCurrentRow = 'EXISTS(SELECT list FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') AND cid = objects.id AND list=0)'; - - $condition .= ' AND IF(' . $anyAllowedRowOrChildren . ',1,IF(' . $inheritedPermission . ', ' . $isDisallowedCurrentRow . ' = 0, 0)) = 1'; - } - - if (!is_null($filter)) { - $db = Db::get(); - $condition .= ' AND CAST(objects.key AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ' . $db->quote($filter); - } - - return $condition; - } - - /** - * @throws Exception - */ #[Override] protected function getTreeNodeConfig(ElementInterface $element): array { @@ -241,1758 +117,182 @@ protected function getTreeNodeConfig(ElementInterface $element): array } #[Route('/get-id-path-paging-info', name: 'getidpathpaginginfo', methods: ['GET'])] - public function getIdPathPagingInfoAction(Request $request): JsonResponse + public function getIdPathPagingInfoAction(GetIdPathPagingInfoHandler $handler, GetIdPathPagingInfoPayload $payload): JsonResponse { - $path = $request->query->get('path'); - $pathParts = explode('/', $path); - $id = (int) array_pop($pathParts); - - $limit = $request->query->get('limit'); - - if (empty($limit)) { - $limit = 30; + if ($payload->path === null) { + return $this->adminJson(['success' => false]); } - $data = []; - - $targetObject = DataObject::getById($id); - $object = $targetObject; - - while ($parent = $object->getParent()) { - $list = new DataObject\Listing(); - $list->setCondition('parentId = ?', $parent->getId()); - $list->setUnpublished(true); - $total = $list->getTotalCount(); - - $info = [ - 'total' => $total, - ]; + $result = $handler($payload); - if ($total > $limit) { - $idList = $list->loadIdList(); - $position = array_search($object->getId(), $idList); - $info['position'] = $position + 1; - $info['page'] = ceil($info['position'] / $limit); - } - - $data[$parent->getId()] = $info; - - $object = $parent; - } - - return $this->adminJson($data); + return $this->adminJson($result->data); } - /** - * @throws Exception - */ #[Route('/get', name: 'get', methods: ['GET'])] - public function getAction(Request $request, EventDispatcherInterface $eventDispatcher, PreviewGeneratorInterface $defaultPreviewGenerator): JsonResponse - { - $objectId = $request->query->getInt('id'); - $objectFromDatabase = DataObject\Concrete::getById($objectId); - - if (!$objectFromDatabase instanceof \OpenDxp\Model\DataObject\Concrete) { - return $this->adminJson(['success' => false, 'message' => 'element_not_found'], JsonResponse::HTTP_NOT_FOUND); - } - - $objectFromDatabase = clone $objectFromDatabase; - - // set the latest available version for editmode - $draftVersion = null; - $object = $this->getLatestVersion($objectFromDatabase, $draftVersion); - - // check for lock - if ($object->isAllowed('save') || $object->isAllowed('publish') || $object->isAllowed('unpublish') || $object->isAllowed('delete')) { - if (Element\Editlock::isLocked($objectId, 'object', $request->getSession()->getId())) { - //Hook for modifying editlock handling - e.g. no editLockResponse but keep old lock - $lockData = [ - 'task' => self::TASK_RESPONSE, - ]; - $event = new GenericEvent($this, [ - 'data' => $lockData, - 'object' => $object, - ]); - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_GET_IS_LOCKED); - $lockData = $event->getArgument('data'); - - if ($lockData['task'] === self::TASK_RESPONSE) { - return $this->getEditLockResponse($objectId, 'object'); - } - - if ($lockData['task'] === self::TASK_OVERWRITE) { - Element\Editlock::lock($objectId, 'object', $request->getSession()->getId()); - } - } else { - Element\Editlock::lock($objectId, 'object', $request->getSession()->getId()); - } - } - - // we need to know if the latest version is published or not (a version), because of lazy loaded fields in $this->getDataForObject() - $objectFromVersion = $object !== $objectFromDatabase; - - if ($object->isAllowed('view')) { - $objectData = []; - - /** ------------------------------------------------------------- - * Load some general data from published object (if existing) - * ------------------------------------------------------------- */ - $objectData['idPath'] = Element\Service::getIdPath($objectFromDatabase); - - $linkGeneratorReference = $objectFromDatabase->getClass()->getLinkGeneratorReference(); - $previewGenerator = $objectFromDatabase->getClass()->getPreviewGenerator(); - if (empty($previewGenerator) && !empty($linkGeneratorReference)) { - $previewGenerator = $defaultPreviewGenerator; - } - - $objectData['hasPreview'] = false; - if ($linkGeneratorReference || $previewGenerator) { - $objectData['hasPreview'] = true; - } - - if ($draftVersion instanceof Model\Version && $objectFromDatabase->getModificationDate() < $draftVersion->getDate()) { - $objectData['draft'] = [ - 'id' => $draftVersion->getId(), - 'modificationDate' => $draftVersion->getDate(), - 'isAutoSave' => $draftVersion->isAutoSave(), - ]; - } - - $objectData['general'] = []; - - $allowedKeys = ['published', 'key', 'id', 'creationDate', 'classId', 'className', 'type', 'parentId', 'userOwner', 'userModification']; - foreach ($objectFromDatabase->getObjectVars() as $key => $value) { - if (in_array($key, $allowedKeys)) { - $objectData['general'][$key] = $value; - } - } - $objectData['general']['classTitle'] = $objectFromDatabase->getClass()->getTitle() ?: $objectFromDatabase->getClassName(); - $objectData['general']['fullpath'] = $objectFromDatabase->getRealFullPath(); - $objectData['general']['locked'] = $objectFromDatabase->isLocked(); - $objectData['general']['php'] = [ - 'classes' => [$objectFromDatabase::class, ...array_values(class_parents($objectFromDatabase))], - 'interfaces' => array_values(class_implements($objectFromDatabase)), - ]; - $objectData['general']['allowInheritance'] = $objectFromDatabase->getClass()->getAllowInherit(); - $objectData['general']['allowVariants'] = $objectFromDatabase->getClass()->getAllowVariants(); - $objectData['general']['showVariants'] = $objectFromDatabase->getClass()->getShowVariants(); - $objectData['general']['showAppLoggerTab'] = $objectFromDatabase->getClass()->getShowAppLoggerTab(); - $objectData['general']['showFieldLookup'] = $objectFromDatabase->getClass()->getShowFieldLookup(); - $objectData['general']['linkGeneratorReference'] = $linkGeneratorReference; - - if ($previewGenerator) { - $objectData['general']['previewConfig'] = $previewGenerator->getPreviewConfig($objectFromDatabase); - } - - $objectData['layout'] = $objectFromDatabase->getClass()->getLayoutDefinitions(); - $objectData['userPermissions'] = $objectFromDatabase->getUserPermissions($this->getAdminUser()); - $objectVersions = Element\Service::getSafeVersionInfo($objectFromDatabase->getVersions()); - $objectData['versions'] = array_splice($objectVersions, -1, 1); - $objectData['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $objectFromDatabase->getScheduledTasks() - ); - - $objectData['childdata']['id'] = $objectFromDatabase->getId(); - $objectData['childdata']['data']['classes'] = $this->prepareChildClasses($objectFromDatabase->getDao()->getClasses()); - $objectData['childdata']['data']['general'] = $objectData['general']; - - /** ------------------------------------------------------------- - * Load remaining general data from latest version - * ------------------------------------------------------------- */ - $allowedKeys = ['modificationDate', 'userModification']; - foreach ($object->getObjectVars() as $key => $value) { - if (in_array($key, $allowedKeys)) { - $objectData['general'][$key] = $value; - } - } - - try { - $this->getDataForObject($object, $objectFromVersion); - } catch (Throwable) { - $object = $objectFromDatabase; - $this->getDataForObject($object, false); - } - - $objectData['data'] = $this->objectData; - $objectData['metaData'] = $this->metaData; - $objectData['properties'] = Element\Service::minimizePropertiesForEditmode($object->getProperties()); - - // this used for the "this is not a published version" hint - // and for adding the published icon to version overview - $objectData['general']['versionDate'] = $objectFromDatabase->getModificationDate(); - $objectData['general']['versionCount'] = $objectFromDatabase->getVersionCount(); - - $userOwnerName = $this->getUserName($objectData['general']['userOwner']); - $userModificationName = ($objectData['general']['userOwner'] == $objectData['general']['userModification']) ? $userOwnerName : $this->getUserName($objectData['general']['userModification']); - $objectData['general']['userOwnerUsername'] = $userOwnerName['userName']; - $objectData['general']['userOwnerFullname'] = $userOwnerName['fullName']; - $objectData['general']['userModificationUsername'] = $userModificationName['userName']; - $objectData['general']['userModificationFullname'] = $userModificationName['fullName']; - - $this->addAdminStyle($object, ElementAdminStyleEvent::CONTEXT_EDITOR, $objectData['general']); - - $currentLayoutId = $request->query->get('layoutId'); - - $validLayouts = DataObject\Service::getValidLayouts($object); - - //Fallback if $currentLayoutId is not set or empty string - //Uses first valid layout instead of admin layout when empty - $ok = false; - foreach ($validLayouts as $layout) { - if ($currentLayoutId == $layout->getId()) { - $ok = true; - } - } - - if (!$ok) { - $currentLayoutId = null; - } - - //main layout has id 0 so we check for is_null() - if ($currentLayoutId === null && $validLayouts !== []) { - if (count($validLayouts) === 1) { - $firstLayout = reset($validLayouts); - $currentLayoutId = $firstLayout->getId(); - } else { - foreach ($validLayouts as $checkDefaultLayout) { - if ($checkDefaultLayout->getDefault()) { - $currentLayoutId = $checkDefaultLayout->getId(); - } - } - } - } - - if ($currentLayoutId === null && count($validLayouts) > 0) { - $currentLayoutId = reset($validLayouts)->getId(); - } - - if ($validLayouts !== []) { - $objectData['validLayouts'] = []; - - foreach ($validLayouts as $validLayout) { - $objectData['validLayouts'][] = ['id' => $validLayout->getId(), 'name' => $validLayout->getName()]; - } - - usort($objectData['validLayouts'], static function ($layoutData1, $layoutData2) { - if ($layoutData2['id'] === '-1') { - return 1; - } - - if ($layoutData1['id'] === '-1') { - return -1; - } - - if ($layoutData2['id'] === '0') { - return 1; - } - if ($layoutData1['id'] === '0') { - return -1; - } - - return strcasecmp($layoutData1['name'], $layoutData2['name']); - }); - - $user = Tool\Admin::getCurrentUser(); - - if ($currentLayoutId == -1 && $user->isAdmin()) { - $layout = DataObject\Service::getSuperLayoutDefinition($object); - $objectData['layout'] = $layout; - } elseif (!empty($currentLayoutId)) { - $objectData['layout'] = $validLayouts[$currentLayoutId]->getLayoutDefinitions(); - } - - $objectData['currentLayoutId'] = $currentLayoutId; - } - - //Hook for modifying return value - e.g. for changing permissions based on object data - //data need to wrapped into a container in order to pass parameter to event listeners by reference so that they can change the values - $event = new GenericEvent($this, [ - 'data' => $objectData, - 'object' => $object, - ]); - - DataObject\Service::enrichLayoutDefinition($objectData['layout'], $object); - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_GET_PRE_SEND_DATA); - $data = $event->getArgument('data'); - - DataObject\Service::removeElementFromSession('object', $object->getId(), $request->getSession()->getId()); - - if ($data['layout'] ?? false) { - $layoutArray = json_decode($this->encodeJson($data['layout']), true); - $this->classFieldDefinitions = json_decode($this->encodeJson($object->getClass()->getFieldDefinitions()), true); - - if (is_array($layoutArray)) { - $this->injectValuesForCustomLayout($layoutArray); - } - - $data['layout'] = $layoutArray; - } - - return $this->adminJson($data); + public function getAction( + GetDataObjectPayload $payload, + GetDataObjectHandler $getDataObject, + ): JsonResponse { + try { + $result = $getDataObject($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - throw $this->createAccessDeniedHttpException(); - } + $this->sessionService->removeObject('object', $payload->id); - private function injectValuesForCustomLayout(array &$layout): void - { - foreach ($layout['children'] as &$child) { - if ($child['datatype'] === 'layout') { - $this->injectValuesForCustomLayout($child); - } else { - foreach ($this->classFieldDefinitions[$child['name']] as $key => $value) { - if (array_key_exists($key, $child) && ($child[$key] === null || $child[$key] === '' || (is_array($child[$key]) && empty($child[$key])))) { - $child[$key] = $value; - } - } - } - } + return $this->adminJson($result->data); } - /** - * @throws Exception - */ #[Route('/get-select-options', name: 'getSelectOptions', methods: ['POST'])] - public function getSelectOptions(Request $request): JsonResponse - { - $objectId = $request->request->getInt('objectId'); - $object = DataObject\Concrete::getById($objectId); - if (!$object instanceof DataObject\Concrete) { - return new JsonResponse(['success'=> false, 'message' => 'Object not found.']); - } - - if ($request->request->get('changedData')) { - $this->applyChanges($object, $this->decodeJson($request->request->get('changedData'))); - } - - $fieldDefinitionConfig = json_decode($request->request->get('fieldDefinition'), true); - /** - * @var DataObject\ClassDefinition\Data\Select|DataObject\ClassDefinition\Data\Multiselect $fieldDefinition - */ - $fieldDefinition = DataObject\Classificationstore\Service::getFieldDefinitionFromJson( - $fieldDefinitionConfig, - $fieldDefinitionConfig['fieldtype'] - ); - - $optionsProvider = OptionsProviderResolver::resolveProvider( - $fieldDefinition->getOptionsProviderClass(), - $fieldDefinition instanceof DataObject\ClassDefinition\Data\Multiselect - ? OptionsProviderResolver::MODE_MULTISELECT - : OptionsProviderResolver::MODE_SELECT - ); - - $context = json_decode($request->request->get('context'), true) ?? []; - $options = $optionsProvider->getOptions( - [ - 'object' => $object, - 'fieldname' => $fieldDefinition->getName(), - 'class' => $object->getClass(), - 'context' => $context, - ], - $fieldDefinition - ); - - return new JsonResponse(['success' => true, 'options' => $options]); - } - - private function applyChanges(DataObject\Concrete $object, array $changes): void - { - foreach ($changes as $key => $value) { - $fd = $object->getClass()->getFieldDefinition($key); - if ($fd) { - if ($fd instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $user = Tool\Admin::getCurrentUser(); - if (!$user->getAdmin()) { - $allowedLanguages = DataObject\Service::getLanguagePermissions($object, $user, 'lEdit'); - if (!is_null($allowedLanguages)) { - $allowedLanguages = array_keys($allowedLanguages); - $submittedLanguages = array_keys($changes[$key]); - foreach ($submittedLanguages as $submittedLanguage) { - if (!in_array($submittedLanguage, $allowedLanguages)) { - unset($value[$submittedLanguage]); - } - } - } - } - } - - if ($fd instanceof ReverseObjectRelation) { - $remoteClass = DataObject\ClassDefinition::getByName($fd->getOwnerClassName()); - $relations = $object->getRelationData($fd->getOwnerFieldName(), false, $remoteClass->getId()); - $toAdd = $this->detectAddedRemoteOwnerRelations($relations, $value); - $toDelete = $this->detectDeletedRemoteOwnerRelations($relations, $value); - if (count($toAdd) > 0 || count($toDelete) > 0) { - $this->processRemoteOwnerRelations($object, $toDelete, $toAdd, $fd->getOwnerFieldName()); - } - } else { - $object->setValue($key, $fd->getDataFromEditmode($value, $object)); - } - } - } - } - - private function getDataForObject(DataObject\Concrete $object, bool $objectFromVersion = false): void - { - foreach ($object->getClass()->getFieldDefinitions(['object' => $object]) as $key => $def) { - $this->getDataForField($object, $key, $def, $objectFromVersion); - } - } - - /** - * Gets recursively attribute data from parent and fills objectData and metaData - */ - private function getDataForField(DataObject\Concrete $object, string $key, DataObject\ClassDefinition\Data $fielddefinition, bool $objectFromVersion, int $level = 0): void - { - $parent = DataObject\Service::hasInheritableParentObject($object); - $getter = 'get' . ucfirst($key); - - // Editmode optimization for lazy loaded relations (note that this is just for AbstractRelations, not for all - // LazyLoadingSupportInterface types. It tries to optimize fetching the data needed for the editmode without - // loading the entire target element. - // ReverseObjectRelation should go in there anyway (regardless if it a version or not), - // so that the values can be loaded. - if ( - (!$objectFromVersion && $fielddefinition instanceof AbstractRelations) - || $fielddefinition instanceof ReverseObjectRelation - ) { - $refId = null; - - if ($fielddefinition instanceof ReverseObjectRelation) { - $refKey = $fielddefinition->getOwnerFieldName(); - $refClass = DataObject\ClassDefinition::getByName($fielddefinition->getOwnerClassName()); - if ($refClass) { - $refId = $refClass->getId(); - } - } else { - $refKey = $key; - } - - $relations = $object->getRelationData($refKey, !$fielddefinition instanceof ReverseObjectRelation, $refId); - - if ($fielddefinition->supportsInheritance() && $relations === [] && !empty($parent)) { - $this->getDataForField($parent, $key, $fielddefinition, $objectFromVersion, $level + 1); - } else { - $data = []; - - if ($fielddefinition instanceof DataObject\ClassDefinition\Data\ManyToOneRelation) { - if (isset($relations[0])) { - $data = $relations[0]; - $data['published'] = (bool)$data['published']; - } else { - $data = null; - } - } elseif ( - ($fielddefinition instanceof DataObject\ClassDefinition\Data\OptimizedAdminLoadingInterface && $fielddefinition->isOptimizedAdminLoading()) - || ($fielddefinition instanceof ManyToManyObjectRelation && !$fielddefinition->getVisibleFields() && !$fielddefinition instanceof DataObject\ClassDefinition\Data\AdvancedManyToManyObjectRelation) - ) { - foreach ($relations as $rkey => $rel) { - $index = $rkey + 1; - $rel['fullpath'] = $rel['path']; - $rel['classname'] = $rel['subtype']; - $rel['rowId'] = $rel['id'] . AbstractRelations::RELATION_ID_SEPARATOR . $index . AbstractRelations::RELATION_ID_SEPARATOR . $rel['type']; - $rel['published'] = (bool)$rel['published']; - $data[] = $rel; - } - } else { - $fieldData = $object->$getter(); - $data = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); - } - $this->objectData[$key] = $data; - $this->metaData[$key]['objectid'] = $object->getId(); - $this->metaData[$key]['inherited'] = $level !== 0; - } - } else { - $fieldData = $object->$getter(); - $isInheritedValue = false; - - if ($fielddefinition instanceof DataObject\ClassDefinition\Data\CalculatedValue) { - $fieldData = new DataObject\Data\CalculatedValue($fielddefinition->getName()); - $fieldData->setContextualData('object', null, null, null, null, null, $fielddefinition); - $value = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); - } else { - $value = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); - } - - // following some exceptions for special data types (localizedfields, objectbricks) - if ($value && ($fieldData instanceof DataObject\Localizedfield || $fieldData instanceof DataObject\Classificationstore)) { - // make sure that the localized field participates in the inheritance detection process - $isInheritedValue = $value['inherited']; - } - if ($fielddefinition instanceof DataObject\ClassDefinition\Data\Objectbricks && is_array($value)) { - // make sure that the objectbricks participate in the inheritance detection process - foreach ($value as $singleBrickData) { - if (!empty($singleBrickData['inherited'])) { - $isInheritedValue = true; - } - } - } - - if ($fielddefinition->isEmpty($fieldData) && !empty($parent)) { - $this->getDataForField($parent, $key, $fielddefinition, $objectFromVersion, $level + 1); - // exception for classification store. if there are no items then it is empty by definition. - // consequence is that we have to preserve the metadata information - if ($fielddefinition instanceof DataObject\ClassDefinition\Data\Classificationstore && $level === 0) { - $this->objectData[$key]['metaData'] = $value['metaData'] ?? []; - $this->objectData[$key]['inherited'] = true; - } - } else { - $isInheritedValue = $isInheritedValue || ($level !== 0); - $this->metaData[$key]['objectid'] = $object->getId(); - - $this->objectData[$key] = $value; - $this->metaData[$key]['inherited'] = $isInheritedValue; + public function getSelectOptions( + GetSelectOptionsPayload $payload, + GetSelectOptionsHandler $handler, + ): JsonResponse { + $options = $handler($payload); - if ($isInheritedValue && !$fielddefinition->isEmpty($fieldData) && !$fielddefinition->supportsInheritance()) { - $this->objectData[$key] = null; - $this->metaData[$key]['inherited'] = false; - $this->metaData[$key]['hasParentValue'] = true; - } - } - } + return $this->adminJson(ApiResponse::ok(['options' => $options])); } #[Route('/get-folder', name: 'getfolder', methods: ['GET'])] - public function getFolderAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $objectId = (int)$request->query->get('id'); - $object = DataObject::getById($objectId); - - if (!$object) { - throw $this->createNotFoundException(); - } - - if ($object->isAllowed('view')) { - $objectData = []; - - $objectData['general'] = []; - $objectData['idPath'] = Element\Service::getIdPath($object); - $objectData['type'] = $object->getType(); - $allowedKeys = ['published', 'key', 'id', 'type', 'path', 'modificationDate', 'creationDate', 'userOwner', 'userModification']; - foreach ($object->getObjectVars() as $key => $value) { - if (in_array($key, $allowedKeys)) { - $objectData['general'][$key] = $value; - } - } - $objectData['general']['fullpath'] = $object->getRealFullPath(); - - $objectData['general']['locked'] = $object->isLocked(); - - $objectData['properties'] = Element\Service::minimizePropertiesForEditmode($object->getProperties()); - $objectData['userPermissions'] = $object->getUserPermissions($this->getAdminUser()); - $objectData['classes'] = $this->prepareChildClasses($object->getDao()->getClasses()); - - $userOwnerName = $this->getUserName($objectData['general']['userOwner']); - $userModificationName = ($objectData['general']['userOwner'] == $objectData['general']['userModification']) ? $userOwnerName : $this->getUserName($objectData['general']['userModification']); - $objectData['general']['userOwnerUsername'] = $userOwnerName['userName']; - $objectData['general']['userOwnerFullname'] = $userOwnerName['fullName']; - $objectData['general']['userModificationUsername'] = $userModificationName['userName']; - $objectData['general']['userModificationFullname'] = $userModificationName['fullName']; - - //Hook for modifying return value - e.g. for changing permissions based on object data - //data need to wrapped into a container in order to pass parameter to event listeners by reference so that they can change the values - $event = new GenericEvent($this, [ - 'data' => $objectData, - 'object' => $object, - ]); - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_GET_PRE_SEND_DATA); - $objectData = $event->getArgument('data'); - - return $this->adminJson($objectData); - } - - throw $this->createAccessDeniedHttpException(); - } - - /** - * @param DataObject\ClassDefinition[] $classes - */ - protected function prepareChildClasses(array $classes): array - { - $reduced = []; - foreach ($classes as $class) { - $reduced[] = [ - 'id' => $class->getId(), - 'name' => $class->getName(), - 'inheritance' => $class->getAllowInherit(), - ]; - } + public function getFolderAction( + IdQueryPayload $payload, + GetDataObjectFolderHandler $handler, + ): JsonResponse { + $result = $handler($payload); - return $reduced; + return $this->adminJson($result->data); } #[Route('/add', name: 'add', methods: ['POST'])] - public function addAction(Request $request, Model\FactoryInterface $modelFactory): JsonResponse - { - $message = ''; - $parent = DataObject::getById((int) $request->request->get('parentId')); - - if (!$parent->isAllowed('create')) { - $message = 'prevented adding object because of missing permissions'; - Logger::debug($message); - } - - $intendedPath = $parent->getRealFullPath() . '/' . $request->request->get('key'); - if (DataObject\Service::pathExists($intendedPath)) { - $message = 'prevented creating object because object with same path+key already exists'; - Logger::debug($message); - } - - //return false if missing permissions or path+key already exists - if (!empty($message)) { - return $this->adminJson([ - 'success' => false, - 'message' => $message, - ]); - } - - $className = 'OpenDxp\\Model\\DataObject\\' . ucfirst($request->request->get('className')); - /** @var DataObject\Concrete $object */ - $object = $modelFactory->build($className); - $object->setOmitMandatoryCheck(true); // allow to save the object although there are mandatory fields - $classId = $request->request->get('classId'); - if ($request->request->get('variantViaTree')) { - $parentId = $request->request->getInt('parentId'); - $parent = DataObject\Concrete::getById($parentId); - $classId = $parent->getClass()->getId(); - } - - $object->setClassId($classId); - $object->setClassName($request->request->get('className')); - $object->setParentId($request->request->getInt('parentId')); - $object->setKey($request->request->get('key')); - $object->setCreationDate(time()); - $object->setUserOwner($this->getAdminUser()->getId()); - $object->setUserModification($this->getAdminUser()->getId()); - $object->setPublished(false); - - $objectType = $request->request->get('objecttype'); - if (in_array($objectType, [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_VARIANT])) { - $object->setType($objectType); - } - - try { - $object->save(); - $return = [ - 'success' => true, - 'id' => $object->getId(), - 'type' => $object->getType(), - 'message' => $message, - ]; - } catch (Exception $e) { - $return = [ - 'success' => false, - 'message' => $e->getMessage(), - ]; - } + public function addAction( + AddObjectPayload $payload, + AddObjectHandler $handler, + ): JsonResponse { + $result = $handler($payload); - return $this->adminJson($return); + return $this->adminJson(ApiResponse::ok([ + 'id' => $result->id, + 'type' => $result->type, + ])); } #[Route('/add-folder', name: 'addfolder', methods: ['POST'])] - public function addFolderAction(Request $request): JsonResponse - { - $success = false; - - $parent = DataObject::getById((int) $request->request->get('parentId')); - if ($parent->isAllowed('create')) { - if (!DataObject\Service::pathExists($parent->getRealFullPath() . '/' . $request->request->get('key'))) { - $folder = DataObject\Folder::create([ - 'parentId' => $request->request->get('parentId'), - 'creationDate' => time(), - 'userOwner' => $this->getAdminUser()->getId(), - 'userModification' => $this->getAdminUser()->getId(), - 'key' => $request->request->get('key'), - 'published' => true, - ]); - - try { - $folder->save(); - $success = true; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - } else { - Logger::debug('prevented creating object id because of missing permissions'); - } + public function addFolderAction( + AddObjectFolderPayload $payload, + AddObjectFolderHandler $handler, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => $success]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ #[Route('/delete', name: 'delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse - { - $type = $request->request->get('type'); - - if ($type === 'children') { - $parentObject = DataObject::getById((int) $request->request->get('id')); - - $list = new DataObject\Listing(); - $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($parentObject->getRealFullPath()) . '/%')); - $list->setLimit((int)$request->request->get('amount')); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('DESC'); - - $deletedItems = []; - foreach ($list as $object) { - $deletedItems[$object->getId()] = $object->getRealFullPath(); - if ($object->isAllowed('delete') && !$object->isLocked()) { - $object->delete(); - } - } - - return $this->adminJson(['success' => true, 'deleted' => $deletedItems]); + public function deleteAction( + DeleteDataObjectPayload $payload, + DeleteDataObjectHandler $handler, + ): JsonResponse { + if ($payload->type !== 'children' && !$payload->id) { + throw new NotFoundHttpException(); } - if ($id = $request->request->get('id')) { - $object = DataObject::getById((int) $id); - if ($object) { - if (!$object->isAllowed('delete')) { - throw $this->createAccessDeniedHttpException(); - } - if ($object->isLocked()) { - return $this->adminJson(['success' => false, 'message' => 'prevented deleting object, because it is locked: ID: ' . $object->getId()]); - } - $object->delete(); - } - // return true, even when the object doesn't exist, this can be the case when using batch delete incl. children - return $this->adminJson(['success' => true]); + $result = $handler($payload); + + if ($payload->type === 'children') { + return $this->adminJson(ApiResponse::ok(['deleted' => $result->deleted])); } - return $this->adminJson(['success' => false]); + // return ok even when the object doesn't exist — valid for batch delete incl. children + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ #[Route('/change-children-sort-by', name: 'changechildrensortby', methods: ['PUT'])] - public function changeChildrenSortByAction(Request $request): JsonResponse - { - $object = DataObject::getById((int) $request->request->get('id')); - if ($object) { - $sortBy = $request->request->get('sortBy'); - $sortOrder = $request->request->get('childrenSortOrder'); - if (!\in_array($sortOrder, ['ASC', 'DESC'])) { - $sortOrder = 'ASC'; - } - - $currentSortBy = $object->getChildrenSortBy(); - - $object->setChildrenSortBy($sortBy); - $object->setChildrenSortOrder($sortOrder); - - if ($currentSortBy != $sortBy) { - $user = Tool\Admin::getCurrentUser(); - - if (!$user->isAdmin() && !$user->isAllowed('objects_sort_method')) { - return $this->json(['success' => false, 'message' => 'Changing the sort method is only allowed for admin users']); - } - - if ($sortBy === 'index') { - $this->reindexBasedOnSortOrder($object, $sortOrder); - } - } - - $object->save(); - - return $this->json(['success' => true]); - } + public function changeChildrenSortByAction( + ChangeChildrenSortByPayload $payload, + ChangeChildrenSortByHandler $handler, + ): JsonResponse { + $handler($payload); - return $this->json(['success' => false, 'message' => 'Unable to change a sorting way of children items.']); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ #[Route('/update', name: 'update', methods: ['PUT'])] - public function updateAction(Request $request): JsonResponse - { - $values = $this->decodeJson($request->request->get('values')); - $ids = $this->decodeJson($request->request->get('id')); - - if (is_array($ids)) { - $return = ['success' => true]; - foreach ($ids as $id) { - $object = DataObject::getById((int)$id); - $return = $this->executeUpdateAction($object, $values); - if (!$return['success']) { - return $this->adminJson($return); - } - } - } else { - $object = DataObject::getById((int)$ids); - $return = $this->executeUpdateAction($object, $values); - } - - return $this->adminJson($return); - } - - /** - * @return array{success: bool, message?: string} - * - * @throws Exception - */ - private function executeUpdateAction(DataObject $object, mixed $values): array - { - $data = ['success' => false]; - - if ($object instanceof DataObject\Concrete) { - $object->setOmitMandatoryCheck(true); - } - - // this prevents the user from renaming, relocating (actions in the tree) if the newest version isn't the published one - // the reason is that otherwise the content of the newer not published version will be overwritten - if ($object instanceof DataObject\Concrete) { - $latestVersion = $object->getLatestVersion(); - if ($latestVersion && $latestVersion->getData()->getModificationDate() != $object->getModificationDate()) { - return ['success' => false, 'message' => "You can't rename or relocate if there's a newer not published version"]; - } - } - - $key = $values['key'] ?? null; - if ($key) { - $key = Service::getValidKey($key, 'object'); - } - - if ($object->isAllowed('settings')) { - if ($key) { - if ($object->isAllowed('rename')) { - $object->setKey($key); - } elseif ($key !== $object->getKey()) { - Logger::debug('prevented renaming object because of missing permissions '); - } - } - - if (!empty($values['parentId'])) { - $parent = DataObject::getById((int) $values['parentId']); - - //check if parent is changed - if ($object->getParentId() !== $parent->getId()) { - if (!$parent->isAllowed('create')) { - throw new Exception('Prevented moving object - no create permission on new parent '); - } - - $objectWithSamePath = DataObject::getByPath($parent->getRealFullPath() . '/' . $object->getKey()); - - if ($objectWithSamePath != null) { - return ['success' => false, 'message' => 'prevented creating object because object with same path+key already exists']; - } - - if ($object->isLocked()) { - return ['success' => false, 'message' => 'prevented moving object, because it is locked: ID: ' . $object->getId()]; - } - - $object->setParentId($values['parentId']); - } - } - - if (array_key_exists('locked', $values)) { - $object->setLocked($values['locked']); - } - - $object->setModificationDate(time()); - $object->setUserModification($this->getAdminUser()->getId()); - - try { - $isIndexUpdate = isset($values['indices']); - - if ($isIndexUpdate) { - // Ensure the update sort index is already available in the postUpdate eventListener - $indexUpdate = is_int($values['indices']) ? $values['indices'] : $values['indices'][$object->getId()]; - $object->setIndex($indexUpdate); - } - - $object->save(); - - if ($isIndexUpdate) { - $this->updateIndexesOfObjectSiblings($object, $indexUpdate); - } - - $data = [ - 'success' => true, - 'treeData' => $this->getTreeNodeConfig($object), - ]; - } catch (Exception $e) { - Logger::error((string) $e); - - return ['success' => false, 'message' => $e->getMessage()]; - } - } elseif ($key && $object->isAllowed('rename')) { - return $this->renameObject($object, $key); - } else { - Logger::debug('prevented update object because of missing permissions.'); - } - - return $data; - } - - private function executeInsideTransaction(callable $fn): void - { - $maxRetries = 5; - for ($retries = 0; $retries < $maxRetries; $retries++) { - try { - Db::get()->beginTransaction(); - - $fn(); - - Db::get()->commit(); - - break; - } catch (Exception $e) { - Db::get()->rollBack(); - - // we try to start the transaction $maxRetries times again (deadlocks, ...) - if ($retries < ($maxRetries - 1)) { - $run = $retries + 1; - $waitTime = random_int(1, 5) * 100000; // microseconds - Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')'); - - usleep($waitTime); // wait specified time until we restart the transaction - } else { - // if the transaction still fail after $maxRetries retries, we throw out the exception - Logger::error('Finally giving up restarting the same transaction again and again, last message: ' . $e->getMessage()); - - throw $e; - } - } - } - } - - protected function reindexBasedOnSortOrder(DataObject\AbstractObject $parentObject, string $currentSortOrder): void - { - $fn = function () use ($parentObject, $currentSortOrder): void { - $list = new DataObject\Listing(); - - $db = Db::get(); - $result = $db->executeStatement( - 'UPDATE '.$list->getDao()->getTableName().' o, - ( - SELECT newIndex, id FROM ( - SELECT @n := @n +1 AS newIndex, id - FROM '.$list->getDao()->getTableName().', - (SELECT @n := -1) variable - WHERE parentId = ? ORDER BY `key` ' . $currentSortOrder - .') tmp - ) order_table - SET o.index = order_table.newIndex - WHERE o.id=order_table.id', - [ - $parentObject->getId(), - ] - ); - - $db = Db::get(); - $children = $db->fetchAllAssociative( - 'SELECT id, modificationDate, versionCount FROM objects WHERE parentId = ? ORDER BY `index` ASC', - [$parentObject->getId()] - ); - $index = 0; - - foreach ($children as $child) { - $this->updateLatestVersionIndex($child['id'], $child['modificationDate']); - $index++; - - DataObject::clearDependentCacheByObjectId($child['id']); - } - }; - - $this->executeInsideTransaction($fn); - } - - private function updateLatestVersionIndex(int $objectId, int $newIndex): void - { - $object = DataObject\Concrete::getById($objectId); - - if ( - $object && - $object->getType() !== DataObject::OBJECT_TYPE_FOLDER && - $latestVersion = $object->getLatestVersion() - ) { - // don't renew references (which means loading the target elements) - // Not needed as we just save a new version with the updated index - $object = $latestVersion->loadData(false); - if ($newIndex !== $object->getIndex()) { - $object->setIndex($newIndex); - } - $latestVersion->save(); + public function updateAction( + UpdateDataObjectPayload $payload, + UpdateDataObjectHandler $handler, + ): JsonResponse { + try { + $result = $handler($payload); + } catch (\Throwable $e) { + return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); } - } - - protected function updateIndexesOfObjectSiblings(DataObject\AbstractObject $updatedObject, int $newIndex): void - { - $fn = function () use ($updatedObject, $newIndex): void { - $list = new DataObject\Listing(); - $updatedObject->saveIndex($newIndex); - - // The cte and the limit are needed to order the data before the newIndex is set - $db = Db::get(); - $db->executeStatement( - 'UPDATE '.$list->getDao()->getTableName().' o, - ( - SELECT newIndex, id - FROM ( - With cte As (SELECT `index`, id FROM ' . $list->getDao()->getTableName() . ' WHERE parentId = ? AND id != ? AND `type` IN (\''.implode( - "','", [ - DataObject::OBJECT_TYPE_OBJECT, - DataObject::OBJECT_TYPE_VARIANT, - DataObject::OBJECT_TYPE_FOLDER, - ] - ).'\') ORDER BY `index` LIMIT '. $updatedObject->getParent()->getChildAmount([ - DataObject::OBJECT_TYPE_OBJECT, - DataObject::OBJECT_TYPE_VARIANT, - DataObject::OBJECT_TYPE_FOLDER, - ]) .') - SELECT @n := IF(@n = ? - 1,@n + 2,@n + 1) AS newIndex, id - FROM cte, - (SELECT @n := -1) variable - ) tmp - ) order_table - SET o.index = order_table.newIndex - WHERE o.id=order_table.id', - [ - $updatedObject->getParentId(), - $updatedObject->getId(), - $newIndex, - ] - ); - - $siblings = $db->fetchAllAssociative( - 'SELECT id, modificationDate, versionCount, `key`, `index` FROM objects - WHERE parentId = ? AND id != ? AND `type` IN ("object", "variant", "folder") ORDER BY `index` ASC', - [$updatedObject->getParentId(), $updatedObject->getId()] - ); - $index = 0; - - foreach ($siblings as $sibling) { - if ($index === $newIndex) { - $index++; - } - - $this->updateLatestVersionIndex($sibling['id'], $index); - $index++; - - DataObject::clearDependentCacheByObjectId($sibling['id']); - } - }; - $this->executeInsideTransaction($fn); + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData])); } - /** - * @throws Exception - */ #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveDataObjectHandler $handler, SaveDataObjectPayload $payload): JsonResponse { - $objectFromDatabase = DataObject\Concrete::getById((int) $request->request->get('id')); - - if (!$objectFromDatabase instanceof DataObject\Concrete) { - return $this->adminJson(['success' => false, 'message' => 'Could not find object']); - } - - // set the latest available version for editmode - $object = $this->getLatestVersion($objectFromDatabase); - $object->setUserModification($this->getAdminUser()->getId()); - - $objectFromVersion = $object !== $objectFromDatabase; - if ($objectFromVersion) { - if (method_exists($object, 'getLocalizedFields')) { - /** @var DataObject\Localizedfield $localizedFields */ - $localizedFields = $object->getLocalizedFields(); - $localizedFields->setLoadedAllLazyData(); - } - - // Mark fields that have changed as dirty - if ($request->query->get('task') !== 'autoSave' && $request->query->get('task') !== 'unpublish') { - foreach ($object->getClass()->getFieldDefinitions() as $fieldName => $fieldDefinition) { - $getter = 'get' . ucfirst($fieldName); - $oldValue = $objectFromDatabase->$getter(); - $newValue = $object->$getter(); - $isEqual = $fieldDefinition instanceof DataObject\ClassDefinition\Data\EqualComparisonInterface - ? $fieldDefinition->isEqual($oldValue, $newValue) - : $oldValue === $newValue; - - if (!$isEqual) { - $object->markFieldDirty($fieldName); - } - } - } - } - - if ($request->request->has('data')) { - try { - $this->applyChanges($object, $this->decodeJson($request->request->get('data'))); - } catch (Throwable) { - $this->applyChanges($objectFromDatabase, $this->decodeJson($request->request->get('data'))); - } - } - - $this->assignPropertiesFromEditmode($request, $object); - $this->applySchedulerDataToElement($request, $object, $this->getAdminUser()); - - if (($request->query->get('task') === 'unpublish' && !$object->isAllowed('unpublish')) || ($request->query->get('task') === 'publish' && !$object->isAllowed('publish'))) { - throw $this->createAccessDeniedHttpException(); - } - - if ($request->query->get('task') === 'unpublish') { - $object->setPublished(false); - } - - if ($request->query->get('task') === 'publish') { - $object->setPublished(true); - } - - // unpublish and save version is possible without checking mandatory fields - if (in_array($request->query->get('task'), ['unpublish', 'version', 'autoSave'])) { - $object->setOmitMandatoryCheck(true); - } - - if (($request->query->get('task') === 'publish') || ($request->query->get('task') === 'unpublish')) { - - $object->save(); - $treeData = $this->getTreeNodeConfig($object); - - $newObject = DataObject::getById($object->getId(), ['force' => true]); - - if ($request->query->get('task') === 'publish') { - $object->deleteAutoSaveVersions($this->getAdminUser()->getId()); - } - - return $this->adminJson([ - 'success' => true, - 'general' => ['modificationDate' => $object->getModificationDate(), - 'versionDate' => $newObject->getModificationDate(), - 'versionCount' => $newObject->getVersionCount(), - ], - 'treeData' => $treeData, - ]); - } - - if ($request->query->get('task') === 'session') { - DataObject\Service::saveElementToSession($object, $request->getSession()->getId(), ''); + $result = $handler($payload); - return $this->adminJson(['success' => true]); + if ($payload->task === 'session' || $payload->task === 'scheduler') { + return $this->adminJson(ApiResponse::ok()); } - if ($request->query->get('task') === 'scheduler' && $object->isAllowed('settings')) { - $object->saveScheduledTasks(); - - return $this->adminJson(['success' => true]); - } - - if ($object->isAllowed('save') || $object->isAllowed('publish')) { - $isAutoSave = $request->query->get('task') === 'autoSave'; - $draftData = []; - - if ($object->isPublished() || $isAutoSave) { - $version = $object->saveVersion(true, true, null, $isAutoSave); - $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), - ]; - } else { - $object->save(); - } - - if ($request->query->get('task') === 'version') { - $object->deleteAutoSaveVersions($this->getAdminUser()->getId()); - } - - $treeData = $this->getTreeNodeConfig($object); - - $newObject = DataObject::getById($object->getId(), ['force' => true]); - - return $this->adminJson([ - 'success' => true, - 'general' => ['modificationDate' => $object->getModificationDate(), - 'versionDate' => $newObject->getModificationDate(), - 'versionCount' => $newObject->getVersionCount(), + if ($payload->task === 'publish' || $payload->task === 'unpublish') { + return $this->adminJson(ApiResponse::ok([ + 'general' => [ + 'modificationDate' => $result->modificationDate, + 'versionDate' => $result->versionDate, + 'versionCount' => $result->versionCount, ], - 'draft' => $draftData, - 'treeData' => $treeData, - ]); + 'treeData' => $result->treeData, + ])); } - throw $this->createAccessDeniedHttpException(); + return $this->adminJson(ApiResponse::ok([ + 'general' => [ + 'modificationDate' => $result->modificationDate, + 'versionDate' => $result->versionDate, + 'versionCount' => $result->versionCount, + ], + 'draft' => $result->draftData, + 'treeData' => $result->treeData, + ])); } #[Route('/save-folder', name: 'savefolder', methods: ['PUT'])] - public function saveFolderAction(Request $request): JsonResponse - { - $object = DataObject::getById((int) $request->request->get('id')); - - if (!$object) { - throw $this->createNotFoundException('Object not found'); - } - - if ($object->isAllowed('publish')) { - try { - // general settings - $general = $this->decodeJson($request->request->get('general')); - $object->setValues($general); - $object->setUserModification($this->getAdminUser()->getId()); - - $this->assignPropertiesFromEditmode($request, $object); - - $object->save(); - - return $this->adminJson(['success' => true]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - throw $this->createAccessDeniedHttpException(); - } - - protected function assignPropertiesFromEditmode(Request $request, DataObject\AbstractObject $object): void - { - if ($request->request->has('properties')) { - $properties = []; - // assign inherited properties - foreach ($object->getProperties() as $p) { - if ($p->isInherited()) { - $properties[$p->getName()] = $p; - } - } - - $propertiesData = $this->decodeJson($request->request->get('properties')); - - if (is_array($propertiesData)) { - foreach ($propertiesData as $propertyName => $propertyData) { - $value = $propertyData['data']; - - try { - $property = new Model\Property(); - $property->setType($propertyData['type']); - $property->setName($propertyName); - $property->setCtype('object'); - $property->setDataFromEditmode($value); - $property->setInheritable($propertyData['inheritable']); - - $properties[$propertyName] = $property; - } catch (Exception) { - Logger::err("Can't add " . $propertyName . ' to object ' . $object->getRealFullPath()); - } - } - } - $object->setProperties($properties); - } - } - - #[Route('/publish-version', name: 'publishversion', methods: ['POST'])] - public function publishVersionAction(Request $request): JsonResponse - { - $id = $request->request->getInt('id'); - $version = Model\Version::getById($id); - $object = $version?->loadData(); - - if (!$object) { - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - - $object = $version->loadData(); - - $currentObject = DataObject::getById($object->getId()); - if ($currentObject->isAllowed('publish')) { - $object->setPublished(true); - $object->setUserModification($this->getAdminUser()->getId()); - - try { - $object->save(); - - $treeData = []; - $this->addAdminStyle($object, ElementAdminStyleEvent::CONTEXT_TREE, $treeData); - - return $this->adminJson( - [ - 'success' => true, - 'general' => ['modificationDate' => $object->getModificationDate() ], - 'treeData' => $treeData, ] - ); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - throw $this->createAccessDeniedHttpException(); - } - - /** - * @throws Exception - */ - #[Route('/preview-version', name: 'previewversion', methods: ['GET'])] - public function previewVersionAction(Request $request, Environment $twig): Response - { - DataObject::setDoNotRestoreKeyAndPath(true); - - $id = $request->query->getInt('id'); - $version = Model\Version::getById($id); - $object = $version?->loadData(); - - if ($object) { - - Tool\UserTimezone::setUserTimezone($request->query->get('userTimezone')); - - if ($timezone = Tool\UserTimezone::getUserTimezone()) { - $twig->getExtension(CoreExtension::class)->setTimezone($timezone); - } - - if (method_exists($object, 'getLocalizedFields')) { - /** @var DataObject\Localizedfield $localizedFields */ - $localizedFields = $object->getLocalizedFields(); - $localizedFields->setLoadedAllLazyData(); - } - - DataObject::setDoNotRestoreKeyAndPath(false); - - if ($object->isAllowed('versions')) { - return $this->render('@OpenDxpAdmin/admin/data_object/data_object/preview_version.html.twig', - [ - 'object' => $object, - 'versionNote' => $version->getNote(), - 'validLanguages' => Tool::getValidLanguages(), - ]); - } - - throw $this->createAccessDeniedException('Permission denied, version id [' . $id . ']'); - } - - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - - /** - * @throws Exception - */ - #[Route('/diff-versions/from/{from}/to/{to}', name: 'diffversions', methods: ['GET'])] - public function diffVersionsAction(Request $request, Environment $twig, int $from, int $to): Response - { - DataObject::setDoNotRestoreKeyAndPath(true); - - $id1 = $from; - $id2 = $to; - - $version1 = Model\Version::getById($id1); - $object1 = $version1?->loadData(); - - if (!$object1) { - throw $this->createNotFoundException('Version with id [' . $id1 . "] doesn't exist"); - } - - if (method_exists($object1, 'getLocalizedFields')) { - /** @var DataObject\Localizedfield $localizedFields1 */ - $localizedFields1 = $object1->getLocalizedFields(); - $localizedFields1->setLoadedAllLazyData(); - } - - $version2 = Model\Version::getById($id2); - $object2 = $version2?->loadData(); - - if (!$object2) { - throw $this->createNotFoundException('Version with id [' . $id2 . "] doesn't exist"); - } - - Tool\UserTimezone::setUserTimezone($request->query->get('userTimezone')); - - if ($timezone = Tool\UserTimezone::getUserTimezone()) { - $twig->getExtension(CoreExtension::class)->setTimezone($timezone); - } - - if (method_exists($object2, 'getLocalizedFields')) { - /** @var DataObject\Localizedfield $localizedFields2 */ - $localizedFields2 = $object2->getLocalizedFields(); - $localizedFields2->setLoadedAllLazyData(); - } - - DataObject::setDoNotRestoreKeyAndPath(false); - - if ($object1->isAllowed('versions') && $object2->isAllowed('versions')) { - return $this->render('@OpenDxpAdmin/admin/data_object/data_object/diff_versions.html.twig', - [ - 'object1' => $object1, - 'versionNote1' => $version1->getNote(), - 'object2' => $object2, - 'versionNote2' => $version2->getNote(), - 'validLanguages' => Tool::getValidLanguages(), - ]); - } + public function saveFolderAction( + SaveDataObjectFolderPayload $payload, + SaveDataObjectFolderHandler $handler, + ): JsonResponse { + $handler($payload); - throw $this->createAccessDeniedException('Permission denied, version ids [' . $id1 . ', ' . $id2 . ']'); + return $this->adminJson(ApiResponse::ok()); } #[Route('/grid-proxy', name: 'gridproxy', methods: ['GET', 'POST', 'PUT'])] public function gridProxyAction( + DataObjectGridProxyPayload $payload, + DataObjectGridProxyHandler $handler, Request $request, - EventDispatcherInterface $eventDispatcher, - GridHelperService $gridHelperService, - LocaleServiceInterface $localeService, - CsrfProtectionHandler $csrfProtection + CsrfProtectionHandler $csrfProtection, ): JsonResponse { - $allParams = [...$request->request->all(), ...$request->query->all()]; - if (isset($allParams['context']) && $allParams['context']) { - $allParams['context'] = json_decode($allParams['context'], true); - } else { - $allParams['context'] = []; - } - - $filterPrepareEvent = new GenericEvent($this, [ - 'requestParams' => $allParams, - ]); - $eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::OBJECT_LIST_BEFORE_FILTER_PREPARE); - - $allParams = $filterPrepareEvent->getArgument('requestParams'); - $csrfProtection->checkCsrfToken($request); - $result = $this->gridProxy( - $allParams, - DataObject::OBJECT_TYPE_OBJECT, - $request, - $eventDispatcher, - $gridHelperService, - $localeService - ); - - return $this->adminJson($result); - } - - #[Route('/copy-info', name: 'copyinfo', methods: ['GET'])] - public function copyInfoAction(Request $request): JsonResponse - { - $transactionId = time(); - $pasteJobs = []; - - Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($transactionId): void { - $session->set((string) $transactionId, ['idMapping' => []]); - }, 'opendxp_copy'); - - if ($request->query->get('type') === 'recursive' || $request->query->get('type') === 'recursive-update-references') { - $object = DataObject::getById((int) $request->query->get('sourceId')); - - // first of all the new parent - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_dataobject_dataobject_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => 'child', - 'transactionId' => $transactionId, - 'saveParentId' => true, - ], - ]]; - - if ($object->hasChildren(DataObject::$types)) { - // get amount of children - $list = new DataObject\Listing(); - $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($object->getRealFullPath()) . '/%')); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('ASC'); - $list->setObjectTypes(DataObject::$types); - $childIds = $list->loadIdList(); - - if (count($childIds) > 0) { - foreach ($childIds as $id) { - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_dataobject_dataobject_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $id, - 'targetParentId' => $request->query->get('targetId'), - 'sourceParentId' => $request->query->get('sourceId'), - 'type' => 'child', - 'transactionId' => $transactionId, - ], - ]]; - } - } - - // add id-rewrite steps - if ($request->query->get('type') === 'recursive-update-references') { - for ($i = 0; $i < (count($childIds) + 1); $i++) { - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_dataobject_dataobject_copyrewriteids'), - 'method' => 'PUT', - 'params' => [ - 'transactionId' => $transactionId, - '_dc' => uniqid('', false), - ], - ]]; - } - } - } - } elseif ($request->query->get('type') === 'child' || $request->query->get('type') === 'replace') { - // the object itself is the last one - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_dataobject_dataobject_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => $request->query->get('type'), - 'transactionId' => $transactionId, - ], - ]]; - } - - return $this->adminJson([ - 'pastejobs' => $pasteJobs, - ]); - } - - /** - * @throws Exception - */ - #[Route('/copy-rewrite-ids', name: 'copyrewriteids', methods: ['PUT'])] - public function copyRewriteIdsAction(Request $request): JsonResponse - { - $transactionId = $request->request->get('transactionId'); - - $idStore = Tool\Session::useBag($request->getSession(), static fn (AttributeBagInterface $session) => $session->get($transactionId), 'opendxp_copy'); - - if (!array_key_exists('rewrite-stack', $idStore)) { - $idStore['rewrite-stack'] = array_values($idStore['idMapping']); - } - - $id = array_shift($idStore['rewrite-stack']); - $object = DataObject::getById($id); - - // create rewriteIds() config parameter - $rewriteConfig = ['object' => $idStore['idMapping']]; - - $object = DataObject\Service::rewriteIds($object, $rewriteConfig); - - $object->setUserModification($this->getAdminUser()->getId()); - $object->save(); - - // write the store back to the session - Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($transactionId, $idStore): void { - $session->set($transactionId, $idStore); - }, 'opendxp_copy'); - - return $this->adminJson([ - 'success' => true, - 'id' => $id, - ]); - } - - #[Route('/copy', name: 'copy', methods: ['POST'])] - public function copyAction(Request $request): JsonResponse - { - $message = ''; - $sourceId = $request->request->getInt('sourceId'); - $source = DataObject::getById($sourceId); - - $session = Tool\Session::getSessionBag($request->getSession(), 'opendxp_copy'); - $sessionBag = $session->get($request->request->get('transactionId')); - - $targetId = $request->request->getInt('targetId'); - if ($request->request->has('targetParentId')) { - $sourceParent = DataObject::getById($request->request->getInt('sourceParentId')); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($sessionBag['parentId']) { - $targetParent = DataObject::getById((int) $sessionBag['parentId']); - } else { - $targetParent = DataObject::getById($request->request->getInt('targetParentId')); - } - - $targetPath = preg_replace('@^' . preg_quote($sourceParent->getRealFullPath(), '@') . '@', $targetParent . '/', $source->getRealPath()); - $target = DataObject::getByPath($targetPath); - } else { - $target = DataObject::getById($targetId); - } - - $user = Tool\Admin::getCurrentUser(); - - if ( - $target->isAllowed('create') && - ($source instanceof DataObject\Concrete ? $user->isAllowed($source->getClassId(), 'class') : true) - ) { - $source = DataObject::getById($sourceId); - if ($source instanceof \OpenDxp\Model\DataObject) { - if ($source instanceof DataObject\Concrete && $latestVersion = $source->getLatestVersion()) { - $source = $latestVersion->loadData(); - $source->setPublished(false); //as latest version is used which is not published - } - - if ($request->request->get('type') === 'child') { - $newObject = $this->_objectService->copyAsChild($target, $source); - - $sessionBag['idMapping'][(int)$source->getId()] = (int)$newObject->getId(); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($request->request->get('saveParentId')) { - $sessionBag['parentId'] = $newObject->getId(); - } - } elseif ($request->request->get('type') === 'replace') { - $concreteTarget = DataObject\Concrete::getById($target->getId()); - $concreteSource = DataObject\Concrete::getById($source->getId()); - $this->_objectService->copyContents($concreteTarget, $concreteSource); - } - - $session->set($request->request->get('transactionId'), $sessionBag); - - return $this->adminJson(['success' => true, 'message' => $message]); - } - - Logger::error("could not execute copy/paste, source object with id [ $sourceId ] not found"); - - return $this->adminJson(['success' => false, 'message' => 'source object not found']); + $result = $handler($payload); + if ($result->requestedLanguage && $result->requestedLanguage !== 'default') { + $request->setLocale($result->requestedLanguage); } - throw $this->createAccessDeniedHttpException(); + return $this->adminJson($result->data); } #[Route('/preview', name: 'preview', methods: ['GET'])] - public function previewAction(Request $request, PreviewGeneratorInterface $defaultPreviewGenerator): RedirectResponse|Response - { - $id = $request->query->getInt('id'); - $object = DataObject\Service::getElementFromSession('object', $id, $request->getSession()->getId()); - - if ($object instanceof DataObject\Concrete) { - $url = null; - if ($previewService = $object->getClass()->getPreviewGenerator()) { - $url = $previewService->generatePreviewUrl($object, ['preview' => true, 'context' => $this, ...$request->query->all()]); - } elseif ($object->getClass()->getLinkGenerator()) { - $parameters = [ - 'preview' => true, - 'context' => $this, - ]; - - $url = $defaultPreviewGenerator->generatePreviewUrl($object, [...$parameters, ...$request->query->all()]); - } - - if (!$url) { - throw new NotFoundHttpException('Cannot render preview due to empty URL'); - } - - // replace all remaining % signs - $url = str_replace('%', '%25', $url); - - $urlParts = parse_url($url); - - $redirectParameters = array_filter([ - 'opendxp_object_preview' => $id, - 'site' => $request->query->getInt(PreviewGeneratorInterface::PARAMETER_SITE), - 'dc' => time(), - ]); - - $redirectUrl = $urlParts['path'] . '?' . http_build_query($redirectParameters) . (isset($urlParts['query']) ? '&' . $urlParts['query'] : ''); - - return $this->redirect($redirectUrl); - } - - throw new NotFoundHttpException(sprintf('Expected an object of type "%s", got "%s"', DataObject\Concrete::class, get_debug_type($object))); - } - - protected function processRemoteOwnerRelations(DataObject\Concrete $object, array $toDelete, array $toAdd, string $ownerFieldName): void - { - $getter = 'get' . ucfirst($ownerFieldName); - $setter = 'set' . ucfirst($ownerFieldName); - - foreach ($toDelete as $id) { - $owner = DataObject::getById($id); - //TODO: lock ?! - if (method_exists($owner, $getter)) { - $currentData = $owner->$getter(); - if (is_array($currentData)) { - $counter = count($currentData); - for ($i = 0; $i < $counter; $i++) { - if ($currentData[$i]->getId() == $object->getId()) { - unset($currentData[$i]); - $owner->$setter($currentData); - - break; - } - } - } elseif ($currentData->getId() == $object->getId()) { - $owner->$setter(null); - } - } - $owner->setUserModification($this->getAdminUser()->getId()); - $owner->save(); - Logger::debug('Saved object id [ ' . $owner->getId() . ' ] by remote modification through [' . $object->getId() . '], Action: deleted [ ' . $object->getId() . " ] from [ $ownerFieldName]"); - } - - foreach ($toAdd as $id) { - $owner = DataObject::getById($id); - //TODO: lock ?! - if (method_exists($owner, $getter)) { - $currentData = $owner->$getter(); - if (is_array($currentData)) { - $currentData[] = $object; - } else { - $currentData = $object; - } - $owner->$setter($currentData); - $owner->setUserModification($this->getAdminUser()->getId()); - $owner->save(); - Logger::debug('Saved object id [ ' . $owner->getId() . ' ] by remote modification through [' . $object->getId() . '], Action: added [ ' . $object->getId() . " ] to [ $ownerFieldName ]"); - } - } - } - - protected function detectDeletedRemoteOwnerRelations(array $relations, array $value): array - { - $originals = []; - $changed = []; - foreach ($relations as $r) { - $originals[] = $r['dest_id']; - } - - foreach ($value as $row) { - $changed[] = $row['id']; - } - - return array_diff($originals, $changed); - } - - protected function detectAddedRemoteOwnerRelations(array $relations, array $value): array - { - $originals = []; - $changed = []; - - foreach ($relations as $r) { - $originals[] = $r['dest_id']; - } - - foreach ($value as $row) { - $changed[] = $row['id']; - } - - return array_diff($changed, $originals); - } - - protected function getLatestVersion(DataObject\Concrete $object, ?Model\Version &$draftVersion = null): DataObject\Concrete - { - $latestVersion = $object->getLatestVersion($this->getAdminUser()?->getId()); - if ($latestVersion) { - $latestObj = $latestVersion->loadData(); - if ($latestObj instanceof DataObject\Concrete) { - $draftVersion = $latestVersion; - - return $latestObj; - } - } - - return $object; - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - // check permissions - $this->checkPermission('objects'); - - $this->_objectService = new DataObject\Service($this->getAdminUser()); + public function previewAction( + GetDataObjectPreviewUrlPayload $payload, + GetDataObjectPreviewUrlHandler $handler, + ): RedirectResponse { + return $this->redirect($handler($payload)); } } diff --git a/src/Controller/Admin/DataObject/DataObjectCopyController.php b/src/Controller/Admin/DataObject/DataObjectCopyController.php new file mode 100644 index 00000000..3a75fcb5 --- /dev/null +++ b/src/Controller/Admin/DataObject/DataObjectCopyController.php @@ -0,0 +1,96 @@ +value)] +class DataObjectCopyController extends AdminAbstractController +{ + #[Route('/copy-info', name: 'copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + Request $request, + ): JsonResponse { + $result = $handler($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { + $session->set((string) $result->transactionId, ['idMapping' => []]); + }, 'opendxp_copy'); + + return $this->adminJson(['pastejobs' => $result->pasteJobs]); + } + + #[Route('/copy-rewrite-ids', name: 'copyrewriteids', methods: ['PUT'])] + public function copyRewriteIdsAction( + RewriteDataObjectIdsPayload $payload, + RewriteDataObjectIdsHandler $rewriteIds, + Request $request, + ): JsonResponse { + $rewriteIds($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($payload): void { + $session->set($payload->transactionId, $payload->updatedIdStore); + }, 'opendxp_copy'); + + return $this->adminJson(ApiResponse::ok(['id' => $payload->objectId])); + } + + #[Route('/copy', name: 'copy', methods: ['POST'])] + public function copyAction( + CopyDataObjectPayload $payload, + CopyDataObjectHandler $copyObject, + Request $request, + ): JsonResponse { + $result = $copyObject($payload); + + if ($result->newObject !== null) { + $sessionBag = $payload->sessionBag; + $sessionBag['idMapping'][$result->sourceId] = $result->newObject->getId(); + + if ($payload->saveParentId) { + $sessionBag['parentId'] = $result->newObject->getId(); + } + + Session::getSessionBag($request->getSession(), 'opendxp_copy')->set($payload->transactionId, $sessionBag); + } + + return $this->adminJson(ApiResponse::ok([ + 'message' => $result->newObject?->getRealFullPath() ?? '', + ])); + } +} diff --git a/src/Controller/Admin/DataObject/DataObjectHelperController.php b/src/Controller/Admin/DataObject/DataObjectHelperController.php index bb10862c..6fe56aef 100644 --- a/src/Controller/Admin/DataObject/DataObjectHelperController.php +++ b/src/Controller/Admin/DataObject/DataObjectHelperController.php @@ -16,43 +16,45 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Doctrine\DBAL\ArrayParameterType; -use Doctrine\DBAL\ParameterType; -use Exception; -use InvalidArgumentException; -use League\Flysystem\FilesystemException; -use League\Flysystem\UnableToReadFile; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Helper\GridHelperService; -use OpenDxp\Bundle\AdminBundle\Model\GridConfig; -use OpenDxp\Bundle\AdminBundle\Model\GridConfigFavourite; -use OpenDxp\Bundle\AdminBundle\Model\GridConfigShare; -use OpenDxp\Bundle\AdminBundle\Service\GridData; -use OpenDxp\Config; -use OpenDxp\Db; -use OpenDxp\File; -use OpenDxp\Localization\LocaleServiceInterface; -use OpenDxp\Logger; -use OpenDxp\Model\DataObject; -use OpenDxp\Model\DataObject\Listing; -use OpenDxp\Model\User; -use OpenDxp\Security\SecurityHelper; -use OpenDxp\Tool; -use OpenDxp\Tool\Storage; -use OpenDxp\Version; -use stdClass; -use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Component\Filesystem\Filesystem; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ApplyGridConfigToAll\ApplyGridConfigToAllHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ApplyGridConfigToAll\ApplyGridConfigToAllPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\DeleteGridColumnConfig\DeleteGridColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\DeleteGridColumnConfig\DeleteGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\DoDataObjectExport\DoDataObjectExportHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\DoDataObjectExport\DoDataObjectExportPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ExecuteBatch\ExecuteBatchHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ExecuteBatch\ExecuteBatchPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetAvailableVisibleFields\GetAvailableVisibleFieldsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetAvailableVisibleFields\GetAvailableVisibleFieldsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetBatchJobs\GetBatchJobsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetBatchJobs\GetBatchJobsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetExportConfigs\GetExportConfigsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetExportConfigs\GetExportConfigsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetExportJobs\GetExportJobsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetExportJobs\GetExportJobsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetGridColumnConfig\GetGridColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\GetGridColumnConfig\GetGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ImportUpload\ImportUploadHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\ImportUpload\ImportUploadPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\LoadObjectData\LoadObjectDataHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\LoadObjectData\LoadObjectDataPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\MarkDataObjectGridConfigFavourite\MarkDataObjectGridConfigFavouriteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\MarkDataObjectGridConfigFavourite\MarkDataObjectGridConfigFavouritePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\PrepareHelperColumnConfigs\PrepareHelperColumnConfigsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\PrepareHelperColumnConfigs\PrepareHelperColumnConfigsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\SaveDataObjectGridColumnConfig\SaveDataObjectGridColumnConfigHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\Helper\SaveDataObjectGridColumnConfig\SaveDataObjectGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Service\Grid\GridExportService; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Tool\Session; use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal @@ -60,1077 +62,101 @@ #[Route('/object-helper', name: 'opendxp_admin_dataobject_dataobjecthelper_')] class DataObjectHelperController extends AdminAbstractController { - public const array SYSTEM_COLUMNS = ['id', 'fullpath', 'key', 'published', 'creationDate', 'modificationDate', 'filename', 'classname']; + public function __construct(private readonly GridExportService $gridExportService) {} #[Route('/load-object-data', name: 'loadobjectdata', methods: ['GET'])] - public function loadObjectDataAction(Request $request): JsonResponse - { - $object = DataObject::getById((int) $request->query->get('id')); - $result = []; - if ($object) { - $result['success'] = true; - $fields = $request->query->all('fields'); - $result['fields'] = GridData\DataObject::getData($object, $fields); - } else { - $result['success'] = false; - } - - return $this->adminJson($result); - } - - public function getMyOwnGridColumnConfigs(int $userId, string $classId, ?string $searchType = null): array - { - $db = Db::get(); - $configListingConditionParts = []; - $configListingConditionParts[] = 'ownerId = ' . $userId; - $configListingConditionParts[] = 'classId = ' . $db->quote($classId); - - if ($searchType) { - $configListingConditionParts[] = 'searchType = ' . $db->quote($searchType); - } - - $configCondition = implode(' AND ', $configListingConditionParts); - $configListing = new GridConfig\Listing(); - $configListing->setOrderKey('name'); - $configListing->setOrder('ASC'); - $configListing->setCondition($configCondition); - $configListing = $configListing->load(); - - $configData = []; - foreach ($configListing as $config) { - $configData[] = $config->getObjectVars(); - } - - return $configData; - } - - public function getSharedGridColumnConfigs(User $user, string $classId, ?string $searchType = null): array - { - $configListing = []; - - $userIds = [$user->getId()]; - // collect all roles - $userIds = [...$userIds, ...$user->getRoles()]; - $db = Db::get(); - - $ids = $db->fetchFirstColumn( - 'SELECT DISTINCT c1.id FROM gridconfigs c1, gridconfig_shares s - WHERE (c1.searchType = ? AND c1.id = s.gridConfigId AND s.sharedWithUserId IN (?) AND c1.classId = ?) - UNION DISTINCT SELECT c2.id FROM gridconfigs c2 WHERE shareGlobally = 1 AND c2.classId = ? AND c2.ownerId != ?', - [$searchType, $userIds, $classId, $classId, $user->getId()], - [ParameterType::STRING, ArrayParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER] - ); - - if ($ids) { - $ids = implode(',', $ids); - $configListing = new GridConfig\Listing(); - $configListing->setOrderKey('name'); - $configListing->setOrder('ASC'); - $configListing->setCondition('id in (' . $ids . ')'); - $configListing = $configListing->load(); - } - - $configData = []; - foreach ($configListing as $config) { - $configData[] = $config->getObjectVars(); - } - - return $configData; + public function loadObjectDataAction( + LoadObjectDataPayload $payload, + LoadObjectDataHandler $handler, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['fields' => $handler($payload)])); } #[Route('/get-export-configs', name: 'getexportconfigs', methods: ['GET'])] - public function getExportConfigsAction(Request $request): JsonResponse - { - $result = []; - $classId = $request->query->get('classId'); - - $list = $this->getMyOwnGridColumnConfigs($this->getAdminUser()->getId(), $classId); - $list = [...$list, ...$this->getSharedGridColumnConfigs($this->getAdminUser(), $classId)]; - - $result[] = [ - 'id' => -1, - 'name' => '--default--', - ]; - - /** @var GridConfig $config */ - foreach ($list as $config) { - $result[] = [ - 'id' => $config['id'], - 'name' => $config['name'], - ]; - } - - return $this->adminJson(['success' => true, 'data' => $result]); + public function getExportConfigsAction( + GetExportConfigsPayload $payload, + GetExportConfigsHandler $getExportConfigs, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $getExportConfigs($payload)])); } #[Route('/grid-delete-column-config', name: 'griddeletecolumnconfig', methods: ['DELETE'])] - public function gridDeleteColumnConfigAction(Request $request, EventDispatcherInterface $eventDispatcher, Config $config): JsonResponse - { - $params = [ - 'id' => $request->request->get('id'), - 'objectId' => $request->request->get('objectId'), - 'name' => $request->request->get('name'), - 'type' => $request->request->get('type'), - 'types' => $request->request->get('types'), - 'gridtype' => $request->request->get('gridtype'), - 'gridConfigId' => $request->request->get('gridConfigId'), - 'searchType' => $request->request->get('searchType'), - 'noSystemColumns' => $request->query->getBoolean('no_system_columns'), - 'noBrickColumns' => $request->query->getBoolean('no_brick_columns'), - 'locale' => $request->getLocale(), - ]; - - $gridConfigId = (int)$request->request->get('gridConfigId'); - $gridConfig = GridConfig::getById($gridConfigId); - $success = false; - if ($gridConfig) { - if ($gridConfig->getOwnerId() !== $this->getAdminUser()->getId() && !$this->getAdminUser()->isAdmin()) { - throw new Exception("don't mess with someone elses grid config"); - } - - $gridConfig->delete(); - $success = true; - } - - $newGridConfig = $this->doGetGridColumnConfig($request, $params, $config, true); - $newGridConfig['deleteSuccess'] = $success; - - $event = new GenericEvent($this, [ - 'data' => $newGridConfig, - 'request' => $request, - 'config' => $config, - 'context' => 'delete', - ]); - - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_GRID_GET_COLUMN_CONFIG_PRE_SEND_DATA); - $newGridConfig = $event->getArgument('data'); - - return $this->adminJson($newGridConfig); + public function gridDeleteColumnConfigAction( + DeleteGridColumnConfigPayload $payload, + DeleteGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->adminJson($handler($payload)); } #[Route('/grid-get-column-config', name: 'gridgetcolumnconfig', methods: ['GET'])] - public function gridGetColumnConfigAction(Request $request, EventDispatcherInterface $eventDispatcher, Config $config): JsonResponse - { - $params = [ - 'id' => $request->query->get('id'), - 'objectId' => $request->query->get('objectId'), - 'name' => $request->query->get('name'), - 'type' => $request->query->get('type'), - 'types' => $request->query->get('types'), - 'gridtype' => $request->query->get('gridtype'), - 'gridConfigId' => $request->query->get('gridConfigId'), - 'searchType' => $request->query->get('searchType'), - 'noSystemColumns' => $request->query->getBoolean('no_system_columns'), - 'noBrickColumns' => $request->query->getBoolean('no_brick_columns'), - ]; - - $result = $this->doGetGridColumnConfig($request, $params, $config); - - $event = new GenericEvent($this, [ - 'data' => $result, - 'request' => $request, - 'config' => $config, - 'context' => 'get', - ]); - - $eventDispatcher->dispatch($event, AdminEvents::OBJECT_GRID_GET_COLUMN_CONFIG_PRE_SEND_DATA); - $result = $event->getArgument('data'); - - return $this->adminJson($result); - } - - private function doGetGridColumnConfig(Request $request, array $params, Config $config, bool $isDelete = false): array - { - $class = null; - $fields = null; - - if ($params['id'] !== null) { - $class = DataObject\ClassDefinition::getById($params['id']); - } elseif ($params['name'] !== null) { - $class = DataObject\ClassDefinition::getByName($params['name']); - } - - $gridConfigId = null; - $gridType = 'search'; - if ($params['gridtype'] !== null) { - $gridType = $params['gridtype']; - } - - $objectId = $params['objectId'] !== null ? (int) $params['objectId'] : 0; - - if ($objectId) { - $fields = DataObject\Service::getCustomGridFieldDefinitions($class->getId(), $objectId); - } - - $context = ['purpose' => 'gridconfig']; - if ($class) { - $context['class'] = $class; - } - - if ($objectId) { - $object = DataObject::getById($objectId); - $context['object'] = $object; - } - - if (!$fields && $class) { - $fields = $class->getFieldDefinitions(); - } - - $types = []; - if ($params['types'] !== null) { - $types = explode(',', $params['types']); - } - - $userId = $this->getAdminUser()->getId(); - - $requestedGridConfigId = $isDelete ? null : $params['gridConfigId']; - - // grid config - $gridConfig = []; - $searchType = $params['searchType']; - - if ((string) ($requestedGridConfigId ?? '') === '' && $class) { - // check if there is a favourite view - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $class->getId(), $objectId ?: 0, $searchType); - if (!$favourite && $objectId) { - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $class->getId(), 0, $searchType); - } - - if ($favourite) { - $requestedGridConfigId = $favourite->getGridConfigId(); - } - } - - if (is_numeric($requestedGridConfigId) && $requestedGridConfigId > 0) { - $db = Db::get(); - $savedGridConfig = GridConfig::getById((int) $requestedGridConfigId); - - if ($savedGridConfig) { - $shared = false; - if (!$this->getAdminUser()->isAdmin()) { - $userIds = [$this->getAdminUser()->getId()]; - $userIds = [...$userIds, ...$this->getAdminUser()->getRoles()]; - $isSharedGlobally = $savedGridConfig->getOwnerId() !== $userId && $savedGridConfig->isShareGlobally(); - - $isSharedWithUser = (bool) $db->fetchOne( - 'SELECT 1 FROM gridconfig_shares WHERE sharedWithUserId IN (?) AND gridConfigId = ?', - [$userIds, $savedGridConfig->getId()], - [ArrayParameterType::INTEGER, ParameterType::INTEGER] - ); - - $shared = $isSharedGlobally || $isSharedWithUser; - - if (!$shared && $savedGridConfig->getOwnerId() !== $this->getAdminUser()->getId()) { - throw new Exception('You are neither the owner of this config nor it is shared with you'); - } - } - - $gridConfigId = $savedGridConfig->getId(); - $gridConfig = $savedGridConfig->getConfig(); - $gridConfig = json_decode($gridConfig, true); - $gridConfigName = SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getName()); - $owner = $savedGridConfig->getOwnerId(); - $ownerObject = User::getById($owner); - if ($ownerObject instanceof User) { - $owner = $ownerObject->getName(); - } - $modificationDate = $savedGridConfig->getModificationDate(); - $gridConfigDescription = SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getDescription()); - $sharedGlobally = $savedGridConfig->isShareGlobally(); - $setAsFavourite = $savedGridConfig->isSetAsFavourite(); - $saveFilters = $savedGridConfig->isSaveFilters(); - - foreach ($gridConfig['columns'] as &$column) { - if (array_key_exists('isOperator', $column) && $column['isOperator']) { - $colAttributes = &$column['fieldConfig']['attributes']; - SecurityHelper::convertHtmlSpecialCharsArrayKeys($colAttributes, ['label', 'attribute', 'param1']); - } - } - } - } - - $localizedFields = []; - if (is_array($fields)) { - foreach ($fields as $field) { - if ($field instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $localizedFields[] = $field; - } - } - } - - $availableFields = []; - - if (empty($gridConfig)) { - $availableFields = $this->getDefaultGridFields( - $params['noSystemColumns'], - $class, - $gridType, - $params['noBrickColumns'], - $fields, - $context, - $objectId, - $types - ); - } else { - $savedColumns = $gridConfig['columns']; - foreach ($savedColumns as $key => $sc) { - if (!$sc['hidden']) { - if (in_array($key, self::SYSTEM_COLUMNS)) { - $colConfig = [ - 'key' => $key, - 'type' => 'system', - 'label' => $key, - 'position' => $sc['position'], - ]; - $this->injectCustomLayoutValues($colConfig, $sc); - $availableFields[] = $colConfig; - } else { - $keyParts = explode('~', $key); - - if (str_starts_with($key, '~')) { - // not needed for now - $type = $keyParts[1]; - $groupAndKeyId = explode('-', $keyParts[3]); - $keyId = (int) $groupAndKeyId[1]; - - if ($type === 'classificationstore') { - $keyDef = DataObject\Classificationstore\KeyConfig::getById($keyId); - if ($keyDef) { - $keyFieldDef = json_decode($keyDef->getDefinition(), true); - if ($keyFieldDef) { - $keyFieldDef = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($keyFieldDef, $keyDef->getType()); - $fieldConfig = $this->getFieldGridConfig($keyFieldDef, $gridType, (string)$sc['position'], true, null, $class, $objectId); - if ($fieldConfig) { - $fieldConfig['key'] = $key; - $fieldConfig['label'] = '#' . $keyFieldDef->getTitle(); - $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); - $availableFields[] = $fieldConfig; - } - } - } - } - } elseif (count($keyParts) > 1) { - $brick = $keyParts[0]; - $brickDescriptor = null; - - if (str_contains($brick, '?')) { - $brickDescriptor = substr($brick, 1); - $brickDescriptor = json_decode($brickDescriptor, true); - $keyPrefix = $brick . '~'; - $brick = $brickDescriptor['containerKey']; - } else { - $keyPrefix = $brick . '~'; - } - - $fieldname = $keyParts[1]; - - $brickClass = DataObject\Objectbrick\Definition::getByKey($brick); - - $fd = null; - if ($brickClass instanceof DataObject\Objectbrick\Definition) { - if ($brickDescriptor) { - $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; - /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedFields */ - $localizedFields = $brickClass->getFieldDefinition($innerContainer); - $fd = $localizedFields->getFieldDefinition($brickDescriptor['brickfield']); - } else { - $fd = $brickClass->getFieldDefinition($fieldname); - } - } - - if ($fd !== null) { - $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string)$sc['position'], true, $keyPrefix, $class, $objectId); - if (!empty($fieldConfig)) { - $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); - $availableFields[] = $fieldConfig; - } - } - } elseif (DataObject\Service::isHelperGridColumnConfig($key)) { - $calculatedColumnConfig = $this->getCalculatedColumnConfig($request, $savedColumns[$key]); - if ($calculatedColumnConfig) { - $availableFields[] = $calculatedColumnConfig; - } - } else { - $fd = $class->getFieldDefinition($key); - //if not found, look for localized fields - if (empty($fd)) { - foreach ($localizedFields as $lf) { - $fd = $lf->getFieldDefinition($key); - if (!empty($fd)) { - break; - } - } - } - - if (!empty($fd)) { - $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string)$sc['position'], true, null, $class, $objectId); - if (!empty($fieldConfig)) { - $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); - $availableFields[] = $fieldConfig; - } - } - } - } - } - } - } - - usort($availableFields, static fn ($a, $b) => $a['position'] <=> $b['position']); - - $frontendLanguages = Tool\Admin::reorderWebsiteLanguages(\OpenDxp\Tool\Admin::getCurrentUser(), $config['general']['valid_languages']); - $language = $frontendLanguages ? $frontendLanguages[0] : $request->getLocale(); - - if (!Tool::isValidLanguage($language)) { - $validLanguages = Tool::getValidLanguages(); - $language = $validLanguages[0]; - } - - if (!empty($gridConfig) && !empty($gridConfig['language'])) { - $language = $gridConfig['language']; - } - - $availableConfigs = $class ? $this->getMyOwnGridColumnConfigs($userId, $class->getId(), $searchType) : []; - $sharedConfigs = $class ? $this->getSharedGridColumnConfigs($this->getAdminUser(), $class->getId(), $searchType) : []; - $settings = $this->getShareSettings((int)$gridConfigId); - $settings['gridConfigId'] = (int)$gridConfigId; - $settings['gridConfigName'] = $gridConfigName ?? null; - $settings['gridConfigDescription'] = $gridConfigDescription ?? null; - $settings['owner'] = $owner ?? null; - $settings['modificationDate'] = $modificationDate ?? null; - $settings['shareGlobally'] = $sharedGlobally ?? null; - $settings['setAsFavourite'] = $setAsFavourite ?? null; - $settings['saveFilters'] = $saveFilters ?? null; - $settings['isShared'] = !$gridConfigId || ($shared ?? null); - $settings['allowVariants'] = $class && $class->getAllowVariants(); - - $context = $gridConfig['context'] ?? null; - if ($context) { - $context = json_decode($context, true); - } - - return [ - 'sortinfo' => $gridConfig['sortinfo'] ?? false, - 'language' => $language, - 'availableFields' => $availableFields, - 'settings' => $settings, - 'onlyDirectChildren' => $gridConfig['onlyDirectChildren'] ?? false, - 'pageSize' => $gridConfig['pageSize'] ?? false, - 'availableConfigs' => $availableConfigs, - 'sharedConfigs' => $sharedConfigs, - 'context' => $context, - 'searchFilter' => $gridConfig['searchFilter'] ?? '', - 'filter' => $gridConfig['filter'] ?? [], - ]; - } - - private function injectCustomLayoutValues(array $fieldConfig, array $savedColumn): array - { - $keys = ['width', 'locked']; - foreach ($keys as $key) { - if (isset($savedColumn[$key])) { - $fieldConfig[$key] = $savedColumn[$key]; - } - } - - $fieldConfigKeys = ['noteditable']; - foreach ($fieldConfigKeys as $fieldConfigKey) { - if (isset($savedColumn['fieldConfig']['layout'][$fieldConfigKey])) { - $setter = 'set' . ucfirst($fieldConfigKey); - $fieldConfig['layout']->$setter($savedColumn['fieldConfig']['layout'][$fieldConfigKey]); - } - } - - return $fieldConfig; - } - - /** - * @param DataObject\ClassDefinition\Data[]|null $fields - */ - public function getDefaultGridFields(bool $noSystemColumns, ?DataObject\ClassDefinition $class, string $gridType, bool $noBrickColumns, ?array $fields, array $context, int $objectId, array $types = []): array - { - $count = 0; - $availableFields = []; - - if (!$noSystemColumns && $class) { - $vis = $class->getPropertyVisibility(); - foreach (self::SYSTEM_COLUMNS as $sc) { - $key = $sc; - if ($key === 'fullpath') { - $key = 'path'; - } - - if ($types === [] && (!empty($vis[$gridType][$key]) || $gridType === 'all')) { - $availableFields[] = [ - 'key' => $sc, - 'type' => 'system', - 'label' => $sc, - 'position' => $count, ]; - $count++; - } - } - } - - $includeBricks = !$noBrickColumns; - - if (is_array($fields)) { - foreach ($fields as $field) { - if ($field instanceof DataObject\ClassDefinition\Data\Localizedfields) { - foreach ($field->getFieldDefinitions($context) as $fd) { - if ($types === [] || in_array($fd->getFieldType(), $types)) { - $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string)$count, false, null, $class, $objectId); - if (!empty($fieldConfig)) { - $availableFields[] = $fieldConfig; - $count++; - } - } - } - } elseif ($field instanceof DataObject\ClassDefinition\Data\Objectbricks && $includeBricks) { - if (in_array($field->getFieldType(), $types)) { - $fieldConfig = $this->getFieldGridConfig($field, $gridType, (string)$count, false, null, $class, $objectId); - if (!empty($fieldConfig)) { - $availableFields[] = $fieldConfig; - $count++; - } - } else { - $allowedTypes = $field->getAllowedTypes(); - foreach ($allowedTypes as $t) { - $brickClass = DataObject\Objectbrick\Definition::getByKey($t); - $brickFields = $brickClass->getFieldDefinitions($context); - - $this->appendBrickFields($field, $brickFields, $availableFields, $gridType, $count, $t, $class, $objectId); - } - } - } elseif ($types === [] || in_array($field->getFieldType(), $types)) { - $fieldConfig = $this->getFieldGridConfig($field, $gridType, (string)$count, $types !== [], null, $class, $objectId); - if (!empty($fieldConfig)) { - $availableFields[] = $fieldConfig; - $count++; - } - } - } - } - - return $availableFields; - } - - /** - * @param DataObject\ClassDefinition\Data[] $brickFields - */ - protected function appendBrickFields(DataObject\ClassDefinition\Data $field, array $brickFields, array &$availableFields, string $gridType, int &$count, string $brickType, DataObject\ClassDefinition $class, int $objectId, ?array $context = null): void - { - foreach ($brickFields as $bf) { - if ($bf instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $localizedFieldDefinitions = $bf->getFieldDefinitions(); - - $localizedContext = [ - 'containerKey' => $brickType, - 'fieldname' => $field->getName(), - ]; - - $this->appendBrickFields($bf, $localizedFieldDefinitions, $availableFields, $gridType, $count, $brickType, $class, $objectId, $localizedContext); - } else { - if ($context) { - $context['brickfield'] = $bf->getName(); - $keyPrefix = '?' . json_encode($context) . '~'; - } else { - $keyPrefix = $brickType . '~'; - } - $fieldConfig = $this->getFieldGridConfig($bf, $gridType, (string)$count, false, $keyPrefix, $class, $objectId); - if (!empty($fieldConfig)) { - $availableFields[] = $fieldConfig; - $count++; - } - } - } - } - - protected function getCalculatedColumnConfig(Request $request, array $config): mixed - { - try { - return Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($config) { - - $existingKey = $config['fieldConfig']['key']; - $calculatedColumnConfig['key'] = $existingKey; - $calculatedColumnConfig['position'] = $config['position']; - $calculatedColumnConfig['isOperator'] = true; - $calculatedColumnConfig['attributes'] = $config['fieldConfig']['attributes']; - $calculatedColumnConfig['width'] = $config['width']; - $calculatedColumnConfig['locked'] = $config['locked']; - - $existingColumns = $session->get('helpercolumns', []); - - if (isset($existingColumns[$existingKey])) { - // if the configuration is still in the session, then reuse it - return $calculatedColumnConfig; - } - - $newKey = '#' . uniqid('', false); - $calculatedColumnConfig['key'] = $newKey; - - // prepare a column config on the fly - $phpConfig = json_encode($config['fieldConfig']); - $phpConfig = json_decode($phpConfig); - $helperColumns = []; - $helperColumns[$newKey] = $phpConfig; - - $helperColumns = [...$helperColumns, ...$existingColumns]; - $session->set('helpercolumns', $helperColumns); - - return $calculatedColumnConfig; - }, 'opendxp_gridconfig'); - } catch (Exception $e) { - Logger::error((string) $e); - } - - return null; + public function gridGetColumnConfigAction( + GetGridColumnConfigPayload $payload, + GetGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->adminJson($handler($payload)); } #[Route('/prepare-helper-column-configs', name: 'preparehelpercolumnconfigs', methods: ['POST'])] - public function prepareHelperColumnConfigs(Request $request): JsonResponse - { - $helperColumns = []; - $newData = []; - /** @var stdClass[] $data */ - $data = json_decode($request->request->get('columns')); - foreach ($data as $item) { - if (!empty($item->isOperator)) { - $itemKey = '#' . uniqid('', false); - - $item->key = $itemKey; - $newData[] = $item; - $helperColumns[$itemKey] = $item; - } else { - $newData[] = $item; - } - } + public function prepareHelperColumnConfigs( + PrepareHelperColumnConfigsPayload $payload, + PrepareHelperColumnConfigsHandler $prepareHelperColumns, + Request $request, + ): JsonResponse { + $result = $prepareHelperColumns($payload); - Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($helperColumns): void { + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { $existingColumns = $session->get('helpercolumns', []); - $helperColumns = [...$helperColumns, ...$existingColumns]; - $session->set('helpercolumns', $helperColumns); + $session->set('helpercolumns', [...$result['helperColumns'], ...$existingColumns]); }, 'opendxp_gridconfig'); - return $this->adminJson(['success' => true, 'columns' => $newData]); + return $this->adminJson(ApiResponse::ok(['columns' => $result['newData']])); } #[Route('/grid-config-apply-to-all', name: 'gridconfigapplytoall', methods: ['POST'])] - public function gridConfigApplyToAllAction(Request $request): JsonResponse - { - $objectId = $request->request->getInt('objectId'); - $object = DataObject::getById($objectId); - - if ($object->isAllowed('list')) { - $classId = $request->request->get('classId'); - $searchType = $request->request->get('searchType'); - $user = $this->getAdminUser(); - $db = Db::get(); - $db->executeStatement( - 'DELETE FROM gridconfig_favourites WHERE ownerId = ? AND classId = ? AND searchType = ? AND objectId != ? AND objectId != 0', - [$user->getId(), $classId, $searchType, $objectId] - ); - - return $this->adminJson(['success' => true]); - } + public function gridConfigApplyToAllAction( + ApplyGridConfigToAllPayload $payload, + ApplyGridConfigToAllHandler $applyToAll, + ): JsonResponse { + $applyToAll($payload); - throw $this->createAccessDeniedHttpException(); + return $this->adminJson(ApiResponse::ok()); } #[Route('/grid-mark-favourite-column-config', name: 'gridmarkfavouritecolumnconfig', methods: ['POST'])] - public function gridMarkFavouriteColumnConfigAction(Request $request): JsonResponse - { - $objectId = $request->request->getInt('objectId'); - $object = DataObject::getById($objectId); - - if ($object->isAllowed('list')) { - $classId = $request->request->get('classId'); - $gridConfigId = $request->request->get('gridConfigId'); - $searchType = $request->request->get('searchType'); - $global = $request->request->get('global'); - $user = $this->getAdminUser(); - $type = $request->request->get('type'); - - $favourite = new GridConfigFavourite(); - $favourite->setOwnerId($user->getId()); - $class = DataObject\ClassDefinition::getById($classId); - if (!$class) { - throw new Exception('class ' . $classId . ' does not exist anymore'); - } - $favourite->setClassId($classId); - $favourite->setSearchType($searchType); - $favourite->setType($type); - $specializedConfigs = false; - - try { - if ($gridConfigId != 0) { - $gridConfig = GridConfig::getById((int)$gridConfigId); - $favourite->setGridConfigId($gridConfig->getId()); - } - $favourite->setObjectId($objectId); - $favourite->save(); - - if ($global) { - $favourite->setObjectId(0); - $favourite->save(); - } - $db = Db::get(); - $count = $db->fetchOne( - 'SELECT * FROM gridconfig_favourites WHERE ownerId = ? AND classId = ? AND searchType = ? AND objectId != ? AND objectId != 0 AND `type` != ?', - [$user->getId(), $classId, $searchType, $objectId, $type] - ); - $specializedConfigs = $count > 0; - } catch (Exception) { - $favourite->delete(); - } - - return $this->adminJson(['success' => true, 'specializedConfigs' => $specializedConfigs]); - } - - throw $this->createAccessDeniedHttpException(); - } - - protected function getShareSettings(int $gridConfigId): array - { - $result = [ - 'sharedUserIds' => [], - 'sharedRoleIds' => [], - ]; - - $db = Db::get(); - $allShares = $db->fetchAllAssociative( - 'SELECT s.sharedWithUserId, u.type FROM gridconfig_shares s, users u - WHERE s.sharedWithUserId = u.id AND s.gridConfigId = ?', - [$gridConfigId] - ); - - foreach ($allShares as $share) { - $type = $share['type']; - $key = 'shared' . ucfirst($type) . 'Ids'; - $result[$key][] = $share['sharedWithUserId']; - } - - foreach ($result as $idx => $value) { - $value = $value ? implode(',', $value) : ''; - $result[$idx] = $value; - } + public function gridMarkFavouriteColumnConfigAction( + MarkDataObjectGridConfigFavouritePayload $payload, + MarkDataObjectGridConfigFavouriteHandler $markFavourite, + ): JsonResponse { + $result = $markFavourite($payload); - return $result; + return $this->adminJson(ApiResponse::ok(['specializedConfigs' => $result->specializedConfigs])); } #[Route('/grid-save-column-config', name: 'gridsavecolumnconfig', methods: ['POST'])] - public function gridSaveColumnConfigAction(Request $request): JsonResponse - { - $objectId = $request->request->getInt('id'); - $object = DataObject::getById($objectId); - - if ($object->isAllowed('list')) { - try { - $classId = $request->request->get('class_id'); - $context = $request->request->get('context'); - - $searchType = $request->request->get('searchType'); - - // grid config - $gridConfigData = $this->decodeJson($request->request->get('gridconfig')); - $gridConfigData['opendxp_version'] = Version::getVersion(); - $gridConfigData['opendxp_revision'] = Version::getRevision(); - - $gridConfigData['context'] = $context; - - unset($gridConfigData['settings']['isShared']); - - $metadata = $request->request->get('settings'); - $metadata = json_decode($metadata, true); - - $gridConfigId = $metadata['gridConfigId']; - $gridConfig = null; - if ($gridConfigId) { - $gridConfig = GridConfig::getById($gridConfigId); - } - - if ($gridConfig && $gridConfig->getOwnerId() !== $this->getAdminUser()->getId() && !$this->getAdminUser()->isAdmin()) { - throw new Exception("don't mess around with somebody elses configuration"); - } - - $this->updateGridConfigShares($gridConfig, $metadata); - - if ($metadata['setAsFavourite'] && $this->getAdminUser()->isAdmin()) { - $this->updateGridConfigFavourites($gridConfig, $metadata, $objectId); - } - - if (!$gridConfig) { - $gridConfig = new GridConfig(); - $gridConfig->setName(date('c')); - $gridConfig->setClassId($classId); - $gridConfig->setSearchType($searchType); - - $gridConfig->setOwnerId($this->getAdminUser()->getId()); - } - - if ($metadata) { - $gridConfig->setName(SecurityHelper::convertHtmlSpecialChars($metadata['gridConfigName'])); - $gridConfig->setDescription(SecurityHelper::convertHtmlSpecialChars($metadata['gridConfigDescription'])); - $gridConfig->setShareGlobally($metadata['shareGlobally'] && $this->getAdminUser()->isAdmin()); - $gridConfig->setSetAsFavourite($metadata['setAsFavourite'] && $this->getAdminUser()->isAdmin()); - $gridConfig->setSaveFilters($metadata['saveFilters'] ?? false); - } - - $gridConfigData = json_encode($gridConfigData); - $gridConfig->setConfig($gridConfigData); - $gridConfig->save(); - - $userId = $this->getAdminUser()->getId(); - - $availableConfigs = $this->getMyOwnGridColumnConfigs($userId, $classId, $searchType); - $sharedConfigs = $this->getSharedGridColumnConfigs($this->getAdminUser(), $classId, $searchType); - - $settings = $this->getShareSettings($gridConfig->getId()); - $settings['gridConfigId'] = (int)$gridConfig->getId(); - $settings['gridConfigName'] = SecurityHelper::convertHtmlSpecialChars($gridConfig->getName()); - $settings['gridConfigDescription'] = SecurityHelper::convertHtmlSpecialChars($gridConfig->getDescription()); - $settings['shareGlobally'] = $gridConfig->isShareGlobally(); - $settings['setAsFavourite'] = $gridConfig->isSetAsFavourite(); - $settings['saveFilters'] = $gridConfig->isSaveFilters(); - $settings['isShared'] = $gridConfig->getOwnerId() !== $this->getAdminUser()->getId() && !$this->getAdminUser()->isAdmin(); - - return $this->adminJson([ - 'success' => true, - 'settings' => $settings, - 'availableConfigs' => $availableConfigs, - 'sharedConfigs' => $sharedConfigs, - ]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - throw $this->createAccessDeniedHttpException(); - } - - /** - * @throws Exception - */ - protected function updateGridConfigShares(?GridConfig $gridConfig, array $metadata): void - { - $user = $this->getAdminUser(); - if (!$gridConfig || !$user->isAllowed('share_configurations')) { - // nothing to do - return; - } - - if ($gridConfig->getOwnerId() !== $user->getId() && !$user->isAdmin()) { - throw new Exception("don't mess with someone elses grid config"); - } - $combinedShares = []; - $sharedUserIds = $metadata['sharedUserIds']; - $sharedRoleIds = $metadata['sharedRoleIds']; - - if ($sharedUserIds) { - $combinedShares = explode(',', $sharedUserIds); - } - - if ($sharedRoleIds) { - $sharedRoleIds = explode(',', $sharedRoleIds); - $combinedShares = [...$combinedShares, ...$sharedRoleIds]; - } - - $db = Db::get(); - $db->delete('gridconfig_shares', ['gridConfigId' => $gridConfig->getId()]); - - foreach ($combinedShares as $id) { - $share = new GridConfigShare(); - $share->setGridConfigId($gridConfig->getId()); - $share->setSharedWithUserId((int) $id); - $share->save(); - } - } - - /** - * @throws Exception - */ - protected function updateGridConfigFavourites(?GridConfig $gridConfig, array $metadata, int $objectId): void - { - $currentUser = $this->getAdminUser(); - - if (!$gridConfig || $currentUser === null || !$currentUser->isAllowed('share_configurations')) { - // nothing to do - return; - } - - if (!$currentUser->isAdmin() && (int) $gridConfig->getOwnerId() !== $currentUser->getId()) { - throw new Exception("don't mess with someone elses grid config"); - } - - $sharedUsers = []; - - if ($metadata['shareGlobally'] === false) { - $sharedUserIds = $metadata['sharedUserIds']; - - if ($sharedUserIds) { - $sharedUsers = array_map(intval(...), explode(',', $sharedUserIds)); - } - } - - if ($metadata['shareGlobally'] === true) { - $users = new User\Listing(); - $users->setCondition('id = ?', $currentUser->getId()); - - foreach ($users as $user) { - $sharedUsers[] = $user->getId(); - } - } - - foreach ($sharedUsers as $id) { - $global = true; - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId( - $id, - $gridConfig->getClassId(), - $objectId, - $gridConfig->getSearchType() - ); - - // If the user has already a favourite for that object we check the current favourite and decide if we update - if ($favourite instanceof GridConfigFavourite) { - $favouriteGridConfig = GridConfig::getById($favourite->getGridConfigId()); - - if ($favouriteGridConfig instanceof GridConfig) { - // Check if the grid config was shared globally if that is *not* the case we also not update - if ($favouriteGridConfig->isShareGlobally() === false) { - continue; - } - - // Check if the user is the owner. If that is the case we do not update the favourite - if ($favouriteGridConfig->getOwnerId() === $id) { - continue; - } - } - } - - // Check if the user has already a global favourite then we do not save the favourite as global - $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId( - $id, - $gridConfig->getClassId(), - 0, - $gridConfig->getSearchType() - ); - - if ($favourite instanceof GridConfigFavourite) { - $favouriteGridConfig = GridConfig::getById($favourite->getGridConfigId()); - - if ($favouriteGridConfig instanceof GridConfig) { - // Check if the grid config was shared globally if that is *not* the case we also not update - if ($favouriteGridConfig->isShareGlobally() === false) { - $global = false; - } - - // Check if the user is the owner. If that is the case we do not update the global favourite - if ($favouriteGridConfig->getOwnerId() === $id) { - $global = false; - } - } - } - - $favourite = new GridConfigFavourite(); - $favourite->setGridConfigId($gridConfig->getId()); - $favourite->setClassId($gridConfig->getClassId()); - $favourite->setObjectId($objectId); - $favourite->setOwnerId($id); - $favourite->setType($gridConfig->getType()); - $favourite->setSearchType($gridConfig->getSearchType()); - $favourite->save(); - - if ($global) { - $favourite->setObjectId(0); - $favourite->save(); - } - } - } - - protected function getFieldGridConfig(DataObject\ClassDefinition\Data $field, string $gridType, string $position, bool $force = false, ?string $keyPrefix = null, ?DataObject\ClassDefinition $class = null, ?int $objectId = null): ?array - { - $key = $keyPrefix . $field->getName(); - $config = null; - $title = $field->getName(); - - if (!empty($field->getTitle())) { - $title = $field->getTitle(); - } - - if ($field instanceof DataObject\ClassDefinition\Data\Slider) { - $config['minValue'] = $field->getMinValue(); - $config['maxValue'] = $field->getMaxValue(); - $config['increment'] = $field->getIncrement(); - } - - if (method_exists($field, 'getWidth')) { - $config['width'] = $field->getWidth(); - } - if (method_exists($field, 'getHeight')) { - $config['height'] = $field->getHeight(); - } - - $visible = false; - if ($gridType === 'search') { - $visible = $field->getVisibleSearch(); - } elseif ($gridType === 'grid') { - $visible = $field->getVisibleGridView(); - } elseif ($gridType === 'all') { - $visible = true; - } - - if (!$field->getInvisible() && ($force || $visible)) { - $context = ['purpose' => 'gridconfig']; - if ($class) { - $context['class'] = $class; - } - - if ($objectId) { - $object = DataObject::getById($objectId); - $context['object'] = $object; - } - DataObject\Service::enrichLayoutDefinition($field, null, $context); - - $result = [ - 'key' => $key, - 'type' => $field->getFieldType(), - 'label' => $title, - 'config' => $config, - 'layout' => $field, - 'position' => $position, - ]; - - if ($field instanceof DataObject\ClassDefinition\Data\EncryptedField) { - $result['delegateDatatype'] = $field->getDelegateDatatype(); - } - - return $result; - } + public function gridSaveColumnConfigAction( + SaveDataObjectGridColumnConfigPayload $payload, + SaveDataObjectGridColumnConfigHandler $saveGridColumnConfig, + ): JsonResponse { + $result = $saveGridColumnConfig($payload); - return null; + return $this->adminJson(ApiResponse::ok([ + 'settings' => $result->settings, + 'availableConfigs' => $result->availableConfigs, + 'sharedConfigs' => $result->sharedConfigs, + ])); } /** * IMPORTER */ #[Route('/import-upload', name: 'importupload', methods: ['POST'])] - public function importUploadAction(Request $request, Filesystem $filesystem): JsonResponse - { - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - - $data = file_get_contents($file->getPathname()); - $data = Tool\Text::convertToUTF8($data); - - $importId = $request->request->get('importId'); - $importId = str_replace('..', '', $importId); - $importFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/import_' . $importId; - $filesystem->dumpFile($importFile, $data); - - $importFileOriginal = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/import_' . $importId . '_original'; - $filesystem->dumpFile($importFileOriginal, $data); + public function importUploadAction( + ImportUploadPayload $payload, + ImportUploadHandler $importUpload, + ): JsonResponse { + $importUpload($payload); - $response = $this->adminJson([ - 'success' => true, - ]); + $response = $this->adminJson(ApiResponse::ok()); // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in // Ext.form.Action.Submit and mark the submission as failed @@ -1139,572 +165,94 @@ public function importUploadAction(Request $request, Filesystem $filesystem): Js return $response; } - protected function extractLanguage(Request $request): string - { - $requestedLanguage = $request->request->get('language'); - if ($requestedLanguage) { - if ($requestedLanguage !== 'default') { - $request->setLocale($requestedLanguage); - } - } else { - $requestedLanguage = $request->getLocale(); - } - - return $requestedLanguage; - } - - protected function getCsvFile(string $fileHandle): string - { - return $fileHandle . '.csv'; - } - #[Route('/get-export-jobs', name: 'getexportjobs', methods: ['POST'])] - public function getExportJobsAction(Request $request, GridHelperService $gridHelperService, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $requestedLanguage = $this->extractLanguage($request); - $allParams = [...$request->request->all(), ...$request->query->all()]; - - //prepare fields - $fieldnames = []; - $fields = json_decode($allParams['fields'][0], true); - foreach ($fields as $field) { - $fieldnames[] = $field['key']; + public function getExportJobsAction( + GetExportJobsPayload $payload, + GetExportJobsHandler $handler, + Request $request, + ): JsonResponse { + if ($payload->requestedLanguage !== $request->getLocale()) { + $request->setLocale($payload->requestedLanguage); } - $allParams['fields'] = $fieldnames; - - $list = $gridHelperService->prepareListingForGrid($allParams, $requestedLanguage, $this->getAdminUser()); - - $beforeListPrepareEvent = new GenericEvent($this, [ - 'list' => $list, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListPrepareEvent, AdminEvents::OBJECT_LIST_BEFORE_EXPORT_PREPARE); - $list = $beforeListPrepareEvent->getArgument('list'); + $result = $handler($payload); - $ids = $list->loadIdList(); - - $jobs = array_chunk($ids, 20); - - $fileHandle = uniqid('export-'); - - $storage = Storage::get('temp'); - $storage->write($this->getCsvFile($fileHandle), ''); - - return $this->adminJson(['success' => true, 'jobs' => $jobs, 'fileHandle' => $fileHandle]); + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs, 'fileHandle' => $result->fileHandle])); } - /** - * @throws Exception|FilesystemException - */ #[Route('/do-export', name: 'doexport', methods: ['POST'])] public function doExportAction( + DoDataObjectExportPayload $payload, + DoDataObjectExportHandler $doExport, Request $request, - LocaleServiceInterface $localeService, - EventDispatcherInterface $eventDispatcher ): JsonResponse { - $fileHandle = File::getValidFilename($request->request->get('fileHandle')); - $ids = $request->request->all('ids'); - $settings = json_decode($request->request->get('settings'), true); - $delimiter = $settings['delimiter'] ?? ';'; - $header = $settings['header'] ?? 'title'; - Tool\UserTimezone::setUserTimezone($request->request->get('userTimezone')); - - $allParams = [...$request->request->all(), ...$request->query->all()]; - - $enableInheritance = $settings['enableInheritance'] ?? false; - DataObject\Concrete::setGetInheritedValues($enableInheritance); - - $class = DataObject\ClassDefinition::getById($request->request->get('classId')); - - if (!$class) { - throw new InvalidArgumentException('No class definition found'); - } - - $className = $class->getName(); - $listClass = '\\OpenDxp\\Model\\DataObject\\' . ucfirst($className) . '\\Listing'; - - /** @var Listing $list */ - $list = new $listClass(); - - $quotedIds = []; - foreach ($ids as $id) { - $quotedIds[] = $list->quote($id); - } - - $list->setObjectTypes(DataObject::$types); - $list->setCondition('id IN (' . implode(',', $quotedIds) . ')'); - $list->setOrderKey(' FIELD(id, ' . implode(',', $quotedIds) . ')', false); - - $beforeListExportEvent = new GenericEvent($this, [ - 'list' => $list, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListExportEvent, AdminEvents::OBJECT_LIST_BEFORE_EXPORT); - - $list = $beforeListExportEvent->getArgument('list'); - - $fields = json_decode($request->request->all('fields')[0], true); - - $addTitles = (bool) $request->request->get('initial'); - - $requestedLanguage = $this->extractLanguage($request); - - $context = [ - 'source' => 'opendxp-export', - ]; - - $contextFromRequest = $request->request->get('context'); - if ($contextFromRequest) { - $contextFromRequest = json_decode($contextFromRequest, true); - $context = [...$context, ...$contextFromRequest]; - } - - $csv = DataObject\Service::getCsvData( - $requestedLanguage, - $localeService, - $list, - $fields, - $header, - $addTitles, - $context - ); - - $temp = tmpfile(); - - try { - $storage = Storage::get('temp'); - $csvFile = $this->getCsvFile($fileHandle); - - $fileStream = $storage->readStream($csvFile); - - stream_copy_to_stream($fileStream, $temp, null, 0); - - $firstLine = true; - - if ($request->request->get('initial') && $header === 'no_header') { - array_shift($csv); - $firstLine = false; - } - - $lineCount = count($csv); - - if (!$addTitles && $lineCount > 0) { - fwrite($temp, "\r\n"); - } - - for ($i = 0; $i < $lineCount; $i++) { - $line = $csv[$i]; - if ($addTitles && $firstLine) { - $firstLine = false; - $line = implode($delimiter, $line); - fwrite($temp, $line); - } else { - fwrite($temp, implode($delimiter, array_map($this->encodeFunc(...), $line))); - } - if ($i < $lineCount - 1) { - fwrite($temp, "\r\n"); - } - } - $storage->writeStream($csvFile, $temp); - } catch (UnableToReadFile $exception) { - Logger::err($exception->getMessage()); - - return $this->adminJson( - [ - 'success' => false, - 'message' => sprintf('export file not found: %s', $fileHandle), - ] - ); - } finally { - if (is_resource($temp)) { - fclose($temp); - } + if ($payload->requestedLanguage !== $request->getLocale()) { + $request->setLocale($payload->requestedLanguage); } - return $this->adminJson(['success' => true]); - } - - public function encodeFunc(string $value): string - { - $value = str_replace('"', '""', $value); + $doExport($payload); - //force wrap value in quotes and return - return '"' . $value . '"'; + return $this->adminJson(ApiResponse::ok()); } #[Route('/download-csv-file', name: 'downloadcsvfile', methods: ['GET'])] - public function downloadCsvFileAction(Request $request): Response - { - $storage = Storage::get('temp'); - $fileHandle = File::getValidFilename($request->query->get('fileHandle')); - $csvFile = $this->getCsvFile($fileHandle); - + public function downloadCsvFileAction( + #[MapQueryParameter] ?string $fileHandle = null, + ): Response { try { - $csvData = $storage->read($csvFile); - $response = new Response($csvData); - $response->headers->set('Content-Type', 'application/csv'); - $disposition = HeaderUtils::makeDisposition( - HeaderUtils::DISPOSITION_ATTACHMENT, - 'export.csv' - ); - - $response->headers->set('Content-Disposition', $disposition); - $storage->delete($csvFile); - - return $response; - } catch (FilesystemException | UnableToReadFile) { - // handle the error + return $this->gridExportService->downloadCsvFile($fileHandle); + } catch (\RuntimeException) { throw $this->createNotFoundException('CSV file not found'); } } #[Route('/download-xlsx-file', name: 'downloadxlsxfile', methods: ['GET'])] - public function downloadXlsxFileAction(Request $request, GridHelperService $gridHelperService): BinaryFileResponse - { - $storage = Storage::get('temp'); - $fileHandle = File::getValidFilename($request->query->get('fileHandle')); - $csvFile = $this->getCsvFile($fileHandle); - + public function downloadXlsxFileAction( + #[MapQueryParameter] ?string $fileHandle = null, + ): BinaryFileResponse { try { - return $gridHelperService->createXlsxExportFile($storage, $fileHandle, $csvFile); - } catch (Exception | FilesystemException | UnableToReadFile) { - // handle the error + return $this->gridExportService->downloadXlsxFile($fileHandle); + } catch (\RuntimeException) { throw $this->createNotFoundException('XLSX file not found'); } } - /** - * Flattens object data to an array with key=>value where - * value is simply a string representation of the value (for objects, hrefs and assets the full path is used) - */ - protected function csvObjectData(DataObject\Concrete $object): array - { - $o = []; - foreach ($object->getClass()->getFieldDefinitions() as $key => $value) { - $o[$key] = $value->getForCsvExport($object); - } - - $o['id (system)'] = $object->getId(); - $o['key (system)'] = $object->getKey(); - $o['fullpath (system)'] = $object->getRealFullPath(); - $o['published (system)'] = $object->isPublished(); - $o['type (system)'] = $object->getType(); - - return $o; - } - #[Route('/get-batch-jobs', name: 'getbatchjobs', methods: ['POST'])] - public function getBatchJobsAction(Request $request, GridHelperService $gridHelperService): JsonResponse - { - if ($request->request->get('language')) { - $request->setLocale($request->request->get('language')); + public function getBatchJobsAction( + GetBatchJobsPayload $payload, + GetBatchJobsHandler $handler, + Request $request, + ): JsonResponse { + if ($payload->locale !== $request->getLocale()) { + $request->setLocale($payload->locale); } - $allParams = [...$request->request->all(), ...$request->query->all()]; - $list = $gridHelperService->prepareListingForGrid($allParams, $request->getLocale(), $this->getAdminUser()); + $result = $handler($payload); - $jobs = $list->loadIdList(); - - return $this->adminJson(['success' => true, 'jobs' => $jobs]); + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs])); } #[Route('/batch', name: 'batch', methods: ['PUT'])] - public function batchAction(Request $request): JsonResponse - { - $success = true; - - try { - if ($request->request->has('data')) { - $params = $this->decodeJson($request->request->get('data'), true); - $object = DataObject\Concrete::getById($params['job']); - - if ($object) { - $requestedLanguage = $params['language']; - if ($requestedLanguage) { - if ($requestedLanguage !== 'default') { - $request->setLocale($requestedLanguage); - } - } else { - $requestedLanguage = $request->getLocale(); - } - - $name = $params['name']; - - if (!$object->isAllowed('save') || ($name === 'published' && !$object->isAllowed('publish'))) { - throw new Exception("Permission denied. You don't have the rights to save this object."); - } - - $append = $params['append'] ?? false; - $remove = $params['remove'] ?? false; - - $className = $object->getClassName(); - $class = DataObject\ClassDefinition::getByName($className); - $value = $params['value']; - if ($params['valueType'] === 'object') { - $value = $this->decodeJson($value); - } - - $parts = explode('~', $name); - - if (str_starts_with($name, '~')) { - $type = $parts[1]; - $field = $parts[2]; - $keyId = $parts[3]; - - if ($type === 'classificationstore') { - $groupKeyId = explode('-', $keyId); - $groupId = (int) $groupKeyId[0]; - $keyId = (int) $groupKeyId[1]; - - $getter = 'get' . ucfirst($field); - if (method_exists($object, $getter)) { - /** @var DataObject\ClassDefinition\Data\Classificationstore $csFieldDefinition */ - $csFieldDefinition = $object->getClass()->getFieldDefinition($field); - $csLanguage = $requestedLanguage; - if (!$csFieldDefinition->isLocalized()) { - $csLanguage = 'default'; - } - - /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ - $fd = $class->getFieldDefinition($field); - $keyConfig = $fd->getKeyConfiguration($keyId); - $dataDefinition = DataObject\Classificationstore\Service::getFieldDefinitionFromKeyConfig($keyConfig); - - /** @var DataObject\Classificationstore $classificationStoreData */ - $classificationStoreData = $object->$getter(); - if ($append) { - $oldValue = $classificationStoreData->getLocalizedKeyValue($groupId, $keyId); - $value = $dataDefinition->appendData($oldValue, $value); - } - if ($remove) { - $oldValue = $classificationStoreData->getLocalizedKeyValue($groupId, $keyId); - $value = $dataDefinition->removeData($oldValue, $value); - } - $classificationStoreData->setLocalizedKeyValue( - $groupId, - $keyId, - $dataDefinition->getDataFromEditmode($value), - $csLanguage - ); - $object->markFieldDirty($field); - } - } - } elseif (count($parts) > 1) { - // check for bricks - $brickType = $parts[0]; - - if (str_contains($brickType, '?')) { - $brickDescriptor = substr($brickType, 1); - $brickDescriptor = json_decode($brickDescriptor, true); - $brickType = $brickDescriptor['containerKey']; - } - $brickKey = $parts[1]; - $brickField = DataObject\Service::getFieldForBrickType($object->getClass(), $brickType); - - $fieldGetter = 'get' . ucfirst($brickField); - $brickGetter = 'get' . ucfirst($brickType); - $valueSetter = 'set' . ucfirst($brickKey); - - $brick = $object->$fieldGetter()->$brickGetter(); - if (empty($brick)) { - $classname = '\\OpenDxp\\Model\\DataObject\\Objectbrick\\Data\\' . ucfirst($brickType); - $brickSetter = 'set' . ucfirst($brickType); - $brick = new $classname($object); - $object->$fieldGetter()->$brickSetter($brick); - } - - $brickClass = DataObject\Objectbrick\Definition::getByKey($brickType); - $field = $brickClass->getFieldDefinition($brickKey); - - $newData = $field->getDataFromEditmode($value, $object); - - if ($append) { - $valueGetter = 'get' . ucfirst($brickKey); - $existingData = $brick->$valueGetter(); - $newData = $field->appendData($existingData, $newData); - } - if ($remove) { - $valueGetter = 'get' . ucfirst($brickKey); - $existingData = $brick->$valueGetter(); - $newData = $field->removeData($existingData, $newData); - } - - $localizedFields = $brickClass->getFieldDefinition('localizedfields'); - $isLocalizedField = false; - if ($localizedFields instanceof DataObject\ClassDefinition\Data\Localizedfields && $localizedFields->getFieldDefinition($brickKey)) { - $isLocalizedField = true; - } - - if ($isLocalizedField) { - $brick->$valueSetter($newData, $params['language']); - } else { - $brick->$valueSetter($newData); - } - } else { - // everything else - $field = $class->getFieldDefinition($name); - if ($field) { - $newData = $field->getDataFromEditmode($value, $object); - - if ($append) { - $existingData = $object->{'get' . $name}(); - $newData = $field->appendData($existingData, $newData); - } - if ($remove) { - $existingData = $object->{'get' . $name}(); - $newData = $field->removeData($existingData, $newData); - } - $object->setValue($name, $newData); - } else { - // check if it is a localized field - if ($params['language']) { - $localizedField = $class->getFieldDefinition('localizedfields'); - if ($localizedField instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $field = $localizedField->getFieldDefinition($name); - if ($field) { - $getter = 'get' . $name; - $setter = 'set' . $name; - $newData = $field->getDataFromEditmode($value, $object); - if ($append) { - $existingData = $object->$getter($params['language']); - $newData = $field->appendData($existingData, $newData); - } - if ($remove) { - $existingData = $object->$getter($params['language']); - $newData = $field->removeData($existingData, $newData); - } - - $object->$setter($newData, $params['language']); - } - } - } - - // seems to be a system field, this is actually only possible for the "published" field yet - if ($name === 'published') { - if ($value === 'false' || empty($value)) { - $object->setPublished(false); - } else { - $object->setPublished(true); - } - } - } - } - - try { - // don't check for mandatory fields here - $object->setOmitMandatoryCheck(!$object->isPublished()); - $object->setUserModification($this->getAdminUser()->getId()); - $object->save(); - $success = true; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - Logger::debug('DataObjectController::batchAction => There is no object left to update.'); - - return $this->adminJson(['success' => false, 'message' => 'DataObjectController::batchAction => There is no object left to update.']); - } - } - } catch (Exception $e) { - Logger::err((string) $e); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); + public function batchAction( + ExecuteBatchPayload $payload, + ExecuteBatchHandler $handler, + ): JsonResponse { + if (!$payload->hasData) { + return $this->adminJson(ApiResponse::ok()); } - return $this->adminJson(['success' => $success]); + $saved = $handler($payload); + + return $this->adminJson(ApiResponse::fromBool($saved)); } #[Route('/get-available-visible-vields', name: 'getavailablevisiblefields', methods: ['GET'])] - public function getAvailableVisibleFieldsAction(Request $request): JsonResponse - { - $classList = []; - $classNameList = []; - - if ($request->query->has('classes')) { - $classNameList = $request->query->get('classes'); - $classNameList = explode(',', $classNameList); - foreach ($classNameList as $className) { - $class = DataObject\ClassDefinition::getByName($className); - if ($class) { - $classList[] = $class; - } - } - } - - if (!$classList) { - return $this->adminJson(['availableFields' => []]); - } - - $availableFields = []; - foreach (self::SYSTEM_COLUMNS as $field) { - $availableFields[] = [ - 'key' => $field, - 'value' => $field, - ]; - } - - /** @var DataObject\ClassDefinition\Data[] $commonFields */ - $commonFields = []; - - $firstOne = true; - foreach ($classNameList as $className) { - $class = DataObject\ClassDefinition::getByName($className); - if ($class) { - $fds = $class->getFieldDefinitions(); - - $additionalFieldNames = array_keys($fds); - $localizedFields = $class->getFieldDefinition('localizedfields'); - if ($localizedFields instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $lfNames = array_keys($localizedFields->getFieldDefinitions()); - $additionalFieldNames = [...$additionalFieldNames, ...$lfNames]; - } - - foreach ($commonFields as $commonFieldKey => $commonFieldDefinition) { - if (!in_array($commonFieldKey, $additionalFieldNames)) { - unset($commonFields[$commonFieldKey]); - } - } - - $this->processAvailableFieldDefinitions($fds, $firstOne, $commonFields); - - $firstOne = false; - } - } - - $commonFieldKeys = array_keys($commonFields); - foreach ($commonFieldKeys as $field) { - $availableFields[] = [ - 'key' => $field, - 'value' => $field, - ]; - } - - return $this->adminJson(['availableFields' => $availableFields]); - } + public function getAvailableVisibleFieldsAction( + GetAvailableVisibleFieldsPayload $payload, + GetAvailableVisibleFieldsHandler $getAvailableFields, + ): JsonResponse { + $result = $getAvailableFields($payload); - /** - * @param DataObject\ClassDefinition\Data[] $fds - * @param DataObject\ClassDefinition\Data[] $commonFields - */ - protected function processAvailableFieldDefinitions(array $fds, bool &$firstOne, array &$commonFields): void - { - foreach ($fds as $fd) { - if ($fd instanceof DataObject\ClassDefinition\Data\Fieldcollections) { - continue; - } - if ($fd instanceof DataObject\ClassDefinition\Data\Objectbricks) { - continue; - } - if ($fd instanceof DataObject\ClassDefinition\Data\Block) { - continue; - } - if ($fd instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $lfDefs = $fd->getFieldDefinitions(); - $this->processAvailableFieldDefinitions($lfDefs, $firstOne, $commonFields); - } elseif ($firstOne || (isset($commonFields[$fd->getName()]) && $commonFields[$fd->getName()]->getFieldtype() == $fd->getFieldtype())) { - $commonFields[$fd->getName()] = $fd; - } - } + return $this->adminJson(['availableFields' => $result->availableFields]); } } diff --git a/src/Controller/Admin/DataObject/DataObjectVersionController.php b/src/Controller/Admin/DataObject/DataObjectVersionController.php new file mode 100644 index 00000000..01267faa --- /dev/null +++ b/src/Controller/Admin/DataObject/DataObjectVersionController.php @@ -0,0 +1,98 @@ +value)] +class DataObjectVersionController extends AdminAbstractController +{ + #[Route('/publish-version', name: 'publishversion', methods: ['POST'])] + public function publishVersionAction(IdBodyPayload $payload, PublishVersionHandler $publishVersion): JsonResponse + { + $result = $publishVersion($payload); + + return $this->adminJson(ApiResponse::ok([ + 'general' => ['modificationDate' => $result->modificationDate], + 'treeData' => $result->treeData, + ])); + } + + #[Route('/preview-version', name: 'previewversion', methods: ['GET'])] + public function previewVersionAction( + Environment $twig, + PreviewVersionHandler $previewVersion, + PreviewVersionPayload $payload, + ): Response + { + $result = $previewVersion($payload); + + Tool\UserTimezone::setUserTimezone($payload->userTimezone); + if ($timezone = Tool\UserTimezone::getUserTimezone()) { + $twig->getExtension(CoreExtension::class)->setTimezone($timezone); + } + + return $this->render('@OpenDxpAdmin/admin/data_object/data_object/preview_version.html.twig', [ + 'object' => $result->object, + 'versionNote' => $result->version->getNote(), + 'validLanguages' => Tool::getValidLanguages(), + ]); + } + + #[Route('/diff-versions/from/{from}/to/{to}', name: 'diffversions', methods: ['GET'])] + public function diffVersionsAction( + Environment $twig, + DiffVersionsHandler $diffVersions, + DiffVersionsPayload $payload, + ): Response + { + $result = $diffVersions($payload); + + Tool\UserTimezone::setUserTimezone($payload->userTimezone); + if ($timezone = Tool\UserTimezone::getUserTimezone()) { + $twig->getExtension(CoreExtension::class)->setTimezone($timezone); + } + + return $this->render('@OpenDxpAdmin/admin/data_object/data_object/diff_versions.html.twig', [ + 'object1' => $result->object1, + 'versionNote1' => $result->version1->getNote(), + 'object2' => $result->object2, + 'versionNote2' => $result->version2->getNote(), + 'validLanguages' => Tool::getValidLanguages(), + ]); + } +} diff --git a/src/Controller/Admin/DataObject/FieldCollectionController.php b/src/Controller/Admin/DataObject/FieldCollectionController.php new file mode 100644 index 00000000..c449b9a4 --- /dev/null +++ b/src/Controller/Admin/DataObject/FieldCollectionController.php @@ -0,0 +1,125 @@ +value)] +class FieldCollectionController extends AdminAbstractController +{ + #[Route('/fieldcollection-get', name: 'fieldcollectionget', methods: ['GET'])] + public function fieldcollectionGetAction(GetFieldCollectionHandler $getFieldCollection, GetFieldCollectionPayload $payload): JsonResponse + { + $result = $getFieldCollection($payload); + $data = $result->data; + $data['isWriteable'] = $result->isWriteable; + + return $this->adminJson($data); + } + + #[Route('/fieldcollection-update', name: 'fieldcollectionupdate', methods: ['PUT', 'POST'])] + public function fieldcollectionUpdateAction(UpdateFieldCollectionHandler $updateFieldCollection, UpdateFieldCollectionPayload $payload): JsonResponse + { + $fcDef = $updateFieldCollection($payload); + + return $this->adminJson(ApiResponse::ok(['id' => $fcDef->getKey()])); + } + + #[Route('/fieldcollection-delete', name: 'fieldcollectiondelete', methods: ['DELETE'])] + public function fieldcollectionDeleteAction(DeleteFieldCollectionHandler $deleteFieldCollection, StringIdBodyPayload $payload): JsonResponse + { + $deleteFieldCollection($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/fieldcollection-tree', name: 'fieldcollectiontree', methods: ['GET', 'POST'])] + public function fieldcollectionTreeAction(GetFieldCollectionTreeHandler $getTree, GetFieldCollectionTreePayload $payload): JsonResponse + { + $result = $getTree($payload); + + if ($payload->forObjectEditor) { + return $this->adminJson(['fieldcollections' => $result->definitions, 'layoutDefinitions' => $result->layoutDefinitions]); + } + + return $this->adminJson($result->definitions); + } + + #[Route('/fieldcollection-list', name: 'fieldcollectionlist', methods: ['GET'])] + public function fieldcollectionListAction(GetFieldCollectionListHandler $getList, GetFieldCollectionListPayload $payload): JsonResponse + { + $result = $getList($payload); + + return $this->adminJson(['fieldcollections' => $result->fieldcollections]); + } + + #[Route('/import-fieldcollection', name: 'importfieldcollection', methods: ['POST'])] + public function importFieldcollectionAction(ImportFieldCollectionHandler $importFieldCollection, ImportFieldCollectionPayload $payload): Response + { + $importFieldCollection($payload); + + $response = $this->adminJson(ApiResponse::ok()); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/export-fieldcollection', name: 'exportfieldcollection', methods: ['GET'])] + public function exportFieldcollectionAction(ExportFieldCollectionHandler $exportFieldCollection, ExportFieldCollectionPayload $payload): Response + { + $result = $exportFieldCollection($payload); + + $response = new Response($result->json); + $response->headers->set('Content-type', 'application/json'); + $response->headers->set('Content-Disposition', 'attachment; filename="fieldcollection_' . $result->key . '_export.json"'); + + return $response; + } + + #[Route('/get-fieldcollection-usages', name: 'getfieldcollectionusages', methods: ['GET'])] + public function getFieldcollectionUsagesAction(GetFieldCollectionUsagesHandler $getUsages, GetFieldCollectionUsagesPayload $payload): Response + { + return $this->adminJson($getUsages($payload)); + } +} diff --git a/src/Controller/Admin/DataObject/ObjectBrickController.php b/src/Controller/Admin/DataObject/ObjectBrickController.php new file mode 100644 index 00000000..cfaa9ce6 --- /dev/null +++ b/src/Controller/Admin/DataObject/ObjectBrickController.php @@ -0,0 +1,124 @@ +value)] +class ObjectBrickController extends AdminAbstractController +{ + #[Route('/objectbrick-get', name: 'objectbrickget', methods: ['GET'])] + public function objectbrickGetAction(GetObjectBrickHandler $getObjectBrick, GetObjectBrickPayload $payload): JsonResponse + { + $result = $getObjectBrick($payload); + $data = $result->data; + $data['isWriteable'] = $result->isWriteable; + + return $this->adminJson($data); + } + + #[Route('/objectbrick-update', name: 'objectbrickupdate', methods: ['PUT', 'POST'])] + public function objectbrickUpdateAction(UpdateObjectBrickHandler $updateObjectBrick, UpdateObjectBrickPayload $payload): JsonResponse + { + $brickDef = $updateObjectBrick($payload); + + return $this->adminJson(ApiResponse::ok(['id' => $brickDef->getKey()])); + } + + #[Route('/objectbrick-delete', name: 'objectbrickdelete', methods: ['DELETE'])] + public function objectbrickDeleteAction(DeleteObjectBrickHandler $deleteObjectBrick, StringIdBodyPayload $payload): JsonResponse + { + $deleteObjectBrick($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/objectbrick-tree', name: 'objectbricktree', methods: ['GET', 'POST'])] + public function objectbrickTreeAction(GetObjectBrickTreeHandler $getTree, GetObjectBrickTreePayload $payload): JsonResponse + { + $result = $getTree($payload); + + if ($payload->forObjectEditor) { + return $this->adminJson(['objectbricks' => $result->definitions, 'layoutDefinitions' => $result->layoutDefinitions]); + } + + return $this->adminJson($result->definitions); + } + + #[Route('/objectbrick-list', name: 'objectbricklist', methods: ['GET'])] + public function objectbrickListAction(GetObjectBrickListHandler $getList, GetObjectBrickListPayload $payload): JsonResponse + { + $result = $getList($payload); + + return $this->adminJson(['objectbricks' => $result->objectbricks]); + } + + #[Route('/import-objectbrick', name: 'importobjectbrick', methods: ['POST'])] + public function importObjectbrickAction(ImportObjectBrickHandler $importObjectBrick, ImportObjectBrickPayload $payload): JsonResponse + { + $importObjectBrick($payload); + $response = $this->adminJson(ApiResponse::ok()); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/export-objectbrick', name: 'exportobjectbrick', methods: ['GET'])] + public function exportObjectbrickAction(ExportObjectBrickHandler $exportObjectBrick, ExportObjectBrickPayload $payload): Response + { + $result = $exportObjectBrick($payload); + + $response = new Response($result->json); + $response->headers->set('Content-type', 'application/json'); + $response->headers->set('Content-Disposition', 'attachment; filename="objectbrick_' . $result->key . '_export.json"'); + + return $response; + } + + #[Route('/get-bricks-usages', name: 'getbrickusages', methods: ['GET'])] + public function getBrickUsagesAction(GetBrickUsagesHandler $getBrickUsages, GetBrickUsagesPayload $payload): Response + { + return $this->adminJson($getBrickUsages($payload)->usages); + } +} diff --git a/src/Controller/Admin/DataObject/QuantityValueController.php b/src/Controller/Admin/DataObject/QuantityValueController.php index c1bab143..fe790f33 100644 --- a/src/Controller/Admin/DataObject/QuantityValueController.php +++ b/src/Controller/Admin/DataObject/QuantityValueController.php @@ -16,18 +16,31 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Model\DataObject\Data\QuantityValue; -use OpenDxp\Model\DataObject\QuantityValue\Service as QuantityValueService; -use OpenDxp\Model\DataObject\QuantityValue\Unit; -use OpenDxp\Model\DataObject\QuantityValue\UnitConversionService; -use OpenDxp\Model\Translation; -use Symfony\Component\HttpFoundation\File\UploadedFile; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ConvertAllQuantityValues\ConvertAllQuantityValuesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ConvertAllQuantityValues\ConvertAllQuantityValuesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ConvertQuantityValue\ConvertQuantityValueHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ConvertQuantityValue\ConvertQuantityValuePayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\CreateQuantityValueUnit\CreateQuantityValueUnitHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\DeleteQuantityValueUnit\DeleteQuantityValueUnitHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ExportQuantityValueUnits\ExportQuantityValueUnitsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\GetQuantityValueUnitList\GetQuantityValueUnitListHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\GetQuantityValueUnitList\GetQuantityValueUnitListPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\GetQuantityValueUnits\GetQuantityValueUnitsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\GetQuantityValueUnits\GetQuantityValueUnitsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ImportQuantityValueUnits\ImportQuantityValueUnitsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\ImportQuantityValueUnits\ImportQuantityValueUnitsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\QuantityValueUnitPayload; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\QuantityValue\UpdateQuantityValueUnit\UpdateQuantityValueUnitHandler; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal @@ -35,250 +48,103 @@ #[Route('/quantity-value', name: 'opendxp_admin_dataobject_quantityvalue_')] class QuantityValueController extends AdminAbstractController { - public function __construct(protected QuantityValueService $service) - { - } - #[Route('/unit-import', name: 'unitimport', methods: ['POST', 'PUT'])] - public function unitImportAction(Request $request): JsonResponse + public function unitImportAction(ImportQuantityValueUnitsPayload $payload, ImportQuantityValueUnitsHandler $importUnits): JsonResponse { - /** @var UploadedFile $uploadFile */ - $uploadFile = $request->files->get('Filedata'); - - $json = file_get_contents($uploadFile->getPathname()); - $success = $this->service->importDefinitionFromJson($json); - $response = $this->adminJson(['success' => $success]); + $success = $importUnits($payload); + $response = $this->adminJson(ApiResponse::fromBool($success)); $response->headers->set('Content-Type', 'text/html'); return $response; } #[Route('/unit-export', name: 'unitexport', methods: ['GET'])] - public function unitExportAction(Request $request): Response + public function unitExportAction(ExportQuantityValueUnitsHandler $exportUnits): Response { - $result = $this->service->generateDefinitionJson(); - $response = new Response($result); + $response = new Response($exportUnits()); $response->headers->set('Content-Type', 'application/json'); $response->headers->set('Content-Disposition', 'attachment;filename: "quantityvalue_unit_export.json"'); return $response; } - /** - * @throws Exception - */ #[Route('/unit-proxy', name: 'unitproxyget', methods: ['GET'])] - public function unitProxyGetAction(Request $request): JsonResponse + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyGetAction(GetQuantityValueUnitsHandler $getUnits, GetQuantityValueUnitsPayload $payload): JsonResponse { - $this->checkPermission('quantityValueUnits'); - - $list = new Unit\Listing(); - - $order = ['ASC', 'ASC', 'ASC']; - $orderKey = ['baseunit', 'factor', 'abbreviation']; - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->query->all()); - - // Prepend user-requested sorting settings but keep the others to keep secondary order of quantity values in respective order - if ($sortingSettings['orderKey']) { - array_unshift($orderKey, $sortingSettings['orderKey']); - } - if ($sortingSettings['order']) { - array_unshift($order, $sortingSettings['order']); - } + $result = $getUnits($payload); - $list->setOrder($order); - $list->setOrderKey($orderKey); - - $list->setLimit((int)$request->query->get('limit', '25')); - $list->setOffset((int)$request->query->get('start', '0')); - - $condition = '1 = 1'; - if ($request->query->get('filter')) { - $filterString = $request->query->get('filter'); - $filters = json_decode($filterString); - $db = \OpenDxp\Db::get(); - foreach ($filters as $f) { - if ($f->type === 'string') { - $condition .= ' AND ' . $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); - } elseif ($f->type === 'numeric') { - $operator = $this->getOperator($f->comparison); - $condition .= ' AND ' . $db->quoteIdentifier($f->property) . ' ' . $operator . ' ' . $db->quote($f->value); - } - } - $list->setCondition($condition); - } - - $units = []; - foreach ($list->getUnits() as $u) { - $units[] = $u->getObjectVars(); - } - - return $this->adminJson(['data' => $units, 'success' => true, 'total' => $list->getTotalCount()]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } - /** - * @throws Exception - */ #[Route('/unit-proxy', name: 'unitproxy', methods: ['POST', 'PUT'])] - public function unitProxyAction(Request $request): JsonResponse - { - $this->checkPermission('quantityValueUnits'); - - if ($request->request->has('data')) { - if ($request->query->get('xaction') === 'destroy') { - $data = json_decode($request->request->get('data'), true); - $id = $data['id']; - $unit = \OpenDxp\Model\DataObject\QuantityValue\Unit::getById($id); - if (!empty($unit)) { - $unit->delete(); - - return $this->adminJson(['data' => [], 'success' => true]); - } - - throw new Exception('Unit with id ' . $id . ' not found.'); - } - if ($request->query->get('xaction') === 'update') { - $data = json_decode($request->request->get('data'), true); - $unit = Unit::getById($data['id']); - if (!empty($unit)) { - if (($data['baseunit'] ?? null) == -1) { - $data['baseunit'] = null; - } - $unit->setValues($data); - $unit->save(); - - return $this->adminJson(['data' => $unit->getObjectVars(), 'success' => true]); - } - - throw new Exception('Unit with id ' . $data['id'] . ' not found.'); - } - if ($request->query->get('xaction') === 'create') { - $data = json_decode($request->request->get('data'), true); - if (isset($data['baseunit']) && $data['baseunit'] === -1) { - $data['baseunit'] = null; - } - $id = $data['id']; - if (Unit::getById($id)) { - throw new Exception('unit with ID [' . $id . '] already exists'); - } - if (mb_strlen($id) > 50) { - throw new Exception('The maximal character length for the unit ID is 50 characters, the provided ID has ' . mb_strlen($id) . ' characters.'); - } - $unit = new Unit(); - $unit->setValues($data); - $unit->save(); - - return $this->adminJson(['data' => $unit->getObjectVars(), 'success' => true]); - } - } + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyAction( + Request $request, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::unitProxyDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::unitProxyUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::unitProxyCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } - return $this->adminJson(['success' => false]); + #[Route('/unit-proxy-destroy', name: 'unitproxy_destroy', methods: ['POST', 'PUT'])] + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyDestroyAction( + QuantityValueUnitPayload $payload, + DeleteQuantityValueUnitHandler $deleteUnit, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $deleteUnit($payload)->data])); } - private function getOperator(string $comparison): string - { - $mapper = [ - 'lt' => '<', - 'gt' => '>', - 'eq' => '=', - ]; + #[Route('/unit-proxy-update', name: 'unitproxy_update', methods: ['POST', 'PUT'])] + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyUpdateAction( + QuantityValueUnitPayload $payload, + UpdateQuantityValueUnitHandler $updateUnit, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $updateUnit($payload)->data])); + } - return $mapper[$comparison]; + #[Route('/unit-proxy-create', name: 'unitproxy_create', methods: ['POST', 'PUT'])] + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyCreateAction( + QuantityValueUnitPayload $payload, + CreateQuantityValueUnitHandler $createUnit, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $createUnit($payload)->data])); } #[Route('/unit-list', name: 'unitlist', methods: ['GET'])] - public function unitListAction(Request $request): JsonResponse + public function unitListAction(GetQuantityValueUnitListHandler $getUnitList, GetQuantityValueUnitListPayload $payload): JsonResponse { - $list = new Unit\Listing(); - $list->setOrderKey(['baseunit', 'factor', 'abbreviation']); - $list->setOrder(['ASC', 'ASC', 'ASC']); - if ($request->query->get('filter')) { - $array = explode(',', $request->query->get('filter')); - $quotedArray = []; - $db = \OpenDxp\Db::get(); - foreach ($array as $a) { - $quotedArray[] = $db->quote($a); - } - $string = implode(',', $quotedArray); - $list->setCondition('id IN (' . $string . ')'); - } + $result = $getUnitList($payload); - $result = []; - $units = $list->getUnits(); - foreach ($units as &$unit) { - try { - if ($unit->getAbbreviation()) { - $unit->setAbbreviation(Translation::getByKeyLocalized($unit->getAbbreviation(), Translation::DOMAIN_ADMIN, - true, true)); - } - if ($unit->getLongname()) { - $unit->setLongname(Translation::getByKeyLocalized($unit->getLongname(), Translation::DOMAIN_ADMIN, true, - true)); - } - $result[] = $unit->getObjectVars(); - } catch (Exception) { - // nothing to do ... - } - } - - return $this->adminJson(['data' => $result, 'success' => true, 'total' => $list->getTotalCount()]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } #[Route('/convert', name: 'convert', methods: ['GET'])] - public function convertAction(Request $request, UnitConversionService $conversionService): JsonResponse + #[IsGranted(CorePermission::Objects->value)] + public function convertAction(ConvertQuantityValueHandler $convert, ConvertQuantityValuePayload $payload): JsonResponse { - $this->checkPermission('objects'); - - $fromUnitId = $request->query->get('fromUnit'); - $toUnitId = $request->query->get('toUnit'); + $result = $convert($payload); - $fromUnit = Unit::getById($fromUnitId); - $toUnit = Unit::getById($toUnitId); - if (!$fromUnit instanceof Unit || !$toUnit instanceof Unit) { - return $this->adminJson(['success' => false]); - } - - try { - $convertedValue = $conversionService->convert(new QuantityValue($request->query->get('value'), $fromUnit), $toUnit); - } catch (Exception) { - return $this->adminJson(['success' => false]); - } - - return $this->adminJson(['value' => $convertedValue->getValue(), 'success' => true]); + return $this->adminJson(ApiResponse::ok(['value' => $result->value])); } #[Route('/convert-all', name: 'convertall', methods: ['GET'])] - public function convertAllAction(Request $request, UnitConversionService $conversionService): JsonResponse + #[IsGranted(CorePermission::Objects->value)] + public function convertAllAction(ConvertAllQuantityValuesHandler $convertAll, ConvertAllQuantityValuesPayload $payload): JsonResponse { - $this->checkPermission('objects'); - - $unitId = $request->query->get('unit'); - - $fromUnit = Unit::getById($unitId); - if (!$fromUnit instanceof Unit) { - return $this->adminJson(['success' => false]); - } - $baseUnit = $fromUnit->getBaseunit() ?? $fromUnit; - - $units = new Unit\Listing(); - $units->setCondition('baseunit = '.$units->quote($baseUnit->getId()).' AND id != '.$units->quote($fromUnit->getId())); - - $convertedValues = []; - foreach ($units->getUnits() as $targetUnit) { - try { - $convertedValue = $conversionService->convert(new QuantityValue($request->query->get('value'), $fromUnit), $targetUnit); - - $convertedValues[] = ['unit' => $targetUnit->getAbbreviation(), 'unitName' => $targetUnit->getLongname(), 'value' => round($convertedValue->getValue(), 4)]; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } + $result = $convertAll($payload); - return $this->adminJson([ - 'value' => $request->query->get('value'), - 'fromUnit' => $fromUnit->getAbbreviation(), - 'values' => $convertedValues, 'success' => true] - ); + return $this->adminJson(ApiResponse::ok([ + 'value' => $result->value, + 'fromUnit' => $result->fromUnit, + 'values' => $result->values, + ])); } } diff --git a/src/Controller/Admin/DataObject/VariantsController.php b/src/Controller/Admin/DataObject/VariantsController.php index 3942b2d3..a4e953c1 100644 --- a/src/Controller/Admin/DataObject/VariantsController.php +++ b/src/Controller/Admin/DataObject/VariantsController.php @@ -1,5 +1,4 @@ request->getInt('id'); - $key = $request->request->get('key'); - $object = DataObject\Concrete::getById($id); + $result = $updateObjectKey($payload); - return $this->adminJson($this->renameObject($object, $key)); + return $this->adminJson($result->data); } - /** - * @throws Exception - */ #[Route('/get-variants', name: 'getvariants', methods: ['POST'])] public function getVariantsAction( + GetVariantsHandler $getVariants, + GetVariantsPayload $payload, Request $request, - EventDispatcherInterface $eventDispatcher, - GridHelperService $gridHelperService, - LocaleServiceInterface $localeService, - CsrfProtectionHandler $csrfProtection + CsrfProtectionHandler $csrfProtection, ): JsonResponse { + $csrfProtection->checkCsrfToken($request); - $parentObject = DataObject\Concrete::getById((int) $request->request->get('objectId')); - - if ($parentObject === null) { - throw new Exception('No Object found with id ' . $request->request->get('objectId')); - } - - if (!$parentObject->isAllowed('view')) { - throw new Exception('Permission denied'); + if ($payload->requestedLanguage !== $request->getLocale()) { + $request->setLocale($payload->requestedLanguage); } - $allParams = [...$request->request->all(), ...$request->query->all()]; - - $allParams['folderId'] = $parentObject->getId(); - $allParams['classId'] = $parentObject->getClassId(); - - $csrfProtection->checkCsrfToken($request); - - $result = $this->gridProxy( - $allParams, - DataObject::OBJECT_TYPE_VARIANT, - $request, - $eventDispatcher, - $gridHelperService, - $localeService - ); + $result = $getVariants($payload); - return $this->adminJson($result); + return $this->adminJson($result->data); } } diff --git a/src/Controller/Admin/Document/DocumentController.php b/src/Controller/Admin/Document/DocumentController.php index f5c53d6a..7c684cf4 100644 --- a/src/Controller/Admin/Document/DocumentController.php +++ b/src/Controller/Admin/Document/DocumentController.php @@ -15,1355 +15,393 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\Document; -use Exception; -use Imagick; -use OpenDxp; use OpenDxp\Bundle\AdminBundle\Controller\Admin\ElementControllerBase; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\AdminStyleTrait; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\UserNameTrait; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Event\ElementAdminStyleEvent; -use OpenDxp\Bundle\AdminBundle\Event\SiteCustomSettingsEvent; -use OpenDxp\Cache\RuntimeCache; -use OpenDxp\Config; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Db; -use OpenDxp\Document\Renderer\DocumentRendererInterface; -use OpenDxp\Event\Traits\RecursionBlockingEventDispatchHelperTrait; -use OpenDxp\Image\HtmlToImage; -use OpenDxp\Logger; -use OpenDxp\Model\Document; -use OpenDxp\Model\Document\DocType; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Exception\ElementLockedException; +use OpenDxp\Controller\Traits\ElementEditLockHelperTrait; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; +use OpenDxp\Bundle\AdminBundle\Handler\Document\AddDocument\AddDocumentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\AddDocument\AddDocumentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\ConvertDocument\ConvertDocumentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\ConvertDocument\ConvertDocumentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DeleteDocument\DeleteDocumentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DeleteDocument\DeleteDocumentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DocTypes\CreateDocType\CreateDocTypeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DocTypes\DeleteDocType\DeleteDocTypeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DocTypes\DocTypePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DocTypes\UpdateDocType\UpdateDocTypeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocTypesByType\GetDocTypesByTypeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocTypesByType\GetDocTypesByTypePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\DocTypes\GetDocTypesList\GetDocTypesListHandler; +use OpenDxp\Bundle\AdminBundle\Payload\Common\EmptyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocumentData\GetDocumentDataHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocumentData\GetDocumentDataPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocumentIdForPath\GetDocumentIdForPathHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocumentIdForPath\GetDocumentIdForPathPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\GetSiteCustomSettings\GetSiteCustomSettingsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\GetSiteCustomSettings\GetSiteCustomSettingsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\RemoveSite\RemoveSiteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\RemoveSite\RemoveSitePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\UpdateSite\UpdateSiteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Site\UpdateSite\UpdateSitePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\AddDocumentTranslation\AddDocumentTranslationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\AddDocumentTranslation\AddDocumentTranslationPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\CheckTranslationLanguage\CheckTranslationLanguageHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\CheckTranslationLanguage\CheckTranslationLanguagePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\DetermineTranslationParent\DetermineTranslationParentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\DetermineTranslationParent\DetermineTranslationParentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\GetLanguageTree\GetLanguageTreeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\GetLanguageTree\GetLanguageTreePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\GetLanguageTreeRoot\GetLanguageTreeRootHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\GetLanguageTreeRoot\GetLanguageTreeRootPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\RemoveDocumentTranslation\RemoveDocumentTranslationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Translation\RemoveDocumentTranslation\RemoveDocumentTranslationPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\TreeGetDocumentChildren\TreeGetDocumentChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\TreeGetDocumentChildren\TreeGetDocumentChildrenPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\UpdateDocument\UpdateDocumentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\UpdateDocument\UpdateDocumentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoPayload; +use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; use OpenDxp\Model\Element\ElementInterface; -use OpenDxp\Model\Element\Service; -use OpenDxp\Model\Exception\ConfigWriteException; -use OpenDxp\Model\Site; -use OpenDxp\Model\Version; -use OpenDxp\Tool; -use OpenDxp\Tool\Session; use Override; -use RuntimeException; -use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; -use Symfony\Component\HttpKernel\Event\ControllerEvent; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use function base64_encode; -use function basename; -use function date; -use function file_exists; -use function file_get_contents; -use function file_put_contents; -use function sprintf; -use function uniqid; -use function unlink; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/document')] -class DocumentController extends ElementControllerBase implements KernelControllerEventInterface +class DocumentController extends ElementControllerBase { - use AdminStyleTrait; - use UserNameTrait; - use RecursionBlockingEventDispatchHelperTrait; + use ElementEditLockHelperTrait; - protected Document\Service $_documentService; + public function __construct(ElementServiceInterface $elementService) + { + parent::__construct($elementService); + } + #[IsGranted(CorePermission::Documents->value)] #[Override] #[Route('/tree-get-root', name: 'opendxp_admin_document_document_treegetroot', methods: ['GET'])] - public function treeGetRootAction(Request $request): JsonResponse - { - return parent::treeGetRootAction($request); + public function treeGetRootAction( + #[MapQueryParameter] ?string $elementType = null, + #[MapQueryParameter(flags: FILTER_NULL_ON_FAILURE)] ?int $id = null, + ): JsonResponse { + return parent::treeGetRootAction($elementType, $id); } + #[IsGranted(CorePermission::Documents->value)] #[Override] #[Route('/delete-info', name: 'opendxp_admin_document_document_deleteinfo', methods: ['GET'])] - public function deleteInfoAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - return parent::deleteInfoAction($request, $eventDispatcher); + public function deleteInfoAction( + GetDeleteInfoHandler $handler, + GetDeleteInfoPayload $payload, + ): JsonResponse { + return parent::deleteInfoAction($handler, $payload); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/get-data-by-id', name: 'opendxp_admin_document_document_getdatabyid', methods: ['GET'])] - public function getDataByIdAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function getDataByIdAction( + GetDocumentDataHandler $handler, + GetDocumentDataPayload $payload, + ): JsonResponse { - $document = Document::getById((int) $request->query->get('id')); - - if (!$document) { - throw $this->createNotFoundException('Document not found'); - } - - $document = clone $document; - $data = $document->getObjectVars(); - $data['versionDate'] = $document->getModificationDate(); - - $userOwnerName = $this->getUserName($document->getUserOwner()); - $userModificationName = ($document->getUserOwner() === $document->getUserModification()) ? $userOwnerName : $this->getUserName($document->getUserModification()); - $data['userOwnerUsername'] = $userOwnerName['userName']; - $data['userOwnerFullname'] = $userOwnerName['fullName']; - $data['userModificationUsername'] = $userModificationName['userName']; - $data['userModificationFullname'] = $userModificationName['fullName']; - - $data['php'] = [ - 'classes' => [$document::class, ...array_values(class_parents($document))], - 'interfaces' => array_values(class_implements($document)), - ]; - - $this->addAdminStyle($document, ElementAdminStyleEvent::CONTEXT_EDITOR, $data); - - $event = new GenericEvent($this, [ - 'data' => $data, - 'document' => $document, - ]); - $eventDispatcher->dispatch($event, AdminEvents::DOCUMENT_GET_PRE_SEND_DATA); - $data = $event->getArgument('data'); - - if ($document->isAllowed('view')) { - return $this->adminJson($data); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - throw $this->createAccessDeniedHttpException(); + return $this->adminJson($result->data); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/tree-get-children-by-id', name: 'opendxp_admin_document_document_treegetchildrenbyid', methods: ['GET'])] - public function treeGetChildrenByIdAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function treeGetChildrenByIdAction( + TreeGetDocumentChildrenHandler $handler, + TreeGetDocumentChildrenPayload $payload, + ): JsonResponse { - $allParams = $request->query->all(); - - $filter = $request->query->get('filter'); - $limit = (int)($allParams['limit'] ?? 100000000); - $offset = (int)($allParams['start'] ?? 0); - - if (!is_null($filter)) { - if (!str_ends_with($filter, '*')) { - $filter .= '*'; - } - $filter = str_replace('*', '%', $filter); - $limit = 100; - $offset = 0; - } - - $document = Document::getById((int) $allParams['node']); - if (!$document) { - throw $this->createNotFoundException('Document was not found'); - } - - $documents = []; - $cv = []; - if ($document->hasChildren()) { - if ($allParams['view']) { - $cv = $this->elementService->getCustomViewById($allParams['view']); - } - - $db = Db::get(); - - $list = new Document\Listing(); - - $condition = 'parentId = ' . $db->quote($document->getId()); - - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $currentUserId = $this->getAdminUser()->getId(); - $userIds[] = $currentUserId; - - $inheritedPermission = $document->getDao()->isInheritingPermission('list', $userIds); - - $anyAllowedRowOrChildren = 'EXISTS(SELECT list FROM users_workspaces_document uwd WHERE userId IN (' . implode(',', $userIds) . ') AND list=1 AND LOCATE(CONCAT(`path`,`key`),cpath)=1 AND - NOT EXISTS(SELECT list FROM users_workspaces_document WHERE userId =' . $currentUserId . ' AND list=0 AND cpath = uwd.cpath))'; - $isDisallowedCurrentRow = 'EXISTS(SELECT list FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') AND cid = id AND list=0)'; - - $condition .= ' AND IF(' . $anyAllowedRowOrChildren . ',1,IF(' . $inheritedPermission . ', ' . $isDisallowedCurrentRow . ' = 0, 0)) = 1'; - } - - if ($filter) { - $condition = '(' . $condition . ')' . ' AND CAST(documents.key AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ' . $db->quote($filter); - } - - $list->setCondition($condition); - - $list->setOrderKey(['index', 'id']); - $list->setOrder(['asc', 'asc']); - - $list->setLimit($limit); - $list->setOffset($offset); - - Service::addTreeFilterJoins($cv, $list); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $list, - 'context' => $allParams, - ]); - - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::DOCUMENT_LIST_BEFORE_LIST_LOAD); - /** @var Document\Listing $list */ - $list = $beforeListLoadEvent->getArgument('list'); - - $childrenList = $list->load(); - - foreach ($childrenList as $childDocument) { - $documentTreeNode = $this->elementService->getElementTreeNodeConfig($childDocument); - // the !isset is for printContainer case, there are no permissions sets there - if (!isset($documentTreeNode['permissions']['list']) || $documentTreeNode['permissions']['list'] == 1) { - $documents[] = $documentTreeNode; - } - } - } + $result = $handler($payload); - //Hook for modifying return value - e.g. for changing permissions based on document data - $event = new GenericEvent($this, [ - 'documents' => $documents, - ]); - - $eventDispatcher->dispatch($event, AdminEvents::DOCUMENT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); - $documents = $event->getArgument('documents'); - - if ($allParams['limit']) { + if ($result->paginated) { return $this->adminJson([ - 'offset' => $offset, - 'limit' => $limit, - 'total' => $document->getChildAmount($this->getAdminUser()), - 'nodes' => $documents, - 'filter' => $request->query->get('filter') ?: '', - 'inSearch' => (int)$request->query->get('inSearch'), + 'offset' => $result->offset, + 'limit' => $result->limit, + 'total' => $result->total, + 'nodes' => $result->documents, + 'filter' => $result->filter ?: '', + 'inSearch' => $payload->inSearch, ]); } - return $this->adminJson($documents); + return $this->adminJson($result->documents); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/add', name: 'opendxp_admin_document_document_add', methods: ['POST'])] - public function addAction(Request $request): JsonResponse + public function addAction( + AddDocumentPayload $payload, + AddDocumentHandler $handler, + ): JsonResponse { - $success = false; - $errorMessage = ''; - - // check for permission - $parentDocument = Document::getById($request->request->getInt('parentId')); - $document = null; - if ($parentDocument->isAllowed('create')) { - $intendedPath = $parentDocument->getRealFullPath() . '/' . $request->request->get('key'); - - if (!Document\Service::pathExists($intendedPath)) { - $createValues = [ - 'userOwner' => $this->getAdminUser()->getId(), - 'userModification' => $this->getAdminUser()->getId(), - 'published' => false, - ]; - - $createValues['key'] = Service::getValidKey($request->request->get('key'), 'document'); - - // check for a docType - $docType = Document\DocType::getById($request->request->get('docTypeId', '')); - - if ($docType) { - $createValues['template'] = $docType->getTemplate(); - $createValues['controller'] = $docType->getController(); - $createValues['staticGeneratorEnabled'] = $docType->getStaticGeneratorEnabled(); - } elseif ($translationsBaseDocumentId = $request->request->get('translationsBaseDocument')) { - $translationsBaseDocument = Document::getById((int) $translationsBaseDocumentId); - if ($translationsBaseDocument instanceof Document\PageSnippet) { - $createValues['template'] = $translationsBaseDocument->getTemplate(); - $createValues['controller'] = $translationsBaseDocument->getController(); - } - } elseif (in_array($request->request->get('type'), ['page', 'snippet', 'email'])) { - $createValues['controller'] = $this->getParameter('opendxp.documents.default_controller'); - } - - if ($request->request->has('inheritanceSource')) { - $createValues['contentMainDocumentId'] = $request->request->get('inheritanceSource'); - } - - switch ($request->request->get('type')) { - case 'page': - $document = Document\Page::create($parentDocument->getId(), $createValues, false); - $document->setTitle($request->request->get('title')); - $document->setProperty('navigation_name', 'text', $request->request->get('name'), false, false); - $document->save(); - $success = true; - - break; - case 'snippet': - $document = Document\Snippet::create($parentDocument->getId(), $createValues); - $success = true; - - break; - case 'email': //ckogler - $document = Document\Email::create($parentDocument->getId(), $createValues); - $success = true; - - break; - case 'link': - $document = Document\Link::create($parentDocument->getId(), $createValues); - $success = true; - - break; - case 'hardlink': - $document = Document\Hardlink::create($parentDocument->getId(), $createValues); - $success = true; - - break; - case 'folder': - $document = Document\Folder::create($parentDocument->getId(), $createValues); - $document->setPublished(true); - - try { - $document->save(); - $success = true; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - - break; - default: - $classname = OpenDxp::getContainer()->get('opendxp.class.resolver.document')->resolve($request->request->get('type')); - - if (Tool::classExists($classname)) { - $document = $classname::create($parentDocument->getId(), $createValues); - - try { - $document->save(); - $success = true; - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } + $result = $handler($payload); - break; - } - - Logger::debug("Unknown document type, can't add [ " . $request->request->get('type') . ' ] '); - - break; - } - } else { - $errorMessage = "prevented adding a document because document with same path+key [ $intendedPath ] already exists"; - Logger::debug($errorMessage); - } - } else { - $errorMessage = 'prevented adding a document because of missing permissions'; - Logger::debug($errorMessage); - } - - if ($success && $document instanceof Document) { - if ($translationsBaseDocumentId = $request->request->get('translationsBaseDocument')) { - $translationsBaseDocument = Document::getById((int) $translationsBaseDocumentId); - - $properties = $translationsBaseDocument->getProperties(); - $properties = [...$properties, ...$document->getProperties()]; - $document->setProperties($properties); - $document->setProperty('language', 'text', $request->request->get('language'), false, true); - $document->save(); - - $service = new Document\Service(); - $service->addTranslation($translationsBaseDocument, $document); - } - - return $this->adminJson([ - 'success' => $success, - 'id' => $document->getId(), - 'type' => $document->getType(), - ]); - } - - return $this->adminJson([ - 'success' => $success, - 'message' => $errorMessage, - ]); + return $this->adminJson(ApiResponse::ok([ + 'id' => $result->document->getId(), + 'type' => $result->document->getType(), + ])); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/delete', name: 'opendxp_admin_document_document_delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse + public function deleteAction( + DeleteDocumentPayload $payload, + DeleteDocumentHandler $handler, + ): JsonResponse { - $type = $request->request->get('type'); - - if ($type === 'children') { - $parentDocument = Document::getById((int) $request->request->get('id')); + $result = $handler($payload); - $list = new Document\Listing(); - $list->setCondition('`path` LIKE ?', [$list->escapeLike($parentDocument->getRealFullPath()) . '/%']); - $list->setLimit((int)$request->request->get('amount')); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('DESC'); - - $documents = $list->load(); - - $deletedItems = []; - foreach ($documents as $document) { - $deletedItems[$document->getId()] = $document->getRealFullPath(); - if ($document->isAllowed('delete') && !$document->isLocked()) { - $document->delete(); - } - } - - return $this->adminJson(['success' => true, 'deleted' => $deletedItems]); + if ($payload->type === 'children') { + return $this->adminJson(ApiResponse::ok(['deleted' => $result->deleted])); } - if ($id = $request->request->get('id')) { - $document = Document::getById((int) $id); - if ($document && $document->isAllowed('delete')) { - try { - if ($document->isLocked()) { - throw new Exception('prevented deleting document, because it is locked: ID: ' . $document->getId()); - } - $document->delete(); - - return $this->adminJson(['success' => true]); - } catch (Exception $e) { - Logger::err((string) $e); - - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - } - - throw $this->createAccessDeniedHttpException(); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - * @throws RuntimeException - */ + #[IsGranted(CorePermission::Documents->value)] #[Route('/update', name: 'opendxp_admin_document_document_update', methods: ['PUT'])] - public function updateAction(Request $request): JsonResponse + public function updateAction( + UpdateDocumentPayload $payload, + UpdateDocumentHandler $handler, + ): JsonResponse { - $data = ['success' => false]; - $allowUpdate = true; - - $document = Document::getById((int) $request->request->get('id')); - - $oldPath = $document->getDao()->getCurrentFullPath(); - $oldDocument = Document::getById($document->getId(), ['force' => true]); - - // this prevents the user from renaming, relocating (actions in the tree) if the newest version isn't published - // the reason is that otherwise the content of the newer not published version will be overwritten - if ($document instanceof Document\PageSnippet) { - $latestVersion = $document->getLatestVersion(); - if ($latestVersion && - $latestVersion->getData()->getModificationDate() != $document->getModificationDate() - ) { - return $this->adminJson( - [ - 'success' => false, - 'message' => "You can't rename or relocate if there's a newer not published version", - ]); - } - } - - if ($document->isAllowed('settings')) { - // if the position is changed the path must be changed || also from the children - if ($parentId = $request->request->get('parentId')) { - $parentDocument = Document::getById((int) $parentId); - - //check if parent is changed - if ($document->getParentId() !== $parentDocument->getId()) { - if (!$parentDocument->isAllowed('create')) { - throw new RuntimeException('Prevented moving document - no create permission on new parent.'); - } - - $intendedPath = $parentDocument->getRealPath(); - $pKey = $parentDocument->getKey(); - if (!empty($pKey)) { - $intendedPath .= $parentDocument->getKey() . '/'; - } - - $documentWithSamePath = Document::getByPath($intendedPath . $document->getKey()); - - if ($documentWithSamePath != null) { - $allowUpdate = false; - } - - if ($document->isLocked()) { - $allowUpdate = false; - } - } - } - - if ($allowUpdate) { - $blockedVars = ['id', 'controller', 'action', 'module']; - - if (!$document->isAllowed('rename') && $request->request->get('key')) { - $blockedVars[] = 'key'; - Logger::debug('prevented renaming document because of missing permissions '); - } - - $updateData = [...$request->request->all(), ...$request->query->all()]; - - foreach ($updateData as $key => $value) { - if (!in_array($key, $blockedVars)) { - $document->setValue($key, $value); - } - } - - $document->setUserModification($this->getAdminUser()->getId()); - - try { - $document->save(); + $result = $handler($payload); - if ($request->request->get('index') !== null) { - $this->updateIndexesOfDocumentSiblings($document, $request->request->get('index')); - } - - $data = [ - 'success' => true, - 'treeData' => $this->getTreeNodeConfig($document), - ]; - if ($oldPath && $oldPath != $document->getRealFullPath()) { - $this->firePostMoveEvent($document, $oldDocument, $oldPath); - } - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - $msg = 'prevented moving document, because document with same path+key already exists' . - ' or the document is locked. ID: ' . $document->getId(); - Logger::debug($msg); - - return $this->adminJson(['success' => false, 'message' => $msg]); - } - } elseif ($document->isAllowed('rename') && $request->request->get('key')) { - //just rename - try { - $document->setKey($request->request->get('key')); - $document->setUserModification($this->getAdminUser()->getId()); - $document->save(); - $data = [ - 'success' => true, - 'treeData' => $this->getTreeNodeConfig($document), - ]; - - if ($oldPath && $oldPath != $document->getRealFullPath()) { - $this->firePostMoveEvent($document, $oldDocument, $oldPath); - } - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } else { - Logger::debug('Prevented update document, because of missing permissions.'); - } - - return $this->adminJson($data); - } - - private function firePostMoveEvent(Document $document, Document $oldDocument, string $oldPath): void - { - $arguments = [ - 'oldPath' => $oldPath, - 'oldDocument' => $oldDocument, - ]; - $documentEvent = new OpenDxp\Event\Model\DocumentEvent($document, $arguments); - $this->dispatchEvent($documentEvent, OpenDxp\Event\DocumentEvents::POST_MOVE_ACTION); - } - - protected function updateIndexesOfDocumentSiblings(Document $document, int $newIndex): void - { - $updateLatestVersionIndex = function ($document, $newIndex): void { - if ($document instanceof Document\PageSnippet && $latestVersion = $document->getLatestVersion()) { - $document = $latestVersion->loadData(); - $document->setIndex($newIndex); - $latestVersion->save(); - } - }; - - // if changed the index change also all documents on the same level - - $document->saveIndex($newIndex); - - $list = new Document\Listing(); - $list->setCondition('parentId = ? AND id != ?', [$document->getParentId(), $document->getId()]); - $list->setOrderKey('index'); - $list->setOrder('asc'); - $childrenList = $list->load(); - - $count = 0; - foreach ($childrenList as $child) { - if ($count === $newIndex) { - $count++; - } - $child->saveIndex($count); - $updateLatestVersionIndex($child, $count); - $count++; - } + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData])); } #[Route('/doc-types', name: 'opendxp_admin_document_document_doctypesget', methods: ['GET'])] - public function docTypesGetAction(Request $request): JsonResponse + public function docTypesGetAction( + EmptyPayload $payload, + GetDocTypesListHandler $getDocTypesList, + ): JsonResponse { - // get list of types - $list = new DocType\Listing(); - - $docTypes = []; - foreach ($list->getDocTypes() as $type) { - if ($this->getAdminUser()->isAllowed($type->getId(), 'docType')) { - $data = $type->getObjectVars(); - $data['writeable'] = $type->isWriteable(); - $docTypes[] = $data; - } - } + $result = $getDocTypesList($payload); - return $this->adminJson(['data' => $docTypes, 'success' => true, 'total' => count($docTypes)]); + return $this->adminJson(ApiResponse::ok(['data' => $result->docTypes, 'total' => $result->total])); } + #[IsGranted(CorePermission::Documents->value)] + #[IsGranted(CorePermission::DocumentTypes->value)] #[Route('/doc-types', name: 'opendxp_admin_document_document_doctypes', methods: ['PUT', 'POST', 'DELETE'])] - public function docTypesAction(Request $request): JsonResponse - { - if ($request->request->get('data')) { - $this->checkPermission('document_types'); - - $data = $this->decodeJson($request->request->get('data')); - if ($request->query->get('xaction') === 'destroy') { - $type = Document\DocType::getById($data['id']); - if (!$type->isWriteable()) { - throw new ConfigWriteException(); - } - $type->delete(); - - return $this->adminJson(['success' => true, 'data' => []]); - } - if ($request->query->get('xaction') === 'update') { - // save type - $type = Document\DocType::getById($data['id']); - if (!$type->isWriteable()) { - throw new ConfigWriteException(); - } - $type->setValues($data); - $type->save(); - $responseData = $type->getObjectVars(); - $responseData['writeable'] = $type->isWriteable(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - - if ($request->query->get('xaction') === 'create') { - if (!(new DocType())->isWriteable()) { - throw new ConfigWriteException(); - } - unset($data['id']); - // save type - $type = Document\DocType::create(); - $type->setValues($data); - $type->save(); - $responseData = $type->getObjectVars(); - $responseData['writeable'] = $type->isWriteable(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - } - - return $this->adminJson(false); + public function docTypesAction( + Request $request, + #[MapQueryParameter] ?string $xaction = null, + ): Response + { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::docTypesDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::docTypesUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::docTypesCreateAction', [], $request->query->all()), + default => $this->adminJson(false), + }; } - /** - * @throws BadRequestHttpException If type is invalid - */ - #[Route('/get-doc-types', name: 'opendxp_admin_document_document_getdoctypes', methods: ['GET'])] - public function getDocTypesAction(Request $request): JsonResponse + #[IsGranted(CorePermission::Documents->value)] + #[IsGranted(CorePermission::DocumentTypes->value)] + #[Route('/doc-types-destroy', name: 'opendxp_admin_document_document_doctypes_destroy', methods: ['PUT', 'POST', 'DELETE'])] + public function docTypesDestroyAction( + DocTypePayload $payload, + DeleteDocTypeHandler $delete, + ): JsonResponse { - $list = new DocType\Listing(); - if ($type = $request->query->get('type')) { - if (!Document\Service::isValidType($type)) { - throw new BadRequestHttpException('Invalid type: ' . $type); - } - $list->setFilter(static fn (DocType $docType) => $docType->getType() === $type); - } - - $docTypes = []; - foreach ($list->getDocTypes() as $type) { - $docTypes[] = $type->getObjectVars(); - } - - return $this->adminJson(['docTypes' => $docTypes]); + return $this->adminJson(ApiResponse::ok(['data' => $delete($payload)->data])); } - #[Route('/version-to-session', name: 'opendxp_admin_document_document_versiontosession', methods: ['POST'])] - public function versionToSessionAction(Request $request): Response + #[IsGranted(CorePermission::Documents->value)] + #[IsGranted(CorePermission::DocumentTypes->value)] + #[Route('/doc-types-update', name: 'opendxp_admin_document_document_doctypes_update', methods: ['PUT', 'POST', 'DELETE'])] + public function docTypesUpdateAction( + DocTypePayload $payload, + UpdateDocTypeHandler $update, + ): JsonResponse { - $id = $request->request->getInt('id'); - $version = Version::getById($id); - $document = $version?->loadData(); - if (!$document) { - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - Document\Service::saveElementToSession($document, $request->getSession()->getId()); - - return new Response(); + return $this->adminJson(ApiResponse::ok(['data' => $update($payload)->data])); } - #[Route('/publish-version', name: 'opendxp_admin_document_document_publishversion', methods: ['POST'])] - public function publishVersionAction(Request $request): JsonResponse + #[IsGranted(CorePermission::Documents->value)] + #[IsGranted(CorePermission::DocumentTypes->value)] + #[Route('/doc-types-create', name: 'opendxp_admin_document_document_doctypes_create', methods: ['PUT', 'POST', 'DELETE'])] + public function docTypesCreateAction( + DocTypePayload $payload, + CreateDocTypeHandler $create, + ): JsonResponse { - $this->versionToSessionAction($request); - - $id = $request->request->getInt('id'); - $version = Version::getById($id); - $document = $version?->loadData(); - if (!$document) { - throw $this->createNotFoundException('Version with id [' . $id . "] doesn't exist"); - } - - $currentDocument = Document::getById($document->getId()); - if ($currentDocument->isAllowed('publish')) { - $document->setPublished(true); - - try { - $document->setKey($currentDocument->getKey()); - $document->setPath($currentDocument->getRealPath()); - $document->setUserModification($this->getAdminUser()->getId()); - - $document->save(); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } + return $this->adminJson(ApiResponse::ok(['data' => $create($payload)->data])); + } - $treeData = []; - $this->addAdminStyle($document, ElementAdminStyleEvent::CONTEXT_EDITOR, $treeData); + #[IsGranted(CorePermission::Documents->value)] + #[Route('/get-doc-types', name: 'opendxp_admin_document_document_getdoctypes', methods: ['GET'])] + public function getDocTypesAction( + GetDocTypesByTypePayload $payload, + GetDocTypesByTypeHandler $getDocTypesByType, + ): JsonResponse + { + $result = $getDocTypesByType($payload); - return $this->adminJson(['success' => true, 'treeData' => $treeData]); + return $this->adminJson(['docTypes' => $result->docTypes]); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/get-site-custom-settings', name: 'opendxp_admin_document_document_get_site_custom_settings', methods: ['POST'])] - public function getSiteCustomSettingsAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function getSiteCustomSettingsAction( + GetSiteCustomSettingsPayload $payload, + GetSiteCustomSettingsHandler $getSiteCustomSettings, + ): JsonResponse { - $site = Site::getById($request->request->getInt('id')); - - $event = new SiteCustomSettingsEvent($site); - $eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS); + $result = $getSiteCustomSettings($payload); - $customSettings = $event->getConfigNodes(); - - return $this->adminJson([ - 'data' => $customSettings, - ]); + return $this->adminJson(['data' => $result->nodes]); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/update-site', name: 'opendxp_admin_document_document_updatesite', methods: ['PUT'])] - public function updateSiteAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function updateSiteAction( + UpdateSitePayload $payload, + UpdateSiteHandler $handler, + ): JsonResponse { - $domains = $request->request->getString('domains'); - $domains = str_replace(' ', '', $domains); - $domains = $domains ? explode("\n", $domains) : []; + $result = $handler($payload); - if (!$site = Site::getByRootId($request->request->getInt('id'))) { - $site = Site::create([ - 'rootId' => $request->request->getInt('id'), - ]); - } - - $localizedErrorDocuments = []; - $validLanguages = Tool::getValidLanguages(); - - foreach ($validLanguages as $language) { - // localized error pages - $requestValue = $request->request->get(sprintf('errorDocument_localized_%s', $language)); - - if (isset($requestValue)) { - $localizedErrorDocuments[$language] = $requestValue; - } - } - - $event = new SiteCustomSettingsEvent($site); - $eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS); - - $customSettings = []; - foreach ($event->getConfigNodes() as $scope => $nodes) { - foreach ($nodes as $node) { - $requestValueName = sprintf('customSettings_%s_%s', $scope, $node['name']); - if ($request->request->has($requestValueName)) { - $value = $request->request->get($requestValueName); - if ($node['type'] === OpenDxp\Bundle\AdminBundle\Enum\SiteCustomConfigNodeType::CHECKBOX->value) { - $value = $value === 'true'; - } - - $customSettings[$scope][$node['name']] = $value; - } - } - } - - $site->setDomains($domains); - $site->setMainDomain($request->request->getString('mainDomain')); - $site->setErrorDocument($request->request->getString('errorDocument')); - $site->setLocalizedErrorDocuments($localizedErrorDocuments); - $site->setRedirectToMainDomain($request->request->getBoolean('redirectToMainDomain')); - $site->setCustomSettings(count($customSettings) === 0 ? null : $customSettings); - $site->save(); - - $site->setRootDocument(null); // do not send the document to the frontend - - return $this->adminJson($site->getObjectVars()); + return $this->adminJson($result->siteVars); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/remove-site', name: 'opendxp_admin_document_document_removesite', methods: ['DELETE'])] - public function removeSiteAction(Request $request): JsonResponse - { - $site = Site::getByRootId($request->request->getInt('id')); - $site->delete(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/copy-info', name: 'opendxp_admin_document_document_copyinfo', methods: ['GET'])] - public function copyInfoAction(Request $request): JsonResponse - { - $transactionId = time(); - $pasteJobs = []; - - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($transactionId): void { - $session->set((string) $transactionId, ['idMapping' => []]); - }, 'opendxp_copy'); - - if ($request->query->get('type') === 'recursive' || $request->query->get('type') === 'recursive-update-references') { - $document = Document::getById((int) $request->query->get('sourceId')); - - // first of all the new parent - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_document_document_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => 'child', - 'language' => $request->query->get('language'), - 'enableInheritance' => $request->query->get('enableInheritance'), - 'transactionId' => $transactionId, - 'saveParentId' => true, - 'resetIndex' => true, - ], - ]]; - - $childIds = []; - if ($document->hasChildren()) { - // get amount of children - $list = new Document\Listing(); - $list->setCondition('`path` LIKE ?', [$list->escapeLike($document->getRealFullPath()) . '/%']); - $list->setOrderKey('LENGTH(`path`)', false); - $list->setOrder('ASC'); - $childIds = $list->loadIdList(); - - if (count($childIds) > 0) { - foreach ($childIds as $id) { - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_document_document_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $id, - 'targetParentId' => $request->query->get('targetId'), - 'sourceParentId' => $request->query->get('sourceId'), - 'type' => 'child', - 'language' => $request->query->get('language'), - 'enableInheritance' => $request->query->get('enableInheritance'), - 'transactionId' => $transactionId, - ], - ]]; - } - } - } - - // add id-rewrite steps - if ($request->query->get('type') === 'recursive-update-references') { - for ($i = 0; $i < (count($childIds) + 1); $i++) { - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_document_document_copyrewriteids'), - 'method' => 'PUT', - 'params' => [ - 'transactionId' => $transactionId, - 'enableInheritance' => $request->query->get('enableInheritance'), - '_dc' => uniqid('', false), - ], - ]]; - } - } - } elseif ($request->query->get('type') === 'child' || $request->query->get('type') === 'replace') { - // the object itself is the last one - $pasteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_document_document_copy'), - 'method' => 'POST', - 'params' => [ - 'sourceId' => $request->query->get('sourceId'), - 'targetId' => $request->query->get('targetId'), - 'type' => $request->query->get('type'), - 'language' => $request->query->get('language'), - 'enableInheritance' => $request->query->get('enableInheritance'), - 'transactionId' => $transactionId, - 'resetIndex' => ($request->query->get('type') === 'child'), - ], - ]]; - } - - return $this->adminJson([ - 'pastejobs' => $pasteJobs, - ]); - } - - #[Route('/copy-rewrite-ids', name: 'opendxp_admin_document_document_copyrewriteids', methods: ['PUT'])] - public function copyRewriteIdsAction(Request $request): JsonResponse - { - $transactionId = $request->request->get('transactionId'); - - $idStore = Session::useBag($request->getSession(), static fn (AttributeBagInterface $session) => $session->get($transactionId), 'opendxp_copy'); - - if (!array_key_exists('rewrite-stack', $idStore)) { - $idStore['rewrite-stack'] = array_values($idStore['idMapping']); - } - - $id = array_shift($idStore['rewrite-stack']); - $document = Document::getById((int) $id); - - if ($document) { - // create rewriteIds() config parameter - $rewriteConfig = ['document' => $idStore['idMapping']]; - - $document = Document\Service::rewriteIds($document, $rewriteConfig, [ - 'enableInheritance' => $request->request->get('enableInheritance') === 'true', - ]); - - $document->setUserModification($this->getAdminUser()->getId()); - $document->save(); - } - - // write the store back to the session - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($transactionId, $idStore): void { - $session->set($transactionId, $idStore); - }, 'opendxp_copy'); - - return $this->adminJson([ - 'success' => true, - 'id' => $id, - ]); - } - - #[Route('/copy', name: 'opendxp_admin_document_document_copy', methods: ['POST'])] - public function copyAction(Request $request): JsonResponse - { - $success = false; - $sourceId = (int)$request->request->get('sourceId'); - $source = Document::getById($sourceId); - $session = Session::getSessionBag($request->getSession(), 'opendxp_copy'); - - $targetId = (int)$request->request->get('targetId'); - - $sessionBag = $session->get($request->request->get('transactionId')); - - if ($request->request->get('targetParentId')) { - $sourceParent = Document::getById((int) $request->request->get('sourceParentId')); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($sessionBag['parentId']) { - $targetParent = Document::getById((int) $sessionBag['parentId']); - } else { - $targetParent = Document::getById((int) $request->request->get('targetParentId')); - } - - $targetPath = preg_replace('@^' . $sourceParent->getRealFullPath() . '@', $targetParent . '/', $source->getRealPath()); - $target = Document::getByPath($targetPath); - } else { - $target = Document::getById($targetId); - } - - if ($target instanceof Document) { - if ($target->isAllowed('create')) { - if ($source !== null) { - if ($source instanceof Document\PageSnippet && $latestVersion = $source->getLatestVersion()) { - $source = $latestVersion->loadData(); - $source->setPublished(false); //as latest version is used which is not published - } - - if ($request->request->get('type') === 'child') { - $enableInheritance = $request->request->get('enableInheritance') === 'true'; - - $language = (string) $request->request->get('language') ?: null; - if ($language && !Tool::isValidLanguage($language)) { - throw new BadRequestHttpException('Invalid language: ' . $language); - } - - $resetIndex = $request->request->get('resetIndex') === 'true'; - - $newDocument = $this->_documentService->copyAsChild($target, $source, $enableInheritance, $resetIndex, $language); - - $sessionBag['idMapping'][(int)$source->getId()] = (int)$newDocument->getId(); - - // this is because the key can get the prefix "_copy" if the target does already exists - if ($request->request->get('saveParentId')) { - $sessionBag['parentId'] = $newDocument->getId(); - } - $session->set($request->request->get('transactionId'), $sessionBag); - } elseif ($request->request->get('type') === 'replace') { - $this->_documentService->copyContents($target, $source); - } - - $success = true; - } else { - Logger::error('prevended copy/paste because document with same path+key already exists in this location'); - } - } else { - Logger::error('could not execute copy/paste because of missing permissions on target [ ' . $targetId . ' ]'); - - throw $this->createAccessDeniedHttpException(); - } - } - - return $this->adminJson(['success' => $success]); - } - - #[Route('/diff-versions/from/{from}/to/{to}', name: 'opendxp_admin_document_document_diffversions', requirements: ['from' => "\d+", 'to' => "\d+"], methods: ['GET'])] - public function diffVersionsAction( - Request $request, - int $from, - int $to, - DocumentRendererInterface $documentRenderer, - RouterInterface $router - ): Response { - // return with error if prerequisites do not match - if (!HtmlToImage::isSupported() || !class_exists('Imagick')) { - return $this->render('@OpenDxpAdmin/admin/document/document/diff_versions_unsupported.html.twig'); - } - - $versionFrom = Version::getById($from); - $docFrom = $versionFrom?->loadData(); - - if (!$docFrom) { - throw $this->createNotFoundException('Version with id [' . $from . "] doesn't exist"); - } - - $versionTo = Version::getById($to); - $docTo = $versionTo?->loadData(); - - if (!$docTo) { - throw $this->createNotFoundException('Version with id [' . $to . "] doesn't exist"); - } - - $comparisonId = uniqid(date('Y-m-d') . '-', true); - $tempFileTemplate = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/version-diff-tmp-' . $comparisonId . '-%s.%s'; - $fromImageFile = sprintf($tempFileTemplate, 'from', 'png'); - $toImageFile = sprintf($tempFileTemplate, 'to', 'png'); - $fromHtmlFile = sprintf($tempFileTemplate, 'from', 'html'); - $toHtmlFile = sprintf($tempFileTemplate, 'to', 'html'); - - $viewParams = []; - - $docContentFrom = $documentRenderer->render($docFrom); - $docContentTo = $documentRenderer->render($docTo); - - file_put_contents($fromHtmlFile, $docContentFrom); - file_put_contents($toHtmlFile, $docContentTo); - - $prefix = Config::getSystemConfiguration('documents')['preview_url_prefix']; - if (empty($prefix)) { - $prefix = $request->getSchemeAndHttpHost(); - } - - try { - HtmlToImage::convert($prefix . $router->generate('opendxp_admin_document_document_diff_versions_html', ['id' => basename($fromHtmlFile)]), $fromImageFile); - HtmlToImage::convert($prefix . $router->generate('opendxp_admin_document_document_diff_versions_html', ['id' => basename($toHtmlFile)]), $toImageFile); - } finally { - unlink($fromHtmlFile); - unlink($toHtmlFile); - } - - $image1 = new Imagick($fromImageFile); - $image2 = new Imagick($toImageFile); - - if ($image1->getImageWidth() === $image2->getImageWidth() && $image1->getImageHeight() === $image2->getImageHeight()) { - $result = $image1->compareImages($image2, Imagick::METRIC_MEANSQUAREERROR); - $result[0]->setImageFormat('png'); - - $viewParams['image'] = base64_encode($result[0]->getImageBlob()); - - $result[0]->clear(); - $result[0]->destroy(); - - } else { - $viewParams['image1'] = base64_encode(file_get_contents($fromImageFile)); - $viewParams['image2'] = base64_encode(file_get_contents($toImageFile)); - } - - // cleanup - $image1->clear(); - $image1->destroy(); - $image2->clear(); - $image2->destroy(); - - unlink($fromImageFile); - unlink($toImageFile); - - return $this->render('@OpenDxpAdmin/admin/document/document/diff_versions.html.twig', $viewParams); - } - - public function diffVersionsHtmlAction(Request $request): BinaryFileResponse + public function removeSiteAction( + RemoveSitePayload $payload, + RemoveSiteHandler $removeSite, + ): JsonResponse { - $file = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . basename($request->query->get('id')); - if (file_exists($file)) { - return new BinaryFileResponse($file); - } + $removeSite($payload); - throw $this->createNotFoundException('Version diff file not found'); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/get-id-for-path', name: 'opendxp_admin_document_document_getidforpath', methods: ['GET'])] - public function getIdForPathAction(Request $request): JsonResponse + public function getIdForPathAction( + GetDocumentIdForPathPayload $payload, + GetDocumentIdForPathHandler $getDocumentIdForPath, + ): JsonResponse { - if ($doc = Document::getByPath($request->query->get('path'))) { - return $this->adminJson([ - 'id' => $doc->getId(), - 'type' => $doc->getType(), - ]); + $result = $getDocumentIdForPath($payload); + if (!$result) { + return $this->adminJson(false); } - return $this->adminJson(false); + return $this->adminJson(['id' => $result->id, 'type' => $result->type]); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/language-tree', name: 'opendxp_admin_document_document_languagetree', methods: ['GET'])] - public function languageTreeAction(Request $request): JsonResponse + public function languageTreeAction( + GetLanguageTreePayload $payload, + GetLanguageTreeHandler $handler, + ): JsonResponse { - $document = Document::getById((int) $request->query->get('node')); + $result = $handler($payload); - $languages = explode(',', $request->query->get('languages')); - - $result = []; - foreach ($document->getChildren() as $child) { - $result[] = $this->getTranslationTreeNodeConfig($child, $languages); - } - - return $this->adminJson($result); + return $this->adminJson($result->nodes); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Documents->value)] #[Route('/language-tree-root', name: 'opendxp_admin_document_document_languagetreeroot', methods: ['GET'])] - public function languageTreeRootAction(Request $request): JsonResponse + public function languageTreeRootAction( + GetLanguageTreeRootPayload $payload, + GetLanguageTreeRootHandler $handler, + ): JsonResponse { - $document = Document::getById((int) $request->query->get('id')); - - if (!$document) { - return $this->adminJson([ - 'success' => false, - ]); - } - $service = new Document\Service(); - - $locales = Tool::getSupportedLocales(); - - $lang = $document->getProperty('language'); - - $columns = [ - [ - 'xtype' => 'treecolumn', - 'text' => $lang ? $locales[$lang] : '', - 'dataIndex' => 'text', - 'cls' => $lang ? 'x-column-header_' . strtolower($lang) : null, - 'width' => 300, - 'sortable' => false, - ], - ]; - - $translations = $service->getTranslations($document); - - $combinedTranslations = $translations; - - if ($parentDocument = $document->getParent()) { - $parentTranslations = $service->getTranslations($parentDocument); - foreach ($parentTranslations as $language => $languageDocumentId) { - $combinedTranslations[$language] = $translations[$language] ?? $languageDocumentId; - } - } - - foreach ($combinedTranslations as $language => $languageDocumentId) { - $languageDocument = Document::getById($languageDocumentId); - - if ($languageDocument && $languageDocument->isAllowed('list') && $language != $document->getProperty('language')) { - $columns[] = [ - 'text' => $locales[$language], - 'dataIndex' => $language, - 'cls' => 'x-column-header_' . strtolower($language), - 'width' => 300, - 'sortable' => false, - ]; - } - } + $result = $handler($payload); return $this->adminJson([ - 'root' => $this->getTranslationTreeNodeConfig($document, array_keys($translations), $translations), - 'columns' => $columns, - 'languages' => array_keys($translations), + 'root' => $result->root, + 'columns' => $result->columns, + 'languages' => $result->languages, ]); } - private function getTranslationTreeNodeConfig(Document $document, array $languages, ?array $translations = null): array - { - $service = new Document\Service(); - - $config = $this->elementService->getElementTreeNodeConfig($document); - - $translations = is_null($translations) ? $service->getTranslations($document) : $translations; - - foreach ($languages as $language) { - if ($languageDocument = $translations[$language] ?? false) { - $languageDocument = Document::getById((int)$languageDocument); - $config[$language] = [ - 'text' => $languageDocument->getKey(), - 'id' => $languageDocument->getId(), - 'type' => $languageDocument->getType(), - 'fullPath' => $languageDocument->getFullPath(), - 'published' => $languageDocument->getPublished(), - 'itemType' => 'document', - 'permissions' => $languageDocument->getUserPermissions($this->getAdminUser()), - ]; - } elseif (!$document instanceof Document\Folder) { - $config[$language] = [ - 'text' => '--', - 'itemType' => 'empty', - ]; - } - } - - return $config; - } - + #[IsGranted(CorePermission::Documents->value)] #[Route('/convert', name: 'opendxp_admin_document_document_convert', methods: ['PUT'])] - public function convertAction(Request $request): JsonResponse + public function convertAction( + ConvertDocumentPayload $payload, + ConvertDocumentHandler $handler, + ): JsonResponse { - $document = Document::getById((int) $request->request->get('id')); - if (!$document) { - throw $this->createNotFoundException(); - } - - $type = $request->request->get('type'); - $class = '\\OpenDxp\\Model\\Document\\' . ucfirst($type); - if (Tool::classExists($class)) { - $new = new $class; - - // overwrite internal store to avoid "duplicate full path" error - RuntimeCache::set('document_' . $document->getId(), $new); - - $props = $document->getObjectVars(); - foreach ($props as $name => $value) { - if (in_array($name, ['children', 'siblings', 'scheduledTasks', 'controller', 'template'])) { - continue; - } - $new->setValue($name, $value); - } - - if ($type === 'hardlink' || $type === 'folder') { - // remove navigation settings - foreach (['name', 'title', 'target', 'exclude', 'class', 'anchor', 'parameters', 'relation', 'accesskey', 'tabindex'] as $propertyName) { - $new->removeProperty('navigation_' . $propertyName); - } - } + $handler($payload); - $new->setType($type); - $new->save(); - } - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/translation-determine-parent', name: 'opendxp_admin_document_document_translationdetermineparent', methods: ['GET'])] - public function translationDetermineParentAction(Request $request): JsonResponse + public function translationDetermineParentAction( + DetermineTranslationParentPayload $payload, + DetermineTranslationParentHandler $handler, + ): JsonResponse { - $success = false; - $targetDocument = null; - - $document = Document::getById((int) $request->query->get('id')); - if ($document) { - $service = new Document\Service(); - $document = $document->getId() === 1 ? $document : $document->getParent(); - - $translations = $service->getTranslations($document); - if (isset($translations[$request->query->get('language')])) { - $targetDocument = Document::getById($translations[$request->query->get('language')]); - $success = true; - } - } + $result = $handler($payload); - return $this->adminJson([ - 'success' => $success, - 'targetPath' => $targetDocument?->getRealFullPath(), - 'targetId' => $targetDocument?->getid(), - ]); + return $this->adminJson(ApiResponse::fromBool($result->found, [ + 'targetPath' => $result->targetPath, + 'targetId' => $result->targetId, + ])); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/translation-add', name: 'opendxp_admin_document_document_translationadd', methods: ['POST'])] - public function translationAddAction(Request $request): JsonResponse + public function translationAddAction( + AddDocumentTranslationPayload $payload, + AddDocumentTranslationHandler $handler, + ): JsonResponse { - $sourceDocument = Document::getById((int) $request->request->get('sourceId')); - $targetDocument = Document::getByPath($request->request->get('targetPath')); - - if ($sourceDocument && $targetDocument) { - if (empty($sourceDocument->getProperty('language'))) { - throw new Exception(sprintf('Source Document(ID:%s) Language(Properties) missing', $sourceDocument->getId())); - } - - if (empty($targetDocument->getProperty('language'))) { - throw new Exception(sprintf('Target Document(ID:%s) Language(Properties) missing', $sourceDocument->getId())); - } + $handler($payload); - $service = new Document\Service; - if ($service->getTranslationSourceId($targetDocument) != $targetDocument->getId()) { - throw new Exception('Target Document already linked to Source Document ID('.$service->getTranslationSourceId($targetDocument).'). Please unlink existing relation first.'); - } - $service->addTranslation($sourceDocument, $targetDocument); - } - - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/translation-remove', name: 'opendxp_admin_document_document_translationremove', methods: ['DELETE'])] - public function translationRemoveAction(Request $request): JsonResponse + public function translationRemoveAction( + RemoveDocumentTranslationPayload $payload, + RemoveDocumentTranslationHandler $handler, + ): JsonResponse { - $sourceDocument = Document::getById($request->request->getInt('sourceId')); - $targetDocument = Document::getById($request->request->getInt('targetId')); - if ($sourceDocument && $targetDocument) { - $service = new Document\Service; - $service->removeTranslationLink($sourceDocument, $targetDocument); - } + $handler($payload); - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Documents->value)] #[Route('/translation-check-language', name: 'opendxp_admin_document_document_translationchecklanguage', methods: ['GET'])] - public function translationCheckLanguageAction(Request $request): JsonResponse - { - $success = false; - $language = null; - $translationLinks = null; - - $document = Document::getByPath($request->query->get('path')); - if ($document) { - $language = $document->getProperty('language'); - if ($language) { - $success = true; - } - - //check if document is already linked to other langauges - $translationLinks = array_keys($this->_documentService->getTranslations($document)); - } - - return $this->adminJson([ - 'success' => $success, - 'language' => $language, - 'translationLinks' => $translationLinks, - ]); - } - - public function onKernelControllerEvent(ControllerEvent $event): void + public function translationCheckLanguageAction( + CheckTranslationLanguagePayload $payload, + CheckTranslationLanguageHandler $handler, + ): JsonResponse { - if (!$event->isMainRequest()) { - return; - } - - // check permissions - $this->checkActionPermission($event, 'documents', ['docTypesGetAction', 'diffVersionsHtmlAction']); + $result = $handler($payload); - $this->_documentService = new Document\Service($this->getAdminUser()); + return $this->adminJson(ApiResponse::fromBool($result->found, [ + 'language' => $result->language, + 'translationLinks' => $result->translationLinks, + ])); } #[Override] diff --git a/src/Controller/Admin/Document/DocumentControllerBase.php b/src/Controller/Admin/Document/DocumentControllerBase.php index 7e1d79be..2b57ddf2 100644 --- a/src/Controller/Admin/Document/DocumentControllerBase.php +++ b/src/Controller/Admin/Document/DocumentControllerBase.php @@ -18,38 +18,31 @@ use Exception; use OpenDxp; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\AdminStyleTrait; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\ApplySchedulerDataTrait; -use OpenDxp\Bundle\AdminBundle\Controller\Traits\UserNameTrait; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Event\ElementAdminStyleEvent; +use OpenDxp\Bundle\AdminBundle\Handler\Document\ChangeMainDocument\ChangeMainDocumentHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\ChangeMainDocument\ChangeMainDocumentPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\RemoveFromSession\RemoveFromSessionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\SaveToSession\SaveToSessionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\SaveToSession\SaveToSessionPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdBodyPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; -use OpenDxp\Bundle\PersonalizationBundle\Model\Document\Targeting\TargetingDocumentInterface; -use OpenDxp\Controller\KernelControllerEventInterface; use OpenDxp\Controller\Traits\ElementEditLockHelperTrait; -use OpenDxp\Logger; use OpenDxp\Model; -use OpenDxp\Model\Document; -use OpenDxp\Model\Element; use OpenDxp\Model\Element\ElementInterface; -use OpenDxp\Model\Property; -use OpenDxp\Model\Version; use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ -abstract class DocumentControllerBase extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(CorePermission::Documents->value)] +abstract class DocumentControllerBase extends AdminAbstractController { - use ApplySchedulerDataTrait; - use AdminStyleTrait; use ElementEditLockHelperTrait; - use UserNameTrait; public const string TASK_PUBLISH = 'publish'; @@ -65,365 +58,66 @@ abstract class DocumentControllerBase extends AdminAbstractController implements public const string TASK_DELETE = 'delete'; - public function __construct(protected ElementServiceInterface $elementService) - { - } - - /** - * @throws Exception - */ - protected function preSendDataActions(array &$data, Model\Document $document, ?Version $draftVersion = null): JsonResponse - { - $documentFromDatabase = Model\Document::getById($document->getId(), ['force' => true]); - - $data['versionDate'] = $documentFromDatabase->getModificationDate(); - $data['userPermissions'] = $document->getUserPermissions(); - $data['idPath'] = Element\Service::getIdPath($document); - - $data['php'] = [ - 'classes' => [$document::class, ...array_values(class_parents($document))], - 'interfaces' => array_values(class_implements($document)), - ]; - - $this->addAdminStyle($document, ElementAdminStyleEvent::CONTEXT_EDITOR, $data); - - if ($draftVersion && $documentFromDatabase->getModificationDate() < $draftVersion->getDate()) { - $data['draft'] = [ - 'id' => $draftVersion->getId(), - 'modificationDate' => $draftVersion->getDate(), - 'isAutoSave' => $draftVersion->isAutoSave(), - ]; - } - - $event = new GenericEvent($this, [ - 'data' => $data, - 'document' => $document, - ]); - OpenDxp::getEventDispatcher()->dispatch($event, AdminEvents::DOCUMENT_GET_PRE_SEND_DATA); - $data = $event->getArgument('data'); - - if ($document->isAllowed('view')) { - return $this->adminJson($data); - } - - throw $this->createAccessDeniedHttpException(); - } - - protected function addPropertiesToDocument(Request $request, Model\Document $document): void - { - // properties - if ($request->request->has('properties')) { - $properties = []; - // assign inherited properties - foreach ($document->getProperties() as $p) { - if ($p->isInherited()) { - $properties[$p->getName()] = $p; - } - } - - $propertiesData = $this->decodeJson($request->request->get('properties')); - - if (is_array($propertiesData)) { - foreach ($propertiesData as $propertyName => $propertyData) { - $value = $propertyData['data']; - - try { - $property = new Property(); - $property->setType($propertyData['type']); - $property->setName($propertyName); - $property->setCtype('document'); - $property->setDataFromEditmode($value); - $property->setInheritable($propertyData['inheritable']); - - if ($propertyName === 'language') { - $property->setInherited($this->getPropertyInheritance($document, $propertyName, $value)); - } - - $properties[$propertyName] = $property; - } catch (Exception) { - Logger::warning("Can't add " . $propertyName . ' to document ' . $document->getRealFullPath()); - } - } - } - if ($document->isAllowed('properties')) { - $document->setProperties($properties); - } - } - - // force loading of properties - $document->getProperties(); - } - - protected function addSettingsToDocument(Request $request, Model\Document $document): void - { - // settings - if ($request->request->has('settings') && $document->isAllowed('settings')) { - $settings = $this->decodeJson($request->request->get('settings')); - if (array_key_exists('prettyUrl', $settings)) { - $settings['prettyUrl'] = htmlspecialchars($settings['prettyUrl']); - } - $document->setValues($settings); - } - } - - protected function addDataToDocument(Request $request, Model\Document $document): void - { - if ($document instanceof Model\Document\PageSnippet) { - $isTargetSpecificEditable = - interface_exists(TargetingDocumentInterface::class) - && $document instanceof TargetingDocumentInterface - && $document->hasTargetGroupSpecificEditables(); - - if ($request->request->get('appendEditables') || $isTargetSpecificEditable) { - $document->getEditables(); - } else { - // ensure no editables (e.g. from session, version, ...) are still referenced - $document->setEditables(null); - } - - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - foreach ($data as $name => $value) { - $data = $value['data'] ?? null; - $type = $value['type']; - $document->setRawEditable($name, $type, $data); - } - } - } - } - - protected function addTranslationsData(Model\Document $document, array &$data): void - { - $service = new Model\Document\Service; - $translations = $service->getTranslations($document); - $unlinkTranslations = $service->getTranslations($document, 'unlink'); - $language = $document->getProperty('language'); - unset($translations[$language], $unlinkTranslations[$language]); - $data['translations'] = $translations; - $data['unlinkTranslations'] = $unlinkTranslations; + public function __construct( + protected ElementServiceInterface $elementService, + ) { } #[Route('/save-to-session', name: 'savetosession', methods: ['POST'])] - public function saveToSessionAction(Request $request): JsonResponse - { - if ($documentId = (int) $request->request->get('id')) { - if (!$document = Model\Document\Service::getElementFromSession('document', $documentId, $request->getSession()->getId())) { - $document = Model\Document\PageSnippet::getById($documentId); - if (!$document) { - throw $this->createNotFoundException(); - } - $document = $this->getLatestVersion($document); - } - - // set dump state to true otherwise the properties will be removed because of the session-serialize - $document->setInDumpState(true); - $this->setValuesToDocument($request, $document); - - Model\Document\Service::saveElementToSession($document, $request->getSession()->getId()); - } - - return $this->adminJson(['success' => true]); - } - - protected function saveToSession(Model\Document $doc, SessionInterface $session, bool $useForSave = false): void - { - // save to session - Model\Document\Service::saveElementToSession($doc, $session->getId()); - - if ($useForSave) { - Model\Document\Service::saveElementToSession($doc, $session->getId(), '_useForSave'); - } - } - - /** - * @return Model\Document|null $sessionDocument - */ - protected function getFromSession(Model\Document $doc, SessionInterface $session): ?Model\Document - { - $sessionDocument = null; - - // check if there's a document in session which should be used as data-source - // see also PageController::clearEditableDataAction() | this is necessary to reset all fields and to get rid of - // outdated and unused data elements in this document (eg. entries of area-blocks) - - if (($sessionDocument = Model\Document\Service::getElementFromSession('document', $doc->getId(), $session->getId())) && - (Model\Document\Service::getElementFromSession('document', $doc->getId(), $session->getId(), '_useForSave'))) { - Model\Document\Service::removeElementFromSession('document', $doc->getId(), $session->getId(), '_useForSave'); - } + public function saveToSessionAction( + SaveToSessionPayload $payload, + SaveToSessionHandler $handler, + ): JsonResponse { + $handler($payload); - return $sessionDocument; + return $this->adminJson(ApiResponse::ok()); } #[Route('/remove-from-session', name: 'removefromsession', methods: ['DELETE'])] - public function removeFromSessionAction(Request $request): JsonResponse - { - Model\Document\Service::removeElementFromSession('document', $request->request->get('id'), $request->getSession()->getId()); - - return $this->adminJson(['success' => true]); - } - - protected function minimizeProperties(Model\Document $document, array &$data): void - { - $data['properties'] = Model\Element\Service::minimizePropertiesForEditmode($document->getProperties()); - } - - protected function getPropertyInheritance(Model\Document $document, string $propertyName, mixed $propertyValue): bool - { - if ($document->getParent()) { - return $propertyValue == $document->getParent()->getProperty($propertyName); - } + public function removeFromSessionAction( + IdBodyPayload $payload, + RemoveFromSessionHandler $handler, + ): JsonResponse { + $handler($payload); - return false; - } - - /** - * @template T of Model\Document\PageSnippet - * - * @param T $document - * - * @return T - */ - protected function getLatestVersion(Model\Document\PageSnippet $document, ?Version &$draftVersion = null): Model\Document\PageSnippet - { - $latestVersion = $document->getLatestVersion($this->getAdminUser()->getId()); - if ($latestVersion) { - $latestDoc = $latestVersion->loadData(); - if ($latestDoc instanceof Model\Document\PageSnippet) { - $draftVersion = $latestVersion; - - return $latestDoc; - } - } - - return $document; + return $this->adminJson(ApiResponse::ok()); } /** * This is used for pages and snippets to change the main document (which is not saved with the normal save button) - * - * @throws Exception */ #[Route('/change-main-document', name: 'changemaindocument', methods: ['PUT'])] - public function changeMainDocumentAction(Request $request): JsonResponse - { - $doc = Model\Document\PageSnippet::getById((int) $request->request->get('id')); - if ($doc instanceof Model\Document\PageSnippet) { - $doc->setEditables([]); - $doc->setContentMainDocumentId($request->request->get('contentMainDocumentPath'), true); - $doc->saveVersion(); - } + public function changeMainDocumentAction( + ChangeMainDocumentPayload $payload, + ChangeMainDocumentHandler $changeMainDocument, + ): JsonResponse { + $changeMainDocument($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - // check permissions - $this->checkPermission('documents'); - } - - abstract protected function setValuesToDocument(Request $request, Model\Document $document): void; - - protected function handleTask(string $task, Model\Document\PageSnippet $page): void - { - if ($task === self::TASK_PUBLISH || $task === self::TASK_VERSION) { - $page->deleteAutoSaveVersions($this->getAdminUser()->getId()); - } - } - - protected function checkForLock(Model\Document $document, string $sessionId): JsonResponse|bool + public function getTreeNodeConfig(ElementInterface $element): array { - // check for lock - if ($document->isAllowed(self::TASK_SAVE) - || $document->isAllowed(self::TASK_PUBLISH) - || $document->isAllowed(self::TASK_UNPUBLISH) - || $document->isAllowed(self::TASK_DELETE)) { - if (Element\Editlock::isLocked($document->getId(), 'document', $sessionId)) { - return $this->getEditLockResponse($document->getId(), 'document'); - } - Element\Editlock::lock($document->getId(), 'document', $sessionId); - } - - return true; + return $this->elementService->getElementTreeNodeConfig($element); } /** - * @throws Element\ValidationException * @throws Exception */ - protected function saveDocument(Model\Document $document, Request $request, bool $latestVersion = false, ?string $task = null): array + protected function preSendDataActions(array $data, Model\Document $document): JsonResponse { - if ($latestVersion && $document instanceof Model\Document\PageSnippet) { - $document = $this->getLatestVersion($document); - } - - //update modification info - $document->setModificationDate(time()); - $document->setUserModification($this->getAdminUser()->getId()); - - $task = strtolower($task ?? $request->query->get('task')); - $version = null; - switch ($task) { - case $task === self::TASK_PUBLISH && $document->isAllowed($task): - $this->setValuesToDocument($request, $document); - $document->setPublished(true); - $document->save(); - - break; - case $task === self::TASK_UNPUBLISH && $document->isAllowed($task): - $this->setValuesToDocument($request, $document); - $document->setPublished(false); - $document->save(); - - break; - case in_array($task, [self::TASK_SAVE, self::TASK_VERSION, self::TASK_AUTOSAVE]) - && $document->isAllowed(self::TASK_SAVE): - if ($document instanceof Model\Document\PageSnippet) { - $this->setValuesToDocument($request, $document); - if ($task === self::TASK_AUTOSAVE || $document->isPublished()) { - $version = $document->saveVersion(true, true, null, $task === self::TASK_AUTOSAVE); - } else { - $document->save(); - } - } - - break; - case $task === self::TASK_SCHEDULER && $document->isAllowed('settings'): - if ($document instanceof Model\Document\PageSnippet - || $document instanceof Model\Document\Hardlink - || $document instanceof Model\Document\Link) { - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); - $document->saveScheduledTasks(); - } + $event = new GenericEvent($this, [ + 'data' => $data, + 'document' => $document, + ]); - break; - default: - throw $this->createAccessDeniedHttpException(); - } + OpenDxp::getEventDispatcher()->dispatch($event, AdminEvents::DOCUMENT_GET_PRE_SEND_DATA); + $data = $event->getArgument('data'); - if ($document instanceof Model\Document\PageSnippet) { - $this->handleTask($task, $document); + if ($document->isAllowed('view')) { + return $this->adminJson($data); } - return [$task, $document, $version]; - } - - protected function populateUsersNames(Document $document, array &$data): void - { - $userOwnerName = $this->getUserName($document->getUserOwner()); - $userModificationName = ($document->getUserOwner() === $document->getUserModification()) ? $userOwnerName : $this->getUserName($document->getUserModification()); - $data['userOwnerUsername'] = $userOwnerName['userName']; - $data['userOwnerFullname'] = $userOwnerName['fullName']; - $data['userModificationUsername'] = $userModificationName['userName']; - $data['userModificationFullname'] = $userModificationName['fullName']; - } - - public function getTreeNodeConfig(ElementInterface $element): array - { - return $this->elementService->getElementTreeNodeConfig($element); + throw $this->createAccessDeniedHttpException(); } } diff --git a/src/Controller/Admin/Document/DocumentCopyController.php b/src/Controller/Admin/Document/DocumentCopyController.php new file mode 100644 index 00000000..f3b6d86f --- /dev/null +++ b/src/Controller/Admin/Document/DocumentCopyController.php @@ -0,0 +1,94 @@ +value)] +class DocumentCopyController extends AdminAbstractController +{ + #[Route('/copy-info', name: 'opendxp_admin_document_document_copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + Request $request, + ): JsonResponse { + $result = $handler($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { + $session->set((string) $result->transactionId, ['idMapping' => []]); + }, 'opendxp_copy'); + + return $this->adminJson(['pastejobs' => $result->pasteJobs]); + } + + #[Route('/copy-rewrite-ids', name: 'opendxp_admin_document_document_copyrewriteids', methods: ['PUT'])] + public function copyRewriteIdsAction( + RewriteDocumentIdsPayload $payload, + RewriteDocumentIdsHandler $rewriteIds, + Request $request, + ): JsonResponse { + $rewriteIds($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($payload): void { + $session->set($payload->transactionId, $payload->updatedIdStore); + }, 'opendxp_copy'); + + return $this->adminJson(ApiResponse::ok(['id' => $payload->documentId])); + } + + #[Route('/copy', name: 'opendxp_admin_document_document_copy', methods: ['POST'])] + public function copyAction( + CopyDocumentPayload $payload, + CopyDocumentHandler $copyDocument, + Request $request, + ): JsonResponse { + $result = $copyDocument($payload); + + if ($result->newDocument !== null) { + $sessionBag = $payload->sessionBag; + $sessionBag['idMapping'][$result->sourceId] = $result->newDocument->getId(); + + if ($payload->saveParentId) { + $sessionBag['parentId'] = $result->newDocument->getId(); + } + + Session::getSessionBag($request->getSession(), 'opendxp_copy')->set($payload->transactionId, $sessionBag); + } + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/Document/DocumentVersionController.php b/src/Controller/Admin/Document/DocumentVersionController.php new file mode 100644 index 00000000..a5b8c882 --- /dev/null +++ b/src/Controller/Admin/Document/DocumentVersionController.php @@ -0,0 +1,92 @@ +value)] + #[Route('/version-to-session', name: 'opendxp_admin_document_document_versiontosession', methods: ['POST'])] + public function versionToSessionAction( + IdBodyPayload $payload, + SaveVersionToSessionHandler $saveToSession, + ): Response { + $saveToSession($payload); + + return new Response(); + } + + #[IsGranted(CorePermission::Documents->value)] + #[Route('/publish-version', name: 'opendxp_admin_document_document_publishversion', methods: ['POST'])] + public function publishVersionAction( + IdBodyPayload $payload, + PublishVersionHandler $publishVersion, + ): JsonResponse { + $result = $publishVersion($payload); + + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData])); + } + + #[IsGranted(CorePermission::Documents->value)] + #[Route('/diff-versions/from/{from}/to/{to}', name: 'opendxp_admin_document_document_diffversions', requirements: ['from' => "\d+", 'to' => "\d+"], methods: ['GET'])] + public function diffVersionsAction( + DiffVersionsPayload $payload, + DiffVersionsHandler $diffVersions, + ): Response { + $result = $diffVersions($payload); + + if (!$result->supported) { + return $this->render('@OpenDxpAdmin/admin/document/document/diff_versions_unsupported.html.twig'); + } + + return $this->render('@OpenDxpAdmin/admin/document/document/diff_versions.html.twig', [ + 'image' => $result->image, + 'image1' => $result->image1, + 'image2' => $result->image2, + ]); + } + + public function diffVersionsHtmlAction( + #[MapQueryParameter] ?string $id = null, + ): BinaryFileResponse { + $file = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . basename($id); + if (file_exists($file)) { + return new BinaryFileResponse($file); + } + + throw $this->createNotFoundException('Version diff file not found'); + } +} diff --git a/src/Controller/Admin/Document/EmailController.php b/src/Controller/Admin/Document/EmailController.php index 6fa621ae..7a0040c0 100644 --- a/src/Controller/Admin/Document/EmailController.php +++ b/src/Controller/Admin/Document/EmailController.php @@ -1,5 +1,4 @@ query->get('id')); - - if (!$email) { - throw $this->createNotFoundException('Email not found'); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - if (($lock = $this->checkForLock($email, $request->getSession()->getId())) instanceof JsonResponse) { - return $lock; - } - - $email = clone $email; - $draftVersion = null; - $email = $this->getLatestVersion($email, $draftVersion); - - $versions = Element\Service::getSafeVersionInfo($email->getVersions()); - $email->setVersions(array_splice($versions, -1, 1)); - $email->setParent(null); - - // unset useless data - $email->setEditables(null); - $email->setChildren(null); - - $data = $email->getObjectVars(); - $data['locked'] = $email->isLocked(); - - $this->addTranslationsData($email, $data); - $this->minimizeProperties($email, $data); - $this->populateUsersNames($email, $data); - - $data['url'] = $email->getUrl(); - - return $this->preSendDataActions($data, $email, $draftVersion); + return $this->preSendDataActions($result->data, $result->email); } - /** - * @throws Exception - */ #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveEmailPayload $payload, SaveEmailHandler $handler): JsonResponse { - $page = Document\Email::getById((int) $request->request->get('id')); - if (!$page) { - throw $this->createNotFoundException('Email not found'); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - [$task, $page, $version] = $this->saveDocument($page, $request); - $this->saveToSession($page, $request->getSession()); - - if ($task === self::TASK_PUBLISH || $task === self::TASK_UNPUBLISH) { - $treeData = $this->getTreeNodeConfig($page); - - return $this->adminJson([ - 'success' => true, + if ($result->task === self::TASK_PUBLISH || $result->task === self::TASK_UNPUBLISH) { + return $this->adminJson(ApiResponse::ok([ 'data' => [ - 'versionDate' => $page->getModificationDate(), - 'versionCount' => $page->getVersionCount(), + 'versionDate' => $result->email->getModificationDate(), + 'versionCount' => $result->email->getVersionCount(), ], - 'treeData' => $treeData, - ]); + 'treeData' => $result->treeData, + ])); } + $draftData = []; - if ($version) { + if ($result->version) { $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), + 'id' => $result->version->getId(), + 'modificationDate' => $result->version->getDate(), + 'isAutoSave' => $result->version->isAutoSave(), ]; } - return $this->adminJson(['success' => true, 'draft' => $draftData]); - } - - protected function setValuesToDocument(Request $request, Document $document): void - { - $this->addSettingsToDocument($request, $document); - $this->addDataToDocument($request, $document); - $this->addPropertiesToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); + return $this->adminJson(ApiResponse::ok(['draft' => $draftData])); } } diff --git a/src/Controller/Admin/Document/FolderController.php b/src/Controller/Admin/Document/FolderController.php index cb073f52..979754f2 100644 --- a/src/Controller/Admin/Document/FolderController.php +++ b/src/Controller/Admin/Document/FolderController.php @@ -1,4 +1,5 @@ query->get('id')); - if (!$folder) { - throw $this->createNotFoundException('Folder not found'); - } - - $folder = clone $folder; - $folder->setParent(null); - - $data = $folder->getObjectVars(); - $data['locked'] = $folder->isLocked(); - - $this->addTranslationsData($folder, $data); - $this->minimizeProperties($folder, $data); - $this->populateUsersNames($folder, $data); + $result = $handler($payload); - return $this->preSendDataActions($data, $folder); + return $this->preSendDataActions($result->data, $result->folder); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveFolderPayload $payload, SaveFolderHandler $handler): JsonResponse { - $folder = Document\Folder::getById((int) $request->request->get('id')); - if (!$folder) { - throw $this->createNotFoundException('Folder not found'); - } + $result = $handler($payload); - $result = $this->saveDocument($folder, $request, false, self::TASK_PUBLISH); - /** @var Document\Folder $folder */ - $folder = $result[1]; - $treeData = $this->getTreeNodeConfig($folder); - - return $this->adminJson(['success' => true, 'treeData' => $treeData]); - } - - protected function setValuesToDocument(Request $request, Document $document): void - { - $this->addPropertiesToDocument($request, $document); + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData])); } } diff --git a/src/Controller/Admin/Document/HardlinkController.php b/src/Controller/Admin/Document/HardlinkController.php index e67e03cf..bd2d3ded 100644 --- a/src/Controller/Admin/Document/HardlinkController.php +++ b/src/Controller/Admin/Document/HardlinkController.php @@ -1,4 +1,5 @@ query->get('id')); - - if (!$link) { - throw $this->createNotFoundException('Hardlink not found'); - } - - if (($lock = $this->checkForLock($link, $request->getSession()->getId())) instanceof JsonResponse) { - return $lock; - } - - $link = clone $link; - $link->setParent(null); - - $data = $link->getObjectVars(); - $data['locked'] = $link->isLocked(); - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $link->getScheduledTasks() - ); - - $this->addTranslationsData($link, $data); - $this->minimizeProperties($link, $data); - $this->populateUsersNames($link, $data); - - if ($link->getSourceDocument()) { - $data['sourcePath'] = $link->getSourceDocument()->getRealFullPath(); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - return $this->preSendDataActions($data, $link); + return $this->preSendDataActions($result->data, $result->link); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveHardlinkPayload $payload, SaveHardlinkHandler $handler): JsonResponse { - $link = Document\Hardlink::getById((int) $request->request->get('id')); - if (!$link) { - throw $this->createNotFoundException('Hardlink not found'); - } + $result = $handler($payload); - $result = $this->saveDocument($link, $request); - /** @var Document\Hardlink $link */ - $link = $result[1]; - $treeData = $this->getTreeNodeConfig($link); - - return $this->adminJson([ - 'success' => true, + return $this->adminJson(ApiResponse::ok([ 'data' => [ - 'versionDate' => $link->getModificationDate(), - 'versionCount' => $link->getVersionCount(), + 'versionDate' => $result->link->getModificationDate(), + 'versionCount' => $result->link->getVersionCount(), ], - 'treeData' => $treeData, - ]); - } - - /** - * @param Document\Hardlink $document - */ - protected function setValuesToDocument(Request $request, Document $document): void - { - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - - $sourceId = null; - if ($sourceDocument = Document::getByPath($data['sourcePath'])) { - $sourceId = $sourceDocument->getId(); - } - $document->setSourceId($sourceId); - $document->setValues($data); - } - - $this->addPropertiesToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); + 'treeData' => $result->treeData, + ])); } } diff --git a/src/Controller/Admin/Document/LinkController.php b/src/Controller/Admin/Document/LinkController.php index 1b19a20f..22326710 100644 --- a/src/Controller/Admin/Document/LinkController.php +++ b/src/Controller/Admin/Document/LinkController.php @@ -1,4 +1,5 @@ query->get('id')); - - if (!$link) { - throw $this->createNotFoundException('Link not found'); - } - - if (($lock = $this->checkForLock($link, $request->getSession()->getId())) instanceof JsonResponse) { - return $lock; + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - $link = clone $link; - - $link->setElement(null); - $link->setParent(null); - - $data = $serializer->serialize($link->getObjectVars(), 'json', []); - $data = json_decode($data, true); - $data['locked'] = $link->isLocked(); - $data['rawHref'] = $link->getRawHref(); - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $link->getScheduledTasks() - ); - - $this->addTranslationsData($link, $data); - $this->minimizeProperties($link, $data); - $this->populateUsersNames($link, $data); - - return $this->preSendDataActions($data, $link); + return $this->preSendDataActions($result->data, $result->link); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveLinkPayload $payload, SaveLinkHandler $handler): JsonResponse { - $link = Document\Link::getById((int) $request->request->get('id')); - if (!$link) { - throw $this->createNotFoundException('Link not found'); - } + $result = $handler($payload); - $result = $this->saveDocument($link, $request); - /** @var Document\Link $link */ - $link = $result[1]; - $treeData = $this->getTreeNodeConfig($link); - - return $this->adminJson([ - 'success' => true, + return $this->adminJson(ApiResponse::ok([ 'data' => [ - 'versionDate' => $link->getModificationDate(), - 'versionCount' => $link->getVersionCount(), + 'versionDate' => $result->link->getModificationDate(), + 'versionCount' => $result->link->getVersionCount(), ], - 'treeData' => $treeData, - ]); - } - - /** - * @param Document\Link $document - */ - protected function setValuesToDocument(Request $request, Document $document): void - { - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - - $path = $data['path']; - - if (!empty($path)) { - $target = null; - if ($data['linktype'] === 'internal' && $data['internalType']) { - $target = Element\Service::getElementByPath($data['internalType'], $path); - if ($target) { - $data['internal'] = $target->getId(); - } - } - - if (!$target) { - if ($target = Document::getByPath($path)) { - $data['internalType'] = 'document'; - $data['internal'] = $target->getId(); - } elseif ($target = Asset::getByPath($path)) { - $data['internalType'] = 'asset'; - $data['internal'] = $target->getId(); - } elseif ($target = Concrete::getByPath($path)) { - $data['internalType'] = 'object'; - $data['internal'] = $target->getId(); - } else { - $data['linktype'] = 'direct'; - $data['internalType'] = null; - $data['internal'] = null; - $data['direct'] = $path; - } - - if ($target) { - $data['linktype'] = 'internal'; - $data['direct'] = ''; - } - } - } else { - // clear content of link - $data['linktype'] = 'internal'; - $data['direct'] = ''; - $data['internalType'] = null; - $data['internal'] = null; - } - - unset($data['path']); - - $document->setValues($data); - } - - $this->addPropertiesToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); + 'treeData' => $result->treeData, + ])); } } diff --git a/src/Controller/Admin/Document/PageController.php b/src/Controller/Admin/Document/PageController.php index fc8a6c05..3bed21f9 100644 --- a/src/Controller/Admin/Document/PageController.php +++ b/src/Controller/Admin/Document/PageController.php @@ -1,5 +1,4 @@ query->get('id')); - - if (!$page) { - throw $this->createNotFoundException('Page not found'); - } - - if (($lock = $this->checkForLock($page, $request->getSession()->getId())) instanceof JsonResponse) { - return $lock; - } - - $page = clone $page; - $draftVersion = null; - $page = $this->getLatestVersion($page, $draftVersion); - - $pageVersions = Element\Service::getSafeVersionInfo($page->getVersions()); - $page->setVersions(array_splice($pageVersions, -1, 1)); - $page->setParent(null); - - // unset useless data - $page->setEditables(null); - $page->setChildren(null); - - $data = $page->getObjectVars(); - $data['locked'] = $page->isLocked(); - - $this->addTranslationsData($page, $data); - $this->minimizeProperties($page, $data); - $this->populateUsersNames($page, $data); - - if ($page->getContentMainDocument()) { - $data['contentMainDocumentPath'] = $page->getContentMainDocument()->getRealFullPath(); - } - - if ($page->getStaticGeneratorEnabled()) { - $data['staticLastGenerated'] = $staticPageGenerator->getLastModified($page); + public function getDataByIdAction( + GetPageDataHandler $handler, + GetPageDataPayload $payload, + ): JsonResponse { + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - $data['url'] = $page->getUrl(); - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $page->getScheduledTasks() - ); - - return $this->preSendDataActions($data, $page, $draftVersion); + return $this->preSendDataActions($result->data, $result->page); } - /** - * @throws Exception - */ #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request, StaticPageGenerator $staticPageGenerator): JsonResponse + public function saveAction(SavePagePayload $payload, StaticPageGenerator $staticPageGenerator, SavePageHandler $handler): JsonResponse { - $oldPage = Document\Page::getById((int) $request->request->get('id')); - if (!$oldPage) { - throw $this->createNotFoundException('Page not found'); - } - - /** @var Document\Page|null $pageSession */ - $pageSession = $this->getFromSession($oldPage, $request->getSession()); - - $page = $pageSession ?: $this->getLatestVersion($oldPage); - - if ($request->request->has('missingRequiredEditable')) { - $page->setMissingRequiredEditable($request->request->get('missingRequiredEditable') === 'true'); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - if ($request->request->has('settings')) { - $settings = $this->decodeJson($request->request->get('settings')); - if ($settings['published'] ?? false) { - $page->setMissingRequiredEditable(null); - } - } - - [$task, $page, $version] = $this->saveDocument($page, $request); - $arguments = [ - 'oldPage' => $oldPage, - 'task' => $task, - ]; - $documentEvent = new DocumentEvent($page, $arguments); - $this->dispatchEvent($documentEvent, DocumentEvents::PAGE_POST_SAVE_ACTION); - if ($task === self::TASK_PUBLISH || $task === self::TASK_UNPUBLISH) { - $treeData = $this->getTreeNodeConfig($page); - + if ($result->task === self::TASK_PUBLISH || $result->task === self::TASK_UNPUBLISH) { $data = [ - 'versionDate' => $page->getModificationDate(), - 'versionCount' => $page->getVersionCount(), + 'versionDate' => $result->page->getModificationDate(), + 'versionCount' => $result->page->getVersionCount(), ]; - - if ($staticGeneratorEnabled = $page->getStaticGeneratorEnabled()) { + if ($staticGeneratorEnabled = $result->page->getStaticGeneratorEnabled()) { $data['staticGeneratorEnabled'] = $staticGeneratorEnabled; - $data['staticLastGenerated'] = $staticPageGenerator->getLastModified($page); + $data['staticLastGenerated'] = $staticPageGenerator->getLastModified($result->page); } - return $this->adminJson([ - 'success' => true, - 'treeData' => $treeData, - 'data' => $data, - ]); + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData, 'data' => $data])); } - $this->saveToSession($page, $request->getSession()); + $draftData = []; - if ($version) { + if ($result->version) { $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), + 'id' => $result->version->getId(), + 'modificationDate' => $result->version->getDate(), + 'isAutoSave' => $result->version->isAutoSave(), ]; } - $treeData = $this->getTreeNodeConfig($page); - return $this->adminJson(['success' => true, 'treeData' => $treeData, 'draft' => $draftData]); + return $this->adminJson(ApiResponse::ok(['treeData' => $result->treeData, 'draft' => $draftData])); } #[Route('/generate-previews', name: 'generatepreviews', methods: ['GET'])] - public function generatePreviewsAction(Request $request, MessageBusInterface $messengerBusOpendxpCore): JsonResponse + public function generatePreviewsAction(GeneratePagePreviewsHandler $handler, EmptyPayload $payload): JsonResponse { - $list = new Document\Listing(); - $list->setCondition('`type` = ?', ['page']); + $handler($payload); - // @todo: this seems completely wrong. - foreach ($list->loadIdList() as $docId) { - $messengerBusOpendxpCore->dispatch( - new GeneratePagePreviewMessage($docId, \OpenDxp\Tool::getHostUrl()) - ); - - break; - } - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/display-preview-image', name: 'display_preview_image', methods: ['GET'])] - public function displayPreviewImageAction(Request $request): BinaryFileResponse - { - $document = Document\Page::getById((int) $request->query->get('id')); - if ($document instanceof Document\Page) { - return new BinaryFileResponse($document->getPreviewImageFilesystemPath(), 200, [ - 'Content-Type' => 'image/jpg', - ]); - } - - throw $this->createNotFoundException('Page not found'); + public function displayPreviewImageAction( + GetPagePreviewImagePathHandler $handler, + GetPagePreviewImagePathPayload $payload, + ): BinaryFileResponse { + $filePath = $handler($payload); + + return new BinaryFileResponse($filePath, 200, [ + 'Content-Type' => 'image/jpg', + ]); } #[Route('/check-pretty-url', name: 'checkprettyurl', methods: ['POST'])] - public function checkPrettyUrlAction(Request $request): JsonResponse + public function checkPrettyUrlAction(CheckPrettyUrlPayload $payload, CheckPrettyUrlHandler $handler): JsonResponse { - $docId = $request->request->getInt('id'); - $path = trim($request->request->get('path', '')); - - $success = true; - - if ($path === '') { - return $this->adminJson([ - 'success' => $success, - ]); - } + $result = $handler($payload); - $message = []; - $path = rtrim($path, '/'); - - // must start with / - if ($path !== '' && !str_starts_with($path, '/')) { - $success = false; - $message[] = 'URL must start with /.'; - } - - if (strlen($path) < 2) { - $success = false; - $message[] = 'URL must be at least 2 characters long.'; - } - - if (!Element\Service::isValidPath($path, 'document')) { - $success = false; - $message[] = 'URL is invalid.'; - } - - $list = new Document\Listing(); - $list->setCondition('(CONCAT(`path`, `key`) = ? OR id IN (SELECT id from documents_page WHERE prettyUrl = ?)) - AND id != ?', [ - $path, $path, $docId, - ]); - - if ($list->getTotalCount() > 0) { - $checkDocument = Document::getById($docId); - $checkSite = Frontend::getSiteForDocument($checkDocument); - $checkSiteId = empty($checkSite) ? 0 : $checkSite->getId(); - - foreach ($list as $document) { - if (empty($document)) { - continue; - } - - $site = Frontend::getSiteForDocument($document); - $siteId = empty($site) ? 0 : $site->getId(); - - if ($siteId === $checkSiteId) { - $success = false; - $message[] = 'URL path already exists.'; - - break; - } - } - } - - return $this->adminJson([ - 'success' => $success, - 'message' => implode('
', $message), - ]); + return $this->adminJson(ApiResponse::fromBool($result->success, ['message' => implode('
', $result->messages)])); } #[Route('/clear-editable-data', name: 'cleareditabledata', methods: ['PUT'])] - public function clearEditableDataAction(Request $request): JsonResponse + public function clearEditableDataAction(ResetEditablesSessionPayload $payload, ResetEditablesSessionHandler $handler): JsonResponse { - $docId = $request->request->getInt('id'); - $doc = Document\PageSnippet::getById($docId); - - if (!$doc) { - throw $this->createNotFoundException('Document not found'); - } - - foreach ($doc->getEditables() as $editable) { - // remove all but target group data - // Hardcoded the TARGET_GROUP_EDITABLE_PREFIX prefix here as we shouldn't remove the bundle specific editables even if bundle is not enabled/installed - if (!preg_match('/^' . preg_quote('persona_ -', '/') . '/', $editable->getName())) { - $doc->removeEditable($editable->getName()); - } - } + $handler($payload); - $this->saveToSession($doc, $request->getSession(), true); - - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ #[Route('/qr-code', name: 'qrcode', methods: ['GET'])] - public function qrCodeAction(Request $request): BinaryFileResponse - { - $page = Document\Page::getById((int) $request->query->get('id')); - - if (!$page) { - throw $this->createNotFoundException('Page not found'); - } - - $url = $page->getUrl(); - - $result = Builder::create() - ->writer(new PngWriter()) - ->data($url) - ->size($request->query->get('download') ? 4000 : 500) - ->build(); - - $tmpFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/qr-code-' . uniqid('', false) . '.png'; - $result->saveToFile($tmpFile); + public function qrCodeAction( + GenerateQrCodeHandler $handler, + GenerateQrCodePayload $payload, + ): BinaryFileResponse { + $tmpFile = $handler($payload); $response = new BinaryFileResponse($tmpFile); $response->headers->set('Content-Type', 'image/png'); - if ($request->query->get('download')) { + if ($payload->download) { $response->setContentDisposition('attachment', 'qrcode-preview.png'); } @@ -328,60 +155,28 @@ public function qrCodeAction(Request $request): BinaryFileResponse } /** - * @throws NotFoundHttpException|Exception + * @throws NotFoundHttpException */ #[Route('/areabrick-render-index-editmode', name: 'areabrick-render-index-editmode', methods: ['POST'])] public function areabrickRenderIndexEditmode( Request $request, - BlockStateStack $blockStateStack, - EditmodeEditableDefinitionCollector $definitionCollector, - Environment $twig, - EditableRenderer $editableRenderer, + RenderAreabrickIndexEditmodePayload $payload, + RenderAreabrickIndexEditmodeHandler $handler, DocumentResolver $documentResolver, - LocaleServiceInterface $localeService + Environment $twig, ): JsonResponse { - $blockStateStackData = json_decode($request->request->get('blockStateStack'), true); - $blockStateStack->loadArray($blockStateStackData); - - $document = Document\PageSnippet::getById((int) $request->request->get('documentId')); - if (!$document) { - throw $this->createNotFoundException(); - } - - $document = clone $document; - $document->setEditables([]); - $documentResolver->setDocument($request, $document); - - $twig->addGlobal('document', $document); - $twig->addGlobal('editmode', true); - // we can't use EditmodeResolver::setForceEditmode() here, because it would also render included documents in editmode - // so we use the attribute as a workaround $request->attributes->set(EditmodeResolver::ATTRIBUTE_EDITMODE, true); - // setting locale manually here before rendering, to make sure editables use the right locale from document - $localeService->setLocale($document->getProperty('language')); + $result = $handler($payload); - $areaBlockConfig = json_decode($request->request->get('areablockConfig'), true); - /** @var Document\Editable\Areablock $areablock */ - $areablock = $editableRenderer->getEditable($document, 'areablock', $request->request->get('realName'), $areaBlockConfig, true); - $areablock->setRealName($request->request->get('realName')); - $areablock->setEditmode(true); - $areaBrickData = json_decode($request->request->get('areablockData'), true); - $areablock->setDataFromEditmode($areaBrickData); - $htmlCode = trim($areablock->renderIndex((int) $request->request->get('index'), true)); + $documentResolver->setDocument($request, $result->document); + $twig->addGlobal('document', $result->document); + $twig->addGlobal('editmode', true); return new JsonResponse([ - 'editableDefinitions' => $definitionCollector->getDefinitions(), - 'htmlCode' => $htmlCode, + 'editableDefinitions' => $result->editableDefinitions, + 'htmlCode' => $result->htmlCode, ]); } - - protected function setValuesToDocument(Request $request, Document $document): void - { - $this->addSettingsToDocument($request, $document); - $this->addDataToDocument($request, $document); - $this->addPropertiesToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); - } } diff --git a/src/Controller/Admin/Document/RenderletController.php b/src/Controller/Admin/Document/RenderletController.php index 0b59a9fe..fdf80b90 100644 --- a/src/Controller/Admin/Document/RenderletController.php +++ b/src/Controller/Admin/Document/RenderletController.php @@ -18,19 +18,10 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\Document; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Document\Editable\EditableHandler; -use OpenDxp\Event\DocumentEvents; -use OpenDxp\Localization\LocaleServiceInterface; -use OpenDxp\Model\Document; -use OpenDxp\Model\Element\ElementInterface; -use OpenDxp\Model\Element\Service; -use OpenDxp\Templating\Renderer\ActionRenderer; -use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter; -use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Component\HttpFoundation\Request; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Renderlet\RenderRenderlet\RenderRenderletHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\Renderlet\RenderRenderlet\RenderRenderletPayload; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal @@ -42,78 +33,11 @@ class RenderletController extends AdminAbstractController */ #[Route('/document_tag/renderlet', name: 'opendxp_admin_document_renderlet_renderlet', methods: ['GET'])] public function renderletAction( - Request $request, - ActionRenderer $actionRenderer, - EditableHandler $editableHandler, - LocaleServiceInterface $localeService, - EventDispatcherInterface $eventDispatcher + RenderRenderletPayload $payload, + RenderRenderletHandler $handler, ): Response { + $result = $handler($payload); - $query = $request->query->all(); - $attributes = []; - - // load element to make sure the request is valid - $element = $this->loadElement($request); - - $event = new GenericEvent($this, [ - 'requestParams' => $query, - 'element' => $element, - ]); - - $eventDispatcher->dispatch($event, DocumentEvents::EDITABLE_RENDERLET_PRE_RENDER); - - $controller = $request->query->get('controller'); - - // set document if set in request - if ($documentId = $request->query->get('opendxp_parentDocument')) { - $document = Document\PageSnippet::getById((int) $documentId); - if ($document) { - $attributes = $actionRenderer->addDocumentAttributes($document, $attributes); - unset($attributes[DynamicRouter::CONTENT_TEMPLATE]); - } - } - - // override template if set - if ($template = $request->query->get('template')) { - $attributes[DynamicRouter::CONTENT_TEMPLATE] = $template; - } - - foreach (['controller', 'action', 'module', 'bundle'] as $key) { - if (isset($query[$key])) { - unset($query[$key]); - } - } - - // setting locale manually here before rendering the action to make sure editables use the right locale - if this - // is needed in multiple places, move this to the tag handler instead (see #1834) - if (isset($attributes['_locale'])) { - $localeService->setLocale($attributes['_locale']); - } - - $result = $editableHandler->renderAction($controller, $attributes, $query); - - return new Response($result); - } - - private function loadElement(Request $request): ElementInterface - { - $element = null; - - $id = $request->query->get('id'); - $type = $request->query->get('type'); - - if ($id && $type) { - $element = Service::getElementById($type, (int)$id); - } - - if (!$element instanceof ElementInterface) { - throw $this->createNotFoundException(sprintf('Element with type %s and ID %d was not found', $type ?: 'null', $id ?: 'null')); - } - - if (!$element->isAllowed('view')) { - throw $this->createAccessDeniedException(sprintf('Access to element with type %s and ID %d is not allowed', $type, $id)); - } - - return $element; + return new Response($result->html); } -} +} \ No newline at end of file diff --git a/src/Controller/Admin/Document/SnippetController.php b/src/Controller/Admin/Document/SnippetController.php index a680ffbc..7cbcc4ad 100644 --- a/src/Controller/Admin/Document/SnippetController.php +++ b/src/Controller/Admin/Document/SnippetController.php @@ -1,4 +1,5 @@ query->get('id')); - - if (!$snippet) { - throw $this->createNotFoundException('Snippet not found'); - } - - if (($lock = $this->checkForLock($snippet, $request->getSession()->getId())) instanceof JsonResponse) { - return $lock; - } - - $snippet = clone $snippet; - $draftVersion = null; - $snippet = $this->getLatestVersion($snippet, $draftVersion); - - $versions = Element\Service::getSafeVersionInfo($snippet->getVersions()); - $snippet->setVersions(array_splice($versions, -1, 1)); - $snippet->setParent(null); - - // unset useless data - $snippet->setEditables(null); - - $data = $snippet->getObjectVars(); - $data['locked'] = $snippet->isLocked(); - - $this->addTranslationsData($snippet, $data); - $this->minimizeProperties($snippet, $data); - $this->populateUsersNames($snippet, $data); - - $data['url'] = $snippet->getUrl(); - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $snippet->getScheduledTasks() - ); - - if ($snippet->getContentMainDocument()) { - $data['contentMainDocumentPath'] = $snippet->getContentMainDocument()->getRealFullPath(); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - - return $this->preSendDataActions($data, $snippet, $draftVersion); + return $this->preSendDataActions($result->data, $result->snippet); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): JsonResponse + public function saveAction(SaveSnippetPayload $payload, SaveSnippetHandler $handler): JsonResponse { - $snippet = Document\Snippet::getById((int) $request->request->get('id')); - if (!$snippet) { - throw $this->createNotFoundException('Snippet not found'); - } - - /** @var Document\Snippet|null $snippetSession */ - $snippetSession = $this->getFromSession($snippet, $request->getSession()); - - $snippet = $snippetSession ?: $this->getLatestVersion($snippet); - - if ($request->request->has('missingRequiredEditable')) { - $snippet->setMissingRequiredEditable($request->request->get('missingRequiredEditable') === 'true'); + try { + $result = $handler($payload); + } catch (ElementLockedException $e) { + return $this->getEditLockResponse($e->getElementId(), $e->getElementType()); } - [$task, $snippet, $version] = $this->saveDocument($snippet, $request); - - if ($task === self::TASK_PUBLISH || $task === self::TASK_UNPUBLISH) { - $this->saveToSession($snippet, $request->getSession()); - - $treeData = $this->getTreeNodeConfig($snippet); - - return $this->adminJson([ - 'success' => true, + if ($result->task === self::TASK_PUBLISH || $result->task === self::TASK_UNPUBLISH) { + return $this->adminJson(ApiResponse::ok([ 'data' => [ - 'versionDate' => $snippet->getModificationDate(), - 'versionCount' => $snippet->getVersionCount(), + 'versionDate' => $result->snippet->getModificationDate(), + 'versionCount' => $result->snippet->getVersionCount(), ], - 'treeData' => $treeData, - ]); + 'treeData' => $result->treeData, + ])); } - $this->saveToSession($snippet, $request->getSession()); - $draftData = []; - if ($version) { + if ($result->version) { $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), + 'id' => $result->version->getId(), + 'modificationDate' => $result->version->getDate(), + 'isAutoSave' => $result->version->isAutoSave(), ]; } - return $this->adminJson(['success' => true, 'draft' => $draftData]); - } - - protected function setValuesToDocument(Request $request, Document $document): void - { - $this->addSettingsToDocument($request, $document); - $this->addDataToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); - $this->addPropertiesToDocument($request, $document); + return $this->adminJson(ApiResponse::ok(['draft' => $draftData])); } } diff --git a/src/Controller/Admin/ElementController.php b/src/Controller/Admin/ElementController.php index ab67ef2a..d517ca8d 100644 --- a/src/Controller/Admin/ElementController.php +++ b/src/Controller/Admin/ElementController.php @@ -16,856 +16,326 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Exception; -use OpenDxp; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\DependencyInjection\OpenDxpAdminExtension; -use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; -use OpenDxp\Db; -use OpenDxp\Event\Model\ResolveElementEvent; -use OpenDxp\Logger; -use OpenDxp\Model; -use OpenDxp\Model\Asset; -use OpenDxp\Model\DataObject; -use OpenDxp\Model\Document; -use OpenDxp\Model\Element; -use OpenDxp\Model\Version; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\Element\AnalyzePermissions\AnalyzePermissionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\AnalyzePermissions\AnalyzePermissionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\DeleteAllVersions\DeleteAllVersionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\DeleteAllVersions\DeleteAllVersionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\DeleteNote\DeleteNoteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\DeleteVersion\DeleteVersionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\DeleteDraft\DeleteDraftHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\FindUsages\FindUsagesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\FindUsages\FindUsagesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetNicePath\GetNicePathHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetNicePath\GetNicePathPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetNoteList\GetNoteListHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetNoteTypes\GetNoteTypesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetNoteTypes\GetNoteTypesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetPredefinedProperties\GetPredefinedPropertiesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetPredefinedProperties\GetPredefinedPropertiesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetReplaceAssignmentsBatchJobs\GetReplaceAssignmentsBatchJobsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetReplaceAssignmentsBatchJobs\GetReplaceAssignmentsBatchJobsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetRequiredByDependencies\GetRequiredByDependenciesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetRequiresDependencies\GetRequiresDependenciesHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetSubtype\GetSubtypeHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetSubtype\GetSubtypePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetVersions\GetVersionsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetVersions\GetVersionsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\AddNote\AddNoteHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\AddNote\AddNotePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\LockElement\LockElementHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\LockElement\LockElementPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\ReplaceAssignments\ReplaceAssignmentsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\ReplaceAssignments\ReplaceAssignmentsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\TypePath\TypePathHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\TypePath\TypePathPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockElements\UnlockElementsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockElements\UnlockElementsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockPropagate\UnlockPropagateHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockPropagate\UnlockPropagatePayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockElement\UnlockElementHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\UnlockElement\UnlockElementPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\VersionUpdate\VersionUpdateHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\VersionUpdate\VersionUpdatePayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdBodyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDependenciesPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\NoteListPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ class ElementController extends AdminAbstractController { - #[Route('/element/lock-element', name: 'opendxp_admin_element_lockelement', methods: ['PUT'])] - public function lockElementAction(Request $request): Response - { - Element\Editlock::lock($request->request->getInt('id'), $request->request->get('type'), $request->getSession()->getId()); - - return $this->adminJson(['success' => true]); + #[Route('/lock-element', name: 'opendxp_admin_element_lockelement', methods: ['PUT'])] + public function lockElementAction( + LockElementHandler $lockElement, + LockElementPayload $payload, + ): Response { + $lockElement($payload); + + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/unlock-element', name: 'opendxp_admin_element_unlockelement', methods: ['PUT'])] - public function unlockElementAction(Request $request): Response - { - Element\Editlock::unlock((int)$request->request->get('id'), $request->request->get('type')); + #[Route('/unlock-element', name: 'opendxp_admin_element_unlockelement', methods: ['PUT'])] + public function unlockElementAction( + UnlockElementHandler $unlockElement, + UnlockElementPayload $payload, + ): Response { + $unlockElement($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/unlock-elements', name: 'opendxp_admin_element_unlockelements', methods: ['POST'])] - public function unlockElementsAction(Request $request): Response - { - $request = json_decode($request->getContent(), true) ?? []; - foreach ($request['elements'] as $elementIdentifierData) { - Element\Editlock::unlock((int)$elementIdentifierData['id'], $elementIdentifierData['type']); - } + #[Route('/unlock-elements', name: 'opendxp_admin_element_unlockelements', methods: ['POST'])] + public function unlockElementsAction( + UnlockElementsHandler $unlockElements, + UnlockElementsPayload $payload, + ): Response { + $unlockElements($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * Returns the element data denoted by the given type and ID or path. - */ - #[Route('/element/get-subtype', name: 'opendxp_admin_element_getsubtype', methods: ['GET'])] - public function getSubtypeAction(Request $request): JsonResponse - { - $idOrPath = trim($request->query->get('id', '')); - $type = $request->query->get('type'); - - $event = new ResolveElementEvent($type, $idOrPath); - OpenDxp::getEventDispatcher()->dispatch($event, AdminEvents::RESOLVE_ELEMENT); - $idOrPath = $event->getId(); - $type = $event->getType(); - - if (is_numeric($idOrPath)) { - $el = Element\Service::getElementById($type, (int) $idOrPath); - } elseif ($type === 'document') { - $el = Document\Service::getByUrl($idOrPath); - } else { - $el = Element\Service::getElementByPath($type, $idOrPath); - } - - if ($el) { - $subtype = null; - if ($el instanceof Asset || $el instanceof Document) { - $subtype = $el->getType(); - } elseif ($el instanceof DataObject\Concrete) { - $subtype = $el->getClassName(); - } elseif ($el instanceof DataObject\Folder) { - $subtype = 'folder'; - } - - return $this->adminJson([ - 'subtype' => $subtype, - 'id' => $el->getId(), - 'type' => $type, - 'success' => true, - ]); - } - - return $this->adminJson([ - 'success' => false, - ]); + #[Route('/get-subtype', name: 'opendxp_admin_element_getsubtype', methods: ['GET'])] + public function getSubtypeAction( + GetSubtypeHandler $getSubtype, + GetSubtypePayload $payload, + ): JsonResponse { + $result = $getSubtype($payload); + + return $this->adminJson(ApiResponse::ok([ + 'subtype' => $result->subtype, + 'id' => $result->id, + 'type' => $result->type, + ])); } - protected function processNoteTypesFromParameters(string $parameterName): JsonResponse - { - $config = $this->getParameter($parameterName); - $result = []; - foreach ($config as $configEntry) { - $result[] = [ - 'name' => $configEntry, - ]; - } + #[Route('/note-types', name: 'opendxp_admin_element_notetypes', methods: ['GET'])] + public function noteTypesAction( + GetNoteTypesPayload $payload, + GetNoteTypesHandler $getNoteTypes, + ): JsonResponse { + $result = $getNoteTypes($payload); - return $this->adminJson(['noteTypes' => $result]); + return $this->adminJson(ApiResponse::ok(['noteTypes' => $result->noteTypes])); } - #[Route('/element/note-types', name: 'opendxp_admin_element_notetypes', methods: ['GET'])] - public function noteTypes(Request $request): JsonResponse - { - return match ($request->query->get('ctype')) { - 'document' => $this->processNoteTypesFromParameters(OpenDxpAdminExtension::PARAM_DOCUMENTS_NOTES_EVENTS_TYPES), - 'asset' => $this->processNoteTypesFromParameters(OpenDxpAdminExtension::PARAM_ASSETS_NOTES_EVENTS_TYPES), - 'object' => $this->processNoteTypesFromParameters(OpenDxpAdminExtension::PARAM_DATAOBJECTS_NOTES_EVENTS_TYPES), - default => $this->adminJson(['noteTypes' => []]), - }; + #[Route('/note-list', name: 'opendxp_admin_element_notelist', methods: ['POST'])] + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteListAction( + Request $request, + NoteListPayload $payload, + GetNoteListHandler $getNoteList, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::noteListDestroyAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } + + $result = $getNoteList($payload); + + return $this->adminJson(ApiResponse::ok([ + 'data' => $result->data, + 'total' => $result->total, + ])); } - #[Route('/element/note-list', name: 'opendxp_admin_element_notelist', methods: ['POST'])] - public function noteListAction(Request $request): JsonResponse - { - $this->checkPermission('notes_events'); - - if ($request->query->get('xaction') === 'destroy') { - $data = $this->decodeJson($request->request->get('data')); - $success = false; - if (($note = Element\Note::getById($data['id'])) && !$note->getLocked()) { - $note->delete(); - $success = true; - } - - return $this->adminJson(['success' => $success]); - } - - $list = new Element\Note\Listing(); - - $offset = (int) $request->request->get('start', 0); - $limit = $request->request->get('limit'); - $limit = $limit ? (int) $limit : null; - - $list->setLimit($limit); - $list->setOffset($offset); - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->request->all()); - if ($sortingSettings['orderKey'] && $sortingSettings['order']) { - $list->setOrderKey($sortingSettings['orderKey']); - $list->setOrder($sortingSettings['order']); - } else { - $list->setOrderKey(['date', 'id']); - $list->setOrder(['DESC', 'DESC']); - } - - $conditions = []; - $filterText = $request->request->get('filterText'); - - if ($filterText) { - $conditions[] = '(' - . '`title` LIKE ' . $list->quote('%'. $filterText .'%') - . ' OR `description` LIKE ' . $list->quote('%'.$filterText.'%') - . ' OR `type` LIKE ' . $list->quote('%'.$filterText.'%') - . ' OR `user` IN (SELECT `id` FROM `users` WHERE `name` LIKE ' . $list->quote('%'.$filterText.'%') . ')' - . " OR DATE_FORMAT(FROM_UNIXTIME(`date`), '%Y-%m-%d') LIKE " . $list->quote('%'.$filterText.'%') - . ')'; - } - - $filterJson = $request->request->get('filter'); - if ($filterJson) { - $db = Db::get(); - $filters = $this->decodeJson($filterJson); - $propertyKey = 'property'; - $comparisonKey = 'operator'; - - foreach ($filters as $filter) { - $operator = '='; - - if ($filter['type'] === 'string') { - $operator = 'LIKE'; - } elseif ($filter['type'] === 'numeric') { - if ($filter[$comparisonKey] === 'lt') { - $operator = '<'; - } elseif ($filter[$comparisonKey] === 'gt') { - $operator = '>'; - } elseif ($filter[$comparisonKey] === 'eq') { - $operator = '='; - } - } elseif ($filter['type'] === 'date') { - if ($filter[$comparisonKey] === 'lt') { - $operator = '<'; - } elseif ($filter[$comparisonKey] === 'gt') { - $operator = '>'; - } elseif ($filter[$comparisonKey] === 'eq') { - $operator = '='; - } - $filter['value'] = strtotime($filter['value']); - } elseif ($filter[$comparisonKey] === 'list') { - $operator = '='; - } elseif ($filter[$comparisonKey] === 'boolean') { - $operator = '='; - $filter['value'] = (int) $filter['value']; - } - // system field - $value = ($filter['value']??''); - if ($operator === 'LIKE') { - $value = '%' . $value . '%'; - } - - if ($filter[$propertyKey] === 'user') { - $conditions[] = '`user` IN (SELECT `id` FROM `users` WHERE `name` LIKE ' . $list->quote($value) . ')'; - } elseif ($filter['type'] === 'date' && $filter[$comparisonKey] === 'eq') { - $maxTime = $value + (86400 - 1); - //specifies the top point of the range used in the condition - $dateCondition = '`' . $filter[$propertyKey] . '` ' . ' BETWEEN ' . $db->quote($value) . ' AND ' . $db->quote($maxTime); - $conditions[] = $dateCondition; - } else { - $conditions[] = $db->quoteIdentifier($filter[$propertyKey]).' '.$operator.' '.$db->quote($value); - } - } - } - - if ($request->request->has('cid') && $request->request->has('ctype')) { - $conditions[] = '(cid = ' . $list->quote($request->request->get('cid')) . ' AND ctype = ' . $list->quote($request->request->get('ctype')) . ')'; - } - - if ($conditions !== []) { - $condition = implode(' AND ', $conditions); - $list->setCondition($condition); - } - - $list->load(); + #[Route('/note-list-destroy', name: 'opendxp_admin_element_notelist_destroy', methods: ['POST'])] + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteListDestroyAction( + NoteListPayload $payload, + DeleteNoteHandler $deleteNote, + ): JsonResponse { + $deleteNote($payload); - $notes = []; - - foreach ($list->getNotes() as $note) { - $e = Element\Service::getNoteData($note); - $notes[] = $e; - } - - return $this->adminJson([ - 'data' => $notes, - 'success' => true, - 'total' => $list->getTotalCount(), - ]); + return $this->adminJson(ApiResponse::ok(['data' => []])); } - #[Route('/element/note-add', name: 'opendxp_admin_element_noteadd', methods: ['POST'])] - public function noteAddAction(Request $request): JsonResponse - { - $this->checkPermission('notes_events'); - - $note = new Element\Note(); - $note->setCid((int) $request->request->get('cid')); - $note->setCtype($request->request->get('ctype')); - $note->setDate(time()); - $note->setTitle($request->request->get('title')); - $note->setDescription($request->request->get('description')); - $note->setType($request->request->get('type')); - $note->setLocked(false); - $note->save(); - - return $this->adminJson([ - 'success' => true, - ]); - } - - #[Route('/element/find-usages', name: 'opendxp_admin_element_findusages', methods: ['GET'])] - public function findUsagesAction(Request $request): JsonResponse - { - $element = null; - if ($request->query->get('id')) { - $element = Element\Service::getElementById($request->query->get('type'), $request->query->getInt('id')); - } elseif ($request->query->get('path')) { - $element = Element\Service::getElementByPath($request->query->get('type'), $request->query->get('path')); - } - - $results = []; - $success = false; - $hasHidden = false; - $total = 0; - $limit = (int)$request->query->get('limit', '50'); - $offset = (int)$request->query->get('start', '0'); - - if ($element instanceof Element\ElementInterface) { - $total = $element->getDependencies()->getRequiredByTotalCount(); - - if ($request->query->has('sort')) { - $sort = json_decode($request->query->get('sort'))[0]; - $orderBy = $sort->property; - $orderDirection = $sort->direction; - } else { - $orderBy = null; - $orderDirection = null; - } - - $queryOffset = $offset; - $queryLimit = $limit; - - while (count($results) < min($limit, $total) && $queryOffset < $total) { - $elements = $element->getDependencies() - ->getRequiredByWithPath($queryOffset, $queryLimit, $orderBy, $orderDirection); - - foreach ($elements as $el) { - $item = Element\Service::getElementById($el['type'], (int) $el['id']); - - if ($item instanceof Element\ElementInterface) { - if ($item->isAllowed('list')) { - $results[] = $el; - } else { - $hasHidden = true; - } - } - } - - $queryOffset += count($elements); - $queryLimit = $limit - count($results); - } - - $success = true; - } + #[Route('/note-add', name: 'opendxp_admin_element_noteadd', methods: ['POST'])] + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteAddAction( + AddNoteHandler $addNote, + AddNotePayload $payload, + ): JsonResponse { + $addNote($payload); - return $this->adminJson([ - 'data' => $results, - 'total' => $total, - 'hasHidden' => $hasHidden, - 'success' => $success, - ]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/get-replace-assignments-batch-jobs', name: 'opendxp_admin_element_getreplaceassignmentsbatchjobs', methods: ['GET'])] - public function getReplaceAssignmentsBatchJobsAction(Request $request): JsonResponse - { - $element = null; - - if ($request->query->get('id')) { - $element = Element\Service::getElementById($request->query->get('type'), $request->query->getInt('id')); - } elseif ($request->query->get('path')) { - $element = Element\Service::getElementByPath($request->query->get('type'), $request->query->get('path')); - } - - if ($element instanceof Element\ElementInterface) { - return $this->adminJson([ - 'success' => true, - 'jobs' => $element->getDependencies()->getRequiredBy(), - ]); - } - - return $this->adminJson(['success' => false], Response::HTTP_NOT_FOUND); + #[Route('/find-usages', name: 'opendxp_admin_element_findusages', methods: ['GET'])] + public function findUsagesAction( + FindUsagesHandler $findUsages, + FindUsagesPayload $payload, + ): JsonResponse { + $result = $findUsages($payload); + + return $this->adminJson(ApiResponse::ok([ + 'data' => $result->data, + 'total' => $result->total, + 'hasHidden' => $result->hasHidden, + ])); } - #[Route('/element/replace-assignments', name: 'opendxp_admin_element_replaceassignments', methods: ['POST'])] - public function replaceAssignmentsAction(Request $request): JsonResponse - { - $success = false; - $message = ''; - $element = Element\Service::getElementById($request->request->get('type'), $request->request->getInt('id')); - $sourceEl = Element\Service::getElementById($request->request->get('sourceType'), $request->request->getInt('sourceId')); - $targetEl = Element\Service::getElementById($request->request->get('targetType'), $request->request->getInt('targetId')); - - if ($element && $sourceEl && $targetEl - && $request->request->get('sourceType') === $request->request->get('targetType') - && $sourceEl->getType() === $targetEl->getType() - && $element->isAllowed('save') - ) { - $rewriteConfig = [ - $request->request->get('sourceType') => [ - $sourceEl->getId() => $targetEl->getId(), - ], - ]; - - if ($element instanceof Document) { - $element = Document\Service::rewriteIds($element, $rewriteConfig); - } elseif ($element instanceof DataObject\AbstractObject) { - $element = DataObject\Service::rewriteIds($element, $rewriteConfig); - } elseif ($element instanceof Asset) { - $element = Asset\Service::rewriteIds($element, $rewriteConfig); - } - - $element->setUserModification($this->getAdminUser()->getId()); - $element->save(); - - $success = true; - } else { - $message = 'source-type and target-type do not match'; - } + #[Route('/get-replace-assignments-batch-jobs', name: 'opendxp_admin_element_getreplaceassignmentsbatchjobs', methods: ['GET'])] + public function getReplaceAssignmentsBatchJobsAction( + GetReplaceAssignmentsBatchJobsHandler $getReplaceAssignmentsBatchJobs, + GetReplaceAssignmentsBatchJobsPayload $payload, + ): JsonResponse { + $jobs = $getReplaceAssignmentsBatchJobs($payload); - return $this->adminJson([ - 'success' => $success, - 'message' => $message, - ]); + return $this->adminJson(ApiResponse::ok(['jobs' => $jobs->jobs])); } - #[Route('/element/unlock-propagate', name: 'opendxp_admin_element_unlockpropagate', methods: ['PUT'])] - public function unlockPropagateAction(Request $request): JsonResponse - { - $success = false; - - $element = Element\Service::getElementById($request->request->get('type'), $request->request->getInt('id')); - if ($element) { - $element->unlockPropagate(); - $success = true; - } + #[Route('/replace-assignments', name: 'opendxp_admin_element_replaceassignments', methods: ['POST'])] + public function replaceAssignmentsAction( + ReplaceAssignmentsHandler $replaceAssignments, + ReplaceAssignmentsPayload $payload, + ): JsonResponse { + $replaceAssignments($payload); - return $this->adminJson([ - 'success' => $success, - ]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/type-path', name: 'opendxp_admin_element_typepath', methods: ['GET'])] - public function typePathAction(Request $request): JsonResponse - { - $id = $request->query->getInt('id'); - $type = $request->query->get('type'); - $data = []; - - if ($type === 'asset') { - $element = Asset::getById($id); - } elseif ($type === 'document') { - $element = Document::getById($id); - } else { - $element = DataObject::getById($id); - } - - if (!$element) { - $data['success'] = false; - - return $this->adminJson($data); - } - - $typePath = Element\Service::getTypePath($element); - - $data['success'] = true; - $data['index'] = method_exists($element, 'getIndex') ? (int) $element->getIndex() : 0; - $data['idPath'] = Element\Service::getIdPath($element); - $data['typePath'] = $typePath; - $data['fullpath'] = $element->getRealFullPath(); - - if ($type !== 'asset') { - $sortIndexPath = Element\Service::getSortIndexPath($element); - $data['sortIndexPath'] = $sortIndexPath; - } + #[Route('/unlock-propagate', name: 'opendxp_admin_element_unlockpropagate', methods: ['PUT'])] + public function unlockPropagateAction( + UnlockPropagateHandler $unlockPropagate, + UnlockPropagatePayload $payload, + ): JsonResponse { + $result = $unlockPropagate($payload); - return $this->adminJson($data); + return $this->adminJson(ApiResponse::fromBool($result->success)); } - #[Route('/element/version-update', name: 'opendxp_admin_element_versionupdate', methods: ['PUT'])] - public function versionUpdateAction(Request $request): JsonResponse - { - $data = $this->decodeJson($request->request->get('data')); + #[Route('/type-path', name: 'opendxp_admin_element_typepath', methods: ['GET'])] + public function typePathAction( + TypePathHandler $typePath, + TypePathPayload $payload, + ): JsonResponse { + $result = $typePath($payload); - $version = Version::getById($data['id']); + $data = [ + 'index' => $result->index, + 'idPath' => $result->idPath, + 'typePath' => $result->typePath, + 'fullpath' => $result->fullpath, + ]; - if ($data['public'] != $version->getPublic() || $data['note'] != $version->getNote()) { - $version->setPublic($data['public']); - $version->setNote($data['note']); - $version->save(); + if ($result->sortIndexPath !== null) { + $data['sortIndexPath'] = $result->sortIndexPath; } - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok($data)); } - /** - * @throws Exception - */ - #[Route('/element/get-nice-path', name: 'opendxp_admin_element_getnicepath', methods: ['POST'])] - public function getNicePathAction(Request $request): JsonResponse - { - $source = $this->decodeJson($request->request->get('source')); + #[Route('/version-update', name: 'opendxp_admin_element_versionupdate', methods: ['PUT'])] + public function versionUpdateAction( + VersionUpdateHandler $versionUpdate, + VersionUpdatePayload $payload, + ): JsonResponse { + $versionUpdate($payload); - if ($source['type'] !== 'object') { - throw new Exception('currently only objects as source elements are supported'); - } - - $result = []; - $id = $source['id']; - $source = DataObject\Concrete::getById($id); - $context = $request->request->has('context') ? $this->decodeJson($request->request->get('context')) : []; - - $ownerType = $context['containerType']; - $fieldname = $context['fieldname']; - - $fd = $this->getNicePathFormatterFieldDefinition($source, $context); - - $targets = $this->decodeJson($request->request->get('targets')); - - $result = $this->convertResultWithPathFormatter($source, $context, $result, $targets); - - if ($request->request->getBoolean('loadEditModeData')) { - $idProperty = $request->request->get('idProperty', 'id'); - $methodName = 'get' . ucfirst($fieldname); - if ($ownerType === 'object' && method_exists($source, $methodName)) { - $data = DataObject\Service::useInheritedValues(true, [$source, $methodName]); - $editModeData = $fd->getDataForEditmode($data, $source); - // Inherited values show as an empty array - if (is_array($editModeData) && $editModeData !== []) { - foreach ($editModeData as $relationObjectAttribute) { - - $relationObjectAttribute['$$nicepath'] = isset($relationObjectAttribute[$idProperty], $result[$relationObjectAttribute[$idProperty]]) - ? $result[$relationObjectAttribute[$idProperty]] - : null; - - $result[$relationObjectAttribute[$idProperty]] = $relationObjectAttribute; - } - } else { - foreach ($result as $resultItemId => $resultItem) { - $result[$resultItemId] = ['$$nicepath' => $resultItem]; - } - } - } else { - Logger::error('Loading edit mode data is not supported for ownertype: ' . $ownerType); - } - } - - return $this->adminJson(['success' => true, 'data' => $result]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ - #[Route('/element/get-versions', name: 'opendxp_admin_element_getversions', methods: ['GET'])] - public function getVersionsAction(Request $request): JsonResponse - { - $id = (int)$request->query->get('id'); - $type = $request->query->get('elementType'); - $allowedTypes = ['asset', 'document', 'object']; - - if ($id && in_array($type, $allowedTypes)) { - $element = Model\Element\Service::getElementById($type, $id); - if ($element) { - if ($element->isAllowed('versions')) { - $schedule = $element->getScheduledTasks(); - $schedules = []; - foreach ($schedule as $task) { - if ($task->getActive()) { - $schedules[$task->getVersion()] = $task->getDate(); - } - } - - //only load auto-save versions from current user - $list = new Version\Listing(); - $list->setLoadAutoSave(true); - $list->setCondition('cid = ? AND ctype = ? AND (autoSave=0 OR (autoSave=1 AND userId = ?)) ', [ - $element->getId(), - Element\Service::getElementType($element), - $this->getAdminUser()->getId(), - ]) - ->setOrderKey('date') - ->setOrder('ASC'); - - $versions = $list->load(); - - $versions = Model\Element\Service::getSafeVersionInfo($versions); - $versions = array_reverse($versions); //reverse array to sort by ID DESC - foreach ($versions as &$version) { - $version['scheduled'] = null; - if (array_key_exists($version['id'], $schedules)) { - $version['scheduled'] = $schedules[$version['id']]; - } - } - - return $this->adminJson(['versions' => $versions]); - } - - throw $this->createAccessDeniedException('Permission denied, ' . $type . ' id [' . $id . ']'); - } - - throw $this->createNotFoundException($type . ' with id [' . $id . "] doesn't exist"); - } + #[Route('/get-nice-path', name: 'opendxp_admin_element_getnicepath', methods: ['POST'])] + public function getNicePathAction( + GetNicePathHandler $getNicePath, + GetNicePathPayload $payload, + ): JsonResponse { + $result = $getNicePath($payload); - throw $this->createNotFoundException('Element type not found'); + return $this->adminJson(ApiResponse::ok(['data' => $result->data])); } - #[Route('/element/delete-draft', name: 'opendxp_admin_element_deletedraft', methods: ['DELETE'])] - public function deleteDraftAction(Request $request): JsonResponse - { - $version = Version::getById((int) $request->request->get('id')); - - if ($version) { - $version->delete(); - } + #[Route('/get-versions', name: 'opendxp_admin_element_getversions', methods: ['GET'])] + public function getVersionsAction( + GetVersionsHandler $getVersions, + GetVersionsPayload $payload, + ): JsonResponse { + $result = $getVersions($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(['versions' => $result->versions]); } - #[Route('/element/delete-version', name: 'opendxp_admin_element_deleteversion', methods: ['DELETE'])] - public function deleteVersionAction(Request $request): JsonResponse - { - $version = Model\Version::getById((int) $request->request->get('id')); - $version->delete(); + #[Route('/delete-draft', name: 'opendxp_admin_element_deletedraft', methods: ['DELETE'])] + public function deleteDraftAction( + DeleteDraftHandler $deleteDraft, + IdBodyPayload $payload, + ): JsonResponse { + $deleteDraft($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/delete-all-versions', name: 'opendxp_admin_element_deleteallversion', methods: ['DELETE'])] - public function deleteAllVersionAction(Request $request): JsonResponse - { - $elementId = $request->request->getInt('id'); - $elementModificationdate = $request->request->get('date'); - $elementType = $request->request->get('type'); - - $versions = new Model\Version\Listing(); - $versions->setCondition('cid = ' . $versions->quote($elementId) . - ' AND date <> ' . $versions->quote($elementModificationdate) . - ' AND ctype = ' . $versions->quote($elementType) - ); - foreach ($versions->load() as $version) { - $version->delete(); - } + #[Route('/delete-version', name: 'opendxp_admin_element_deleteversion', methods: ['DELETE'])] + public function deleteVersionAction( + DeleteVersionHandler $deleteVersion, + IdBodyPayload $payload, + ): JsonResponse { + $deleteVersion($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/get-requires-dependencies', name: 'opendxp_admin_element_getrequiresdependencies', methods: ['GET'])] - public function getRequiresDependenciesAction(Request $request): JsonResponse - { - $id = $request->query->getInt('id'); - $type = $request->query->get('elementType'); - $allowedTypes = ['asset', 'document', 'object']; - $offset = (int) $request->query->get('start', '0'); - $limit = (int) $request->query->get('limit', '25'); - $filterRequires = $request->query->get('filter'); - $value = null; - $elements = null; - - if ($id && in_array($type, $allowedTypes)) { - $element = Model\Element\Service::getElementById($type, $id); - $dependencies = $element->getDependencies(); - - if ($filterRequires) { - $filters = $this->decodeJson($filterRequires); - - foreach ($filters as $filter) { - - if ($filter['type'] === 'string') { - $value = ($filter['value']??''); - } + #[Route('/delete-all-versions', name: 'opendxp_admin_element_deleteallversion', methods: ['DELETE'])] + public function deleteAllVersionAction( + DeleteAllVersionsHandler $deleteAllVersions, + DeleteAllVersionsPayload $payload, + ): JsonResponse { + $deleteAllVersions($payload); - $elements = $element->getDependencies()->getFilterRequiresByPath($offset, $limit, $value); - - } - - if (count($elements) > 0) { - $result = Model\Element\Service::getFilterRequiresForFrontend($elements); - $result['total'] = count($result['requires']); - - return $this->adminJson($result); - } - - return $this->adminJson($elements); - - } - - if ($element instanceof Model\Element\ElementInterface) { - $dependenciesResult = Model\Element\Service::getRequiresDependenciesForFrontend($dependencies, $offset, $limit); - - $dependenciesResult['start'] = $offset; - $dependenciesResult['limit'] = $limit; - $dependenciesResult['total'] = $dependencies->getRequiresTotalCount(); - - return $this->adminJson($dependenciesResult); - } - } - - return $this->adminJson(false); + return $this->adminJson(ApiResponse::ok()); } - #[Route('/element/get-required-by-dependencies', name: 'opendxp_admin_element_getrequiredbydependencies', methods: ['GET'])] - public function getRequiredByDependenciesAction(Request $request): JsonResponse - { - $id = $request->query->getInt('id'); - $type = $request->query->get('elementType'); - $allowedTypes = ['asset', 'document', 'object']; - $offset = (int) $request->query->get('start', '0'); - $limit = (int) $request->query->get('limit', '25'); - $filterRequiredBy = $request->query->get('filter'); - $value = null; - $elements = null; - - if ($id && in_array($type, $allowedTypes)) { - $element = Model\Element\Service::getElementById($type, $id); - $dependencies = $element->getDependencies(); - - if ($filterRequiredBy) { - $filters = $this->decodeJson($filterRequiredBy); + #[Route('/get-requires-dependencies', name: 'opendxp_admin_element_getrequiresdependencies', methods: ['GET'])] + public function getRequiresDependenciesAction( + GetRequiresDependenciesHandler $getRequiresDependencies, + GetDependenciesPayload $payload, + ): JsonResponse { + $result = $getRequiresDependencies($payload); - foreach ($filters as $filter) { - - if ($filter['type'] === 'string') { - $value = ($filter['value']??''); - } - - $elements = $element->getDependencies()->getFilterRequiredByPath($offset, $limit, $value); - - } - - $result = [ - 'start' => $offset, - 'limit' => $limit, - 'requiredBy' => [], // Initialize 'requiredBy' as an empty array - ]; - - if (count($elements) > 0) { - $result = Model\Element\Service::getFilterRequiredByPathForFrontend($elements); - $result['total'] = count($result['requiredBy']); - - return $this->adminJson($result); - } - - return $this->adminJson($elements); - - } - - if ($element instanceof Model\Element\ElementInterface) { - $dependenciesResult = Model\Element\Service::getRequiredByDependenciesForFrontend($dependencies, $offset, $limit); - - $dependenciesResult['start'] = $offset; - $dependenciesResult['limit'] = $limit; - $dependenciesResult['total'] = $dependencies->getRequiredByTotalCount(); - - return $this->adminJson($dependenciesResult); - } - } - - return $this->adminJson(false); + return $this->adminJson($result->data); } - #[Route('/element/get-predefined-properties', name: 'opendxp_admin_element_getpredefinedproperties', methods: ['GET'])] - public function getPredefinedPropertiesAction(Request $request, TranslatorInterface $translator): JsonResponse - { - $properties = []; - $type = $request->query->get('elementType'); - $query = $request->query->get('query'); - $allowedTypes = ['asset', 'document', 'object']; - - if (in_array($type, $allowedTypes, true)) { - $list = new Model\Property\Predefined\Listing(); - $list->setFilter(function (Model\Property\Predefined $predefined) use ($type, $query, $translator) { - if (!str_contains($predefined->getCtype(), $type)) { - return false; - } - - return !($query && stripos($translator->trans($predefined->getName(), [], 'admin'), (string) $query) === false); - }); - - foreach ($list->getProperties() as $type) { - $properties[] = $type->getObjectVars(); - } - } + #[Route('/get-required-by-dependencies', name: 'opendxp_admin_element_getrequiredbydependencies', methods: ['GET'])] + public function getRequiredByDependenciesAction( + GetRequiredByDependenciesHandler $getRequiredByDependencies, + GetDependenciesPayload $payload, + ): JsonResponse { + $result = $getRequiredByDependencies($payload); - return $this->adminJson(['properties' => $properties]); + return $this->adminJson($result->data); } - #[Route('/element/analyze-permissions', name: 'opendxp_admin_element_analyzepermissions', methods: ['POST'])] - public function analyzePermissionsAction(Request $request): Response - { - $userId = $request->request->getInt('userId'); - if ($userId) { - $userList = []; - if ($user = Model\User::getById($userId)) { - $userList[] = $user; - } - } else { - $userList = new Model\User\Listing(); - $userList->setCondition('`type` = ?', ['user']); - $userList = $userList->load(); - } - - $elementType = $request->request->get('elementType'); - $elementId = $request->request->getInt('elementId'); - - $element = Element\Service::getElementById($elementType, $elementId); - - $result = Element\PermissionChecker::check($element, $userList); + #[Route('/get-predefined-properties', name: 'opendxp_admin_element_getpredefinedproperties', methods: ['GET'])] + public function getPredefinedPropertiesAction( + GetPredefinedPropertiesHandler $getPredefinedProperties, + GetPredefinedPropertiesPayload $payload, + ): JsonResponse { + $result = $getPredefinedProperties($payload); - return $this->adminJson( - [ - 'data' => $result, - 'success' => true, - ] - ); + return $this->adminJson(['properties' => $result->properties]); } - /** - * @throws Exception - */ - protected function getNicePathFormatterFieldDefinition(DataObject\Concrete $source, array $context): DataObject\ClassDefinition\Data|bool|null - { - $ownerType = $context['containerType']; - $fieldname = $context['fieldname']; - $fd = null; - - if ($ownerType === 'object') { - $subContainerType = $context['subContainerType'] ?? null; - if ($subContainerType) { - $subContainerKey = $context['subContainerKey']; - $subContainer = $source->getClass()->getFieldDefinition($subContainerKey); - if (method_exists($subContainer, 'getFieldDefinition')) { - $fd = $subContainer->getFieldDefinition($fieldname); - } - } else { - $fd = $source->getClass()->getFieldDefinition($fieldname); - } - } elseif ($ownerType === 'localizedfield') { - $localizedfields = $source->getClass()->getFieldDefinition('localizedfields'); - if ($localizedfields instanceof DataObject\ClassDefinition\Data\Localizedfields) { - $fd = $localizedfields->getFieldDefinition($fieldname); - } - } elseif ($ownerType === 'objectbrick') { - $fdBrick = DataObject\Objectbrick\Definition::getByKey($context['containerKey']); - $fd = $fdBrick->getFieldDefinition($fieldname); - } elseif ($ownerType === 'fieldcollection') { - $containerKey = $context['containerKey']; - $fdCollection = DataObject\Fieldcollection\Definition::getByKey($containerKey); - if (($context['subContainerType'] ?? null) === 'localizedfield') { - /** @var DataObject\ClassDefinition\Data\Localizedfields $fdLocalizedFields */ - $fdLocalizedFields = $fdCollection->getFieldDefinition('localizedfields'); - $fd = $fdLocalizedFields->getFieldDefinition($fieldname); - } else { - $fd = $fdCollection->getFieldDefinition($fieldname); - } - } - - return $fd; - } - - /** - * @throws Exception - */ - protected function convertResultWithPathFormatter(DataObject\Concrete $source, array $context, array $result, array $targets): array - { - $fd = $this->getNicePathFormatterFieldDefinition($source, $context); - - if ($fd instanceof DataObject\ClassDefinition\PathFormatterAwareInterface) { - $formatter = $fd->getPathFormatterClass(); - - if (null !== $formatter) { - $pathFormatter = DataObject\ClassDefinition\Helper\PathFormatterResolver::resolvePathFormatter( - $fd->getPathFormatterClass() - ); - - if ($pathFormatter instanceof DataObject\ClassDefinition\PathFormatterInterface) { - $result = $pathFormatter->formatPath($result, $source, $targets, [ - 'fd' => $fd, - 'context' => $context, - ]); - } - } - } + #[Route('/analyze-permissions', name: 'opendxp_admin_element_analyzepermissions', methods: ['POST'])] + public function analyzePermissionsAction( + AnalyzePermissionsHandler $analyzePermissions, + AnalyzePermissionsPayload $payload, + ): Response { + $result = $analyzePermissions($payload); - return $result; + return $this->adminJson(ApiResponse::ok(['data' => $result->data])); } } diff --git a/src/Controller/Admin/ElementControllerBase.php b/src/Controller/Admin/ElementControllerBase.php index 08e4e5cb..a36106b9 100644 --- a/src/Controller/Admin/ElementControllerBase.php +++ b/src/Controller/Admin/ElementControllerBase.php @@ -16,25 +16,16 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\Event\AssetEvents; -use OpenDxp\Bundle\AdminBundle\Event\Model\AssetDeleteInfoEvent; -use OpenDxp\Bundle\AdminBundle\Event\Model\DataObjectDeleteInfoEvent; -use OpenDxp\Bundle\AdminBundle\Event\Model\DocumentDeleteInfoEvent; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoPayload; use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; -use OpenDxp\Event\DataObjectEvents; -use OpenDxp\Event\DocumentEvents; -use OpenDxp\Logger; -use OpenDxp\Model\Asset; -use OpenDxp\Model\DataObject\AbstractObject; -use OpenDxp\Model\Document; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; use OpenDxp\Model\Element\ElementInterface; use OpenDxp\Model\Element\Service; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; /** * @internal @@ -54,15 +45,15 @@ protected function getTreeNodeConfig(ElementInterface $element): array } #[Route('/tree-get-root', name: 'treegetroot', methods: ['GET'])] - public function treeGetRootAction(Request $request): JsonResponse + public function treeGetRootAction( + #[MapQueryParameter] ?string $elementType = null, + #[MapQueryParameter(flags: FILTER_NULL_ON_FAILURE)] ?int $id = null, + ): JsonResponse { - $type = $request->query->get('elementType'); + $type = $elementType; $allowedTypes = ['asset', 'document', 'object']; - $id = 1; - if ($request->query->get('id')) { - $id = (int)$request->query->get('id'); - } + $id = $id ?? 1; if (in_array($type, $allowedTypes)) { $root = Service::getElementById($type, $id); @@ -70,163 +61,18 @@ public function treeGetRootAction(Request $request): JsonResponse return $this->adminJson($this->getTreeNodeConfig($root)); } - return $this->adminJson(['success' => false, 'id' => $id]); + return $this->adminJson(ApiResponse::error(null, ['id' => $id])); } - return $this->adminJson(['success' => false, 'message' => 'missing_permission']); + return $this->adminJson(ApiResponse::error('missing_permission')); } - /** - * @throws Exception - */ #[Route('/delete-info', name: 'deleteinfo', methods: ['GET'])] - public function deleteInfoAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function deleteInfoAction( + GetDeleteInfoHandler $handler, + GetDeleteInfoPayload $payload, + ): JsonResponse { - $hasDependency = false; - $errors = false; - $deleteJobs = []; - $itemResults = []; - - $totalChildren = 0; - - $ids = $request->query->get('id'); - $ids = explode(',', $ids); - $type = $request->query->get('type'); - - foreach ($ids as $id) { - try { - $element = Service::getElementById($type, (int) $id); - if (!$element) { - continue; - } - - if (!$hasDependency) { - $hasDependency = $element->getDependencies()->isRequired(); - } - } catch (Exception) { - Logger::err('failed to access element with id: ' . $id); - - continue; - } - - // check for children - $event = null; - $eventName = null; - - if ($element instanceof Asset) { - $event = new AssetDeleteInfoEvent($element); - $eventName = AssetEvents::DELETE_INFO; - } elseif ($element instanceof Document) { - $event = new DocumentDeleteInfoEvent($element); - $eventName = DocumentEvents::DELETE_INFO; - } elseif ($element instanceof AbstractObject) { - $event = new DataObjectDeleteInfoEvent($element); - $eventName = DataObjectEvents::DELETE_INFO; - } - - if ($element->isLocked()) { - $itemResults[] = [ - 'id' => $element->getId(), - 'type' => $element->getType(), - 'key' => $element->getKey(), - 'reason' => 'Element is locked', - 'allowed' => false, - ]; - $errors |= true; - - continue; - } - - $eventDispatcher->dispatch($event, $eventName); - - if (!$event->getDeletionAllowed()) { - $itemResults[] = [ - 'id' => $element->getId(), - 'type' => $element->getType(), - 'key' => $element->getKey(), - 'reason' => $event->getReason(), - 'allowed' => false, - ]; - $errors |= true; - - continue; - } - - $itemResults[] = [ - 'id' => $element->getId(), - 'type' => $element->getType(), - 'key' => $element->getKey(), - 'path' => $element->getPath(), - 'allowed' => true, - ]; - - $deleteJobs[] = [[ - 'url' => $this->generateUrl('opendxp_admin_recyclebin_add'), - 'method' => 'POST', - 'params' => [ - 'type' => $type, - 'id' => $element->getId(), - ], - ]]; - - $hasChildren = $element->hasChildren(); - if (!$hasDependency) { - $hasDependency = $hasChildren; - } - - if ($hasChildren) { - // get amount of children - $list = $element::getList(['unpublished' => true]); - $pathColumn = 'path'; - $list->setCondition($pathColumn . ' LIKE ?', [$element->getRealFullPath() . '/%']); - $children = $list->getTotalCount(); - $totalChildren += $children; - - if ($children > 0) { - $deleteObjectsPerRequest = 5; - for ($i = 0, $iMax = ceil($children / $deleteObjectsPerRequest); $i < $iMax; $i++) { - $deleteJobs[] = [[ - 'url' => $request->getBaseUrl() . '/admin/' . $type . '/delete', - 'method' => 'DELETE', - 'params' => [ - 'step' => $i, - 'amount' => $deleteObjectsPerRequest, - 'type' => 'children', - 'id' => $element->getId(), - ], - ]]; - } - } - } - - // the element itself is the last one - $deleteJobs[] = [[ - 'url' => $request->getBaseUrl() . '/admin/' . $type . '/delete', - 'method' => 'DELETE', - 'params' => [ - 'id' => $element->getId(), - ], - ]]; - } - - // get the element key in case of just one - $elementKey = false; - if (count($ids) === 1) { - $element = Service::getElementById($type, (int) $ids[0]); - - if ($element instanceof ElementInterface) { - $elementKey = $element->getKey(); - } - } - - return $this->adminJson([ - 'hasDependencies' => $hasDependency, - 'children' => $totalChildren, - 'deletejobs' => $deleteJobs, - 'batchDelete' => count($ids) > 1, - 'elementKey' => $elementKey, - 'errors' => $errors, - 'itemResults' => $itemResults, - ]); + return $this->adminJson($handler($payload)); } } diff --git a/src/Controller/Admin/EmailController.php b/src/Controller/Admin/EmailController.php index 1fe9b431..29e79dff 100644 --- a/src/Controller/Admin/EmailController.php +++ b/src/Controller/Admin/EmailController.php @@ -16,20 +16,35 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\Email\Blocklist\CreateBlocklistEntry\CreateBlocklistEntryHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\Blocklist\DeleteBlocklistEntry\DeleteBlocklistEntryHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\Blocklist\UpdateBlocklistEntry\UpdateBlocklistEntryHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\DeleteEmailLog\DeleteEmailLogHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\GetBlocklist\GetBlocklistHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\GetEmailLogs\GetEmailLogsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\GetEmailLogs\GetEmailLogsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ResendEmail\ResendEmailHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ResendEmail\ResendEmailPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Email\SendTestEmail\SendTestEmailHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\SendTestEmail\SendTestEmailPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\GetEmailLogParams\GetEmailLogParamsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\ShowEmailLogHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\ShowEmailLogPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdBodyPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Email\BlocklistPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use OpenDxp\Http\RequestHelper; -use OpenDxp\Logger; -use OpenDxp\Mail; -use OpenDxp\Model\Element\ElementInterface; -use OpenDxp\Model\Tool; -use ReflectionClass; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Profiler\Profiler; -use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal @@ -37,478 +52,131 @@ #[Route('/email')] class EmailController extends AdminAbstractController { - /** - * @throws Exception - */ + #[IsGranted(new Expression('is_granted("emails") or is_granted("gdpr_data_extractor")'))] #[Route('/email-logs', name: 'opendxp_admin_email_emaillogs', methods: ['GET', 'POST'])] - public function emailLogsAction(Request $request): JsonResponse - { - if (!$this->getAdminUser()->isAllowed('emails') && !$this->getAdminUser()->isAllowed('gdpr_data_extractor')) { - throw new Exception("Permission denied, user needs 'emails' permission."); - } - - $list = new Tool\Email\Log\Listing(); - if ($request->request->has('documentId')) { - $list->setCondition('documentId = ' . (int)$request->request->get('documentId')); - } - $list->setLimit((int)$request->request->get('limit', 50)); - $list->setOffset((int)$request->request->get('start', 0)); - $list->setOrderKey('sentDate'); - - if ($request->request->has('filter')) { - $filterTerm = $request->request->get('filter'); - if ($filterTerm === '*') { - $filterTerm = ''; - } - - $filterTerm = str_replace('%', '*', $filterTerm); - $filterTerm = htmlspecialchars($filterTerm, ENT_QUOTES); - - if (strpos($filterTerm, '@')) { - $parts = explode(' ', $filterTerm); - $parts = array_map(static function ($part) { - if (strpos($part, '@')) { - return '"' . $part . '"'; - } - - return $part; - }, $parts); - $filterTerm = implode(' ', $parts); - } - - if (str_starts_with($filterTerm, '@')) { - $filterTerm = str_replace('@', '', $filterTerm); - } - - $condition = '( MATCH (`from`,`to`,`cc`,`bcc`,`subject`,`params`) AGAINST (' . $list->quote($filterTerm) . ' IN BOOLEAN MODE) )'; - - if ($request->request->has('documentId')) { - $condition .= 'AND documentId = ' . (int)$request->request->get('documentId'); - } - - $list->setCondition($condition); - } - - $list->setOrder('DESC'); - - $data = $list->load(); - $jsonData = []; - - foreach ($data as $entry) { - $tmp = $entry->getObjectVars(); - unset($tmp['bodyHtml'], $tmp['bodyText']); - $jsonData[] = $tmp; - } - - return $this->adminJson([ - 'data' => $jsonData, - 'success' => true, - 'total' => $list->getTotalCount(), - ]); + public function emailLogsAction( + GetEmailLogsHandler $getEmailLogs, + GetEmailLogsPayload $payload, + ): JsonResponse { + $result = $getEmailLogs($payload); + + return $this->adminJson(ApiResponse::ok([ + 'data' => $result->data, + 'total' => $result->total, + ])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Emails->value)] #[Route('/show-email-log', name: 'opendxp_admin_email_showemaillog', methods: ['GET'])] - public function showEmailLogAction(Request $request, ?Profiler $profiler): JsonResponse|Response - { + public function showEmailLogAction( + ShowEmailLogHandler $showEmailLog, + GetEmailLogParamsHandler $getEmailLogParams, + ShowEmailLogPayload $payload, + ?Profiler $profiler, + ): JsonResponse|Response { if ($profiler) { $profiler->disable(); } - if (!$this->getAdminUser()->isAllowed('emails')) { - throw $this->createAccessDeniedHttpException("Permission denied, user needs 'emails' permission."); - } - - $type = $request->query->get('type'); - $emailLog = Tool\Email\Log::getById((int) $request->query->get('id')); - - if (!$emailLog) { - throw $this->createNotFoundException(); - } - - if ($type === 'text') { - return $this->render('@OpenDxpAdmin/admin/email/text.html.twig', ['log' => $emailLog->getTextLog()]); - } - - if ($type === 'html') { - return new Response($emailLog->getHtmlLog(), 200, [ + return match ($payload->type) { + 'params' => $this->adminJson($getEmailLogParams($payload->id)), + 'text' => $this->render('@OpenDxpAdmin/admin/email/text.html.twig', ['log' => $showEmailLog($payload->id)->textLog]), + 'html' => new Response($showEmailLog($payload->id)->htmlLog, 200, [ 'Content-Security-Policy' => "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:", - ]); - } - - if ($type === 'params') { - try { - $params = $emailLog->getParams(); - } catch (Exception) { - Logger::warning('Could not decode JSON param string'); - $params = []; - } - foreach ($params as &$entry) { - $this->enhanceLoggingData($entry); - } - - return $this->adminJson($params); - } - - if ($type === 'details') { - $data = $emailLog->getObjectVars(); - - return $this->adminJson($data); - } - - return new Response('No Type specified'); - - } - - protected function enhanceLoggingData(?array &$data, ?array &$fullEntry = null): void - { - if (!is_array($data)) { - return; - } - - if (!empty($data['objectClass'])) { - $class = '\\' . ltrim($data['objectClass'], '\\'); - $reflection = new ReflectionClass($class); - - if (!empty($data['objectId']) && $reflection->implementsInterface(ElementInterface::class)) { - $obj = $class::getById($data['objectId']); - $data['objectPath'] = is_null($obj) ? '' : $obj->getRealFullPath(); - - //check for classmapping - if (stristr($class, '\\OpenDxp\\Model') === false) { - $niceClassName = '\\' . ltrim($reflection->getParentClass()->getName(), '\\'); - } else { - $niceClassName = $class; - } - - $niceClassName = str_replace(['\\OpenDxp\\Model\\', '_'], ['', '\\'], $niceClassName); - - $tmp = explode('\\', $niceClassName); - if (in_array($tmp[0], ['DataObject', 'Document', 'Asset'])) { - $data['objectClassBase'] = $tmp[0]; - $data['objectClassSubType'] = $tmp[1]; - } - } - } - - foreach ($data as &$value) { - - if (!is_array($value)) { - continue; - } - - $this->enhanceLoggingData($value, $data); - } - - unset($value); - - if ($data['children'] ?? false) { - foreach ($data['children'] as $key => $entry) { - if (is_string($key)) { //key must be integers - unset($data['children'][$key]); - } - } - $data['iconCls'] = 'opendxp_icon_folder'; - $data['data'] = ['type' => 'simple', 'value' => 'Children (' . count($data['children']) . ')']; - } else { - //setting the icon class - if (empty($data['iconCls'])) { - if (($data['objectClassBase'] ?? '') === 'DataObject') { - $fullEntry['iconCls'] = 'opendxp_icon_object'; - } elseif (($data['objectClassBase'] ?? '') === 'Asset') { - $fullEntry['iconCls'] = match ($data['objectClassSubType']) { - 'Image' => 'opendxp_icon_image', - 'Video' => 'opendxp_icon_wmv', - 'Text' => 'opendxp_icon_txt', - 'Document' => 'opendxp_icon_pdf', - default => 'opendxp_icon_asset', - }; - } elseif (str_starts_with($data['objectClass'] ?? '', 'Document')) { - $fullEntry['iconCls'] = 'opendxp_icon_' . strtolower($data['objectClassSubType']); - } else { - $data['iconCls'] = 'opendxp_icon_text'; - } - } - - $data['leaf'] = true; - } + ]), + 'details' => $this->adminJson($showEmailLog($payload->id)->objectVars), + default => new Response('No Type specified'), + }; } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Emails->value)] #[Route('/delete-email-log', name: 'opendxp_admin_email_deleteemaillog', methods: ['DELETE'])] - public function deleteEmailLogAction(Request $request): JsonResponse - { - if (!$this->getAdminUser()->isAllowed('emails')) { - throw $this->createAccessDeniedHttpException("Permission denied, user needs 'emails' permission."); - } - - $success = false; - $emailLog = Tool\Email\Log::getById((int) $request->request->get('id')); - if ($emailLog instanceof Tool\Email\Log) { - $emailLog->delete(); - $success = true; - } + public function deleteEmailLogAction( + DeleteEmailLogHandler $deleteEmailLog, + IdBodyPayload $payload, + ): JsonResponse { + $deleteEmailLog($payload); - return $this->adminJson([ - 'success' => $success, - ]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Emails->value)] #[Route('/resend-email', name: 'opendxp_admin_email_resendemail', methods: ['POST'])] - public function resendEmailAction(Request $request): JsonResponse - { - if (!$this->getAdminUser()->isAllowed('emails')) { - throw $this->createAccessDeniedHttpException("Permission denied, user needs 'emails' permission."); - } - - $success = false; - $emailLog = Tool\Email\Log::getById((int) $request->request->get('id')); - - if ($emailLog instanceof Tool\Email\Log) { - $mail = new Mail(); - $mail->preventDebugInformationAppending(); - $mail->setIgnoreDebugMode(true); - - if (!empty($request->request->get('to'))) { - $emailLog->setTo(null); - $emailLog->setCc(null); - $emailLog->setBcc(null); - } else { - $mail->disableLogging(); - } - - if ($html = $emailLog->getHtmlLog()) { - $mail->html($html); - } - - if ($text = $emailLog->getTextLog()) { - $mail->text($text); - } - - foreach (['From', 'To', 'Cc', 'Bcc', 'ReplyTo'] as $field) { - if (!$values = $request->request->get(strtolower($field))) { - $getter = 'get' . $field; - $values = $emailLog->{$getter}(); - } - - $values = \OpenDxp\Helper\Mail::parseEmailAddressField($values); - - if ($values) { - [$value] = $values; - $prefix = 'add'; - $mail->{$prefix . $field}(new Address($value['email'], $value['name'])); - } - } - - $mail->subject($emailLog->getSubject()); - - // add document - if ($emailLog->getDocumentId()) { - $mail->setDocument($emailLog->getDocumentId()); - } - - // re-add params - try { - $params = $emailLog->getParams(); - } catch (Exception) { - Logger::warning('Could not decode JSON param string'); - $params = []; - } + public function resendEmailAction( + ResendEmailHandler $resendEmail, + ResendEmailPayload $payload, + ): JsonResponse { + $resendEmail($payload); - foreach ($params as $entry) { - $data = null; - $hasChildren = isset($entry['children']) && is_array($entry['children']); - - if ($hasChildren) { - $childData = []; - foreach ($entry['children'] as $childParam) { - $childData[$childParam['key']] = $this->parseLoggingParamObject($childParam); - } - $data = $childData; - } else { - $data = $this->parseLoggingParamObject($entry); - } - - $mail->setParam($entry['key'], $data); - } - - $mail->send(); - $success = true; - } - - return $this->adminJson([ - 'success' => $success, - ]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Emails->value)] #[Route('/send-test-email', name: 'opendxp_admin_email_sendtestemail', methods: ['POST'])] - public function sendTestEmailAction(Request $request): JsonResponse - { - if (!$this->getAdminUser()->isAllowed('emails')) { - throw new Exception("Permission denied, user needs 'emails' permission."); - } - + public function sendTestEmailAction( + Request $request, + SendTestEmailHandler $sendTestEmail, + SendTestEmailPayload $payload, + ): JsonResponse { // Simulate a frontend request to prefix assets $request->attributes->set(RequestHelper::ATTRIBUTE_FRONTEND_REQUEST, true); - $mail = new Mail(); - - if ($request->request->get('emailType') === 'text') { - $mail->text(strip_tags($request->request->get('content'))); - } elseif ($request->request->get('emailType') === 'html') { - $mail->html($request->request->get('content')); - } elseif ($request->request->get('emailType') === 'document') { - $doc = \OpenDxp\Model\Document::getByPath($request->request->get('documentPath')); - - if ($doc instanceof \OpenDxp\Model\Document\Email) { - $mail->setDocument($doc); - - if ($request->request->has('mailParamaters') && $mailParamsArray = json_decode($request->request->get('mailParamaters'), true)) { - foreach ($mailParamsArray as $mailParam) { - if ($mailParam['key']) { - $mail->setParam($mailParam['key'], $mailParam['value']); - } - } - } - } else { - throw new Exception('Email document not found!'); - } - } - - if ($from = $request->request->get('from')) { - $addressArray = \OpenDxp\Helper\Mail::parseEmailAddressField($from); - if ($addressArray) { - //use the first address only - [$cleanedFromAddress] = $addressArray; - $mail->from(new Address($cleanedFromAddress['email'], $cleanedFromAddress['name'])); - } - } - - $toAddresses = \OpenDxp\Helper\Mail::parseEmailAddressField($request->request->get('to')); - foreach ($toAddresses as $cleanedToAddress) { - $mail->addTo($cleanedToAddress['email'], $cleanedToAddress['name']); - } - - $mail->subject($request->request->get('subject')); - $mail->setIgnoreDebugMode(true); - - $mail->send(); + $sendTestEmail($payload); - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Emails->value)] #[Route('/blocklist', name: 'opendxp_admin_email_blocklist', methods: ['POST'])] - public function blocklistAction(Request $request): JsonResponse - { - if (!$this->getAdminUser()->isAllowed('emails')) { - throw new Exception("Permission denied, user needs 'emails' permission."); - } - - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - - if (is_array($data)) { - foreach ($data as $key => &$value) { - if (is_string($value)) { - if ($key === 'address') { - $value = filter_var($value, FILTER_SANITIZE_EMAIL); - } - - $value = trim($value); - } - } - } - - if ($request->query->get('xaction') === 'destroy') { - $address = Tool\Email\Blocklist::getByAddress($data['address']); - $address->delete(); - - return $this->adminJson(['success' => true, 'data' => []]); - } - - if ($request->query->get('xaction') === 'update') { - $address = Tool\Email\Blocklist::getByAddress($data['address']); - $address->setValues($data); - $address->save(); - - return $this->adminJson(['data' => $address->getObjectVars(), 'success' => true]); - } - - if ($request->query->get('xaction') === 'create') { - unset($data['id']); - - $address = new Tool\Email\Blocklist(); - $address->setValues($data); - $address->save(); - - return $this->adminJson(['data' => $address->getObjectVars(), 'success' => true]); - } - } else { - - $list = new Tool\Email\Blocklist\Listing(); - - $list->setLimit((int) $request->request->get('limit', 50)); - $list->setOffset((int) $request->request->get('start', 0)); - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->request->all()); - - if ($sortingSettings['orderKey']) { - $list->setOrderKey($sortingSettings['orderKey']); - $list->setOrder($sortingSettings['order']); - } - - if ($request->request->has('filter')) { - $list->setCondition('`address` LIKE ' . $list->quote('%'.$request->request->get('filter').'%')); - } - - $data = $list->load(); - $jsonData = []; - foreach ($data as $entry) { - $jsonData[] = $entry->getObjectVars(); - } + public function blocklistAction( + Request $request, + BlocklistPayload $payload, + GetBlocklistHandler $getBlocklist, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::blocklistDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::blocklistUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::blocklistCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } + + $result = $getBlocklist($payload); + + return $this->adminJson(ApiResponse::ok([ + 'data' => $result->data, + 'total' => $result->total, + ])); + } - return $this->adminJson([ - 'success' => true, - 'data' => $jsonData, - 'total' => $list->getTotalCount(), - ]); - } + #[IsGranted(CorePermission::Emails->value)] + #[Route('/blocklist-destroy', name: 'opendxp_admin_email_blocklist_destroy', methods: ['POST'])] + public function blocklistDestroyAction( + BlocklistPayload $payload, + DeleteBlocklistEntryHandler $deleteBlocklistEntry, + ): JsonResponse { + $deleteBlocklistEntry($payload); - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['data' => []])); } - protected function parseLoggingParamObject(array $params): mixed - { - $data = null; - if ($params['data']['type'] === 'object') { - $class = '\\' . ltrim($params['data']['objectClass'], '\\'); - $reflection = new ReflectionClass($class); - - if (!empty($params['data']['objectId']) && $reflection->implementsInterface(ElementInterface::class)) { - $obj = $class::getById($params['data']['objectId']); - if (!is_null($obj)) { - $data = $obj; - } - } - } else { - $data = $params['data']['value']; - } + #[IsGranted(CorePermission::Emails->value)] + #[Route('/blocklist-update', name: 'opendxp_admin_email_blocklist_update', methods: ['POST'])] + public function blocklistUpdateAction( + BlocklistPayload $payload, + UpdateBlocklistEntryHandler $updateBlocklistEntry, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $updateBlocklistEntry($payload)])); + } - return $data; + #[IsGranted(CorePermission::Emails->value)] + #[Route('/blocklist-create', name: 'opendxp_admin_email_blocklist_create', methods: ['POST'])] + public function blocklistCreateAction( + BlocklistPayload $payload, + CreateBlocklistEntryHandler $createBlocklistEntry, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $createBlocklistEntry($payload)])); } } diff --git a/src/Controller/Admin/IndexController.php b/src/Controller/Admin/IndexController.php index ca2b7ccd..c0fc048c 100644 --- a/src/Controller/Admin/IndexController.php +++ b/src/Controller/Admin/IndexController.php @@ -1,5 +1,4 @@ getAdminUser(); - $perspectiveConfig = new \OpenDxp\Bundle\AdminBundle\Perspective\Config(); - $templateParams = [ - 'config' => $config, - 'systemSettings' => SystemSettingsConfig::get(), - 'adminSettings' => AdminConfig::get(), - 'perspectiveConfig' => $perspectiveConfig, - ]; - - $this - ->setAdminLanguage($request, $user) - ->addRuntimePerspective($templateParams, $user) - ->addPluginAssets($bundleManager, $templateParams); - - $this->buildOpenDxpSettings( - $request, - $templateParams, - $user, - $kernel, - $maintenanceExecutor, - $csrfProtection, - $maintenanceModeHelper - ); if ($user->getTwoFactorAuthentication('required') && !$user->getTwoFactorAuthentication('enabled')) { return $this->redirectToRoute('opendxp_admin_2fa_setup'); } - // allow to alter settings via an event - $settingsEvent = new IndexActionSettingsEvent($templateParams['settings'] ?? []); - $this->eventDispatcher->dispatch($settingsEvent, AdminEvents::INDEX_ACTION_SETTINGS); - $templateParams['settings'] = $settingsEvent->getSettings(); - - return $this->render($settingsEvent->getTemplate() ?: '@OpenDxpAdmin/admin/index/index.html.twig', $templateParams); - } - - #[Route('/index/statistics', name: 'opendxp_admin_index_statistics', methods: ['GET'])] - public function statisticsAction(Request $request, Connection $db, KernelInterface $kernel): JsonResponse - { - if (!$request->isXmlHttpRequest()) { - throw $this->createAccessDeniedHttpException(); - } - - try { - $mysqlVersion = $db->fetchOne('SELECT VERSION()'); - } catch (Throwable) { - $mysqlVersion = null; - } - - try { - $data = [ - 'instance_id' => $this->getInstanceId(), - 'revision' => Version::getRevision(), - 'version' => Version::getVersion(), - 'major_version' => Version::getMajorVersion(), - 'php_version' => PHP_VERSION, - 'db_version' => $mysqlVersion, - 'bundles' => array_keys($kernel->getBundles()), - ]; - } catch (Throwable) { - $data = []; - } - - try { - $this->httpClient->request( - 'POST', - 'https://metrics.opendxp.io/statistics', - [ - 'json' => $data, - ] - ); - } catch (GuzzleException) { - // fail silently - } - - return $this->adminJson([ - 'success' => true, - ]); - } - - protected function addRuntimePerspective(array &$templateParams, User $user): static - { - $runtimePerspective = \OpenDxp\Bundle\AdminBundle\Perspective\Config::getRuntimePerspective($user); - $templateParams['runtimePerspective'] = $runtimePerspective; - - return $this; - } - - protected function addPluginAssets(OpenDxpBundleManager $bundleManager, array &$templateParams): static - { - $templateParams['pluginJsPaths'] = $bundleManager->getJsPaths(); - $templateParams['pluginCssPaths'] = $bundleManager->getCssPaths(); - - return $this; - } - - protected function setAdminLanguage(Request $request, User $user): static - { - // set user language $request->setLocale($user->getLanguage()); - if ($this->translator instanceof LocaleAwareInterface) { - $this->translator->setLocale($user->getLanguage()); - } - - return $this; - } - - protected function buildOpenDxpSettings( - Request $request, - array &$templateParams, - User $user, KernelInterface $kernel, - ExecutorInterface $maintenanceExecutor, - CsrfProtectionHandler $csrfProtection, - Tool\MaintenanceModeHelperInterface $maintenanceModeHelper - ): static { - $config = $templateParams['config']; - $systemSettings = $templateParams['systemSettings']; - $adminSettings = $templateParams['adminSettings']; - $requiredLanguages = $systemSettings['general']['valid_languages']; - $dashboardHelper = new Dashboard($user); - $customAdminEntrypoint = $this->getParameter('opendxp_admin.custom_admin_route_name'); - - try { - $adminEntrypointUrl = $this->generateUrl($customAdminEntrypoint, [], UrlGeneratorInterface::ABSOLUTE_URL); - } catch (Exception) { - // if the custom admin entrypoint is not defined, return null in the settings - $adminEntrypointUrl = null; - } - - if (array_key_exists('required_languages', $systemSettings['general'])) { - $requiredLanguages = $systemSettings['general']['required_languages']; - } - - $settings = [ - 'instanceId' => $this->getInstanceId(), - 'version' => Version::getVersion(), - 'build' => Version::getRevision(), - 'debug' => OpenDxp::inDebugMode(), - 'devmode' => OpenDxp::inDevMode(), - 'disableMinifyJs' => OpenDxp::disableMinifyJs(), - 'environment' => $kernel->getEnvironment(), - 'sessionId' => htmlentities($request->getSession()->getId(), ENT_QUOTES, 'UTF-8'), - - // languages - 'language' => $request->getLocale(), - 'websiteLanguages' => Admin::reorderWebsiteLanguages( - $this->getAdminUser(), - $systemSettings['general']['valid_languages'], - true - ), - 'requiredLanguages' => $requiredLanguages, - - // flags - 'showCloseConfirmation' => true, - 'debug_admin_translations' => (bool)$systemSettings['general']['debug_admin_translations'], - 'document_generatepreviews' => (bool)$config['documents']['generate_preview'], - 'asset_disable_tree_preview' => (bool)$adminSettings['assets']['disable_tree_preview'], - 'asset_hide_edit' => (bool)$adminSettings['assets']['hide_edit_image'], - 'asset_tree_paging_limit' => $config['assets']['tree_paging_limit'], - 'asset_default_upload_path' => $config['assets']['default_upload_path'], - 'chromium' => HtmlToImage::isSupported(), - 'videoconverter' => Video::isAvailable(), - 'main_domain' => $systemSettings['general']['domain'], - 'custom_admin_entrypoint_url' => $adminEntrypointUrl, - 'timezone' => $config['general']['timezone'] ?: date_default_timezone_get(), - 'tile_layer_url_template' => $config['maps']['tile_layer_url_template'], - 'geocoding_url_template' => $config['maps']['geocoding_url_template'], - 'reverse_geocoding_url_template' => $config['maps']['reverse_geocoding_url_template'], - 'document_tree_paging_limit' => $config['documents']['tree_paging_limit'], - 'object_tree_paging_limit' => $config['objects']['tree_paging_limit'], - 'hostname' => htmlentities(\OpenDxp\Tool::getHostname(), ENT_QUOTES, 'UTF-8'), - 'dependency' => $config['dependency']['enabled'], - - 'document_auto_save_interval' => $config['documents']['auto_save_interval'], - 'object_auto_save_interval' => $config['objects']['auto_save_interval'], - - // perspective and portlets - 'perspective' => $templateParams['runtimePerspective'], - 'availablePerspectives' => \OpenDxp\Bundle\AdminBundle\Perspective\Config::getAvailablePerspectives($user), - 'disabledPortlets' => $dashboardHelper->getDisabledPortlets(), - - // this stuff is used to decide whether the "add" button should be grayed out or not - 'image-thumbnails-writeable' => (new Asset\Image\Thumbnail\Config())->isWriteable(), - 'video-thumbnails-writeable' => (new Asset\Video\Thumbnail\Config())->isWriteable(), - 'document-types-writeable' => (new DocType())->isWriteable(), - 'predefined-properties-writeable' => (new Predefined())->isWriteable(), - 'predefined-asset-metadata-writeable' => (new \OpenDxp\Model\Metadata\Predefined())->isWriteable(), - 'perspectives-writeable' => \OpenDxp\Bundle\AdminBundle\Perspective\Config::isWriteable(), - 'custom-views-writeable' => \OpenDxp\Bundle\AdminBundle\CustomView\Config::isWriteable(), - 'class-definition-writeable' => !isset($_SERVER['OPENDXP_CLASS_DEFINITION_WRITABLE']) || - (bool) $_SERVER['OPENDXP_CLASS_DEFINITION_WRITABLE'], - 'object-custom-layout-writeable' => (new CustomLayout())->isWriteable(), - 'select-options-writeable' => (new \OpenDxp\Model\DataObject\SelectOptions\Config())->isWriteable(), - - // search types - 'asset_search_types' => Asset::getTypes(), - - // document types - 'document_types_configuration' => Document::getTypesConfiguration(), - 'document_search_types' => Document::getTypes(), - 'document_valid_types' => array_values(array_filter(Document::getTypes(), fn ($type) => $type !== 'folder')), - // email search compatible document types - 'document_email_search_types' => $config['documents']['email_search'], - 'select_options_provider_class' => SelectOptionsOptionsProvider::class, - ]; - - $this - ->addSystemVarSettings($settings) - ->addMaintenanceSettings($settings, $maintenanceExecutor, $maintenanceModeHelper) - ->addMailSettings($settings, $config, $systemSettings) - ->addCustomViewSettings($settings) - ->addNotificationSettings($settings, $config); - - $settings['csrfToken'] = $csrfProtection->getCsrfToken($request->getSession()); - - $templateParams['settings'] = $settings; - - return $this; - } - - private function getInstanceId(): string - { - $instanceId = 'not-set'; - - try { - $instanceId = $this->getParameter('secret'); - $instanceId = sha1(substr($instanceId, 3, -3)); - } catch (Exception) { - // nothing to do + if ($translator instanceof LocaleAwareInterface) { + $translator->setLocale($user->getLanguage()); } - return $instanceId; - } - - protected function addSystemVarSettings(array &$settings): static - { - // upload limit - $max_upload = OpenDxp\Helper\FileSystemHelper::filesizeToBytes(ini_get('upload_max_filesize') . 'B'); - $max_post = OpenDxp\Helper\FileSystemHelper::filesizeToBytes(ini_get('post_max_size') . 'B'); - $upload_mb = min($max_upload, $max_post) ?: $max_upload; - - $settings['upload_max_filesize'] = (int) $upload_mb; + $result = $settingsHandler($payload); - // session lifetime (gc) - $session_gc_maxlifetime = ini_get('session.gc_maxlifetime'); - if (empty($session_gc_maxlifetime)) { - $session_gc_maxlifetime = 120; - } - - $settings['session_gc_maxlifetime'] = (int)$session_gc_maxlifetime; - - return $this; + return $this->render($result->template ?? '@OpenDxpAdmin/admin/index/index.html.twig', $result->templateParams); } - protected function addMaintenanceSettings( - array &$settings, - ExecutorInterface $maintenanceExecutor, - Tool\MaintenanceModeHelperInterface $maintenanceModeHelper - ): static { - // check maintenance - $maintenance_active = false; - // maintenance script should run at least every hour + a little tolerance - if (($lastExecution = $maintenanceExecutor->getLastExecution()) && time() - $lastExecution < 3660) { - $maintenance_active = true; - } - - $settings['maintenance_active'] = $maintenance_active; - $settings['maintenance_mode'] = $maintenanceModeHelper->isActive(); - - return $this; - } - - protected function addMailSettings(array &$settings, Config $config, array $systemSettings): static - { - //mail settings - $mailIncomplete = false; - if (isset($config['email']) && $systemSettings['email']) { - if (OpenDxp::inDebugMode() && empty($systemSettings['email']['debug']['email_addresses'])) { - $mailIncomplete = true; - } - if (empty($config['email']['sender']['email'])) { - $mailIncomplete = true; - } - } - - $settings['mail'] = !$mailIncomplete; - $settings['mailDefaultAddress'] = $config['email']['sender']['email'] ?? null; - - return $this; - } - - protected function addCustomViewSettings(array &$settings): static - { - $cvData = []; - - // still needed when publishing objects - $cvConfig = \OpenDxp\Bundle\AdminBundle\CustomView\Config::get(); - - foreach ($cvConfig as $node) { - $tmpData = $node; - // backwards compatibility - $treeType = $tmpData['treetype'] ?: 'object'; - $rootNode = Service::getElementByPath($treeType, $tmpData['rootfolder']); - - if ($rootNode) { - $tmpData['rootId'] = $rootNode->getId(); - $tmpData['allowedClasses'] = $tmpData['classes'] ?? null; - $tmpData['showroot'] = (bool)$tmpData['showroot']; + #[Route('/index/statistics', name: 'opendxp_admin_index_statistics', methods: ['GET'])] + public function statisticsAction( + Request $request, + EmptyPayload $payload, + StatisticsHandler $statisticsHandler, + ): JsonResponse { - // Check if a user has privileges to that node - if ($rootNode->isAllowed('list')) { - $cvData[] = $tmpData; - } - } + if (!$request->isXmlHttpRequest()) { + throw $this->createAccessDeniedHttpException(); } - $settings['customviews'] = $cvData; - - return $this; - } - - /** - * @return $this - */ - protected function addNotificationSettings(array &$settings, Config $config): static - { - $enabled = (bool)$config['notifications']['enabled']; - - $settings['notifications_enabled'] = $enabled; - $settings['checknewnotification_enabled'] = $enabled && (bool) $config['notifications']['check_new_notification']['enabled']; - - // convert the config parameter interval (seconds) in milliseconds - $settings['checknewnotification_interval'] = $config['notifications']['check_new_notification']['interval'] * 1000; + $statisticsHandler($payload); - return $this; + return $this->adminJson(ApiResponse::ok()); } public function onKernelResponseEvent(ResponseEvent $event): void diff --git a/src/Controller/Admin/InstallController.php b/src/Controller/Admin/InstallController.php index df2ef2ef..45d36045 100644 --- a/src/Controller/Admin/InstallController.php +++ b/src/Controller/Admin/InstallController.php @@ -16,10 +16,9 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Doctrine\DBAL\Connection; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Tool\Requirements; -use Symfony\Component\HttpFoundation\Request; +use OpenDxp\Bundle\AdminBundle\Handler\Install\CheckSystem\CheckSystemHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Install\CheckSystem\CheckSystemPayload; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Routing\Attribute\Route; @@ -31,15 +30,17 @@ class InstallController extends AdminAbstractController { #[Route('/check', name: 'opendxp_admin_install_check', methods: ['GET', 'POST'])] - public function checkAction(Request $request, Connection $db, ?Profiler $profiler): Response - { + public function checkAction( + CheckSystemPayload $payload, + CheckSystemHandler $handler, + ?Profiler $profiler, + ): Response { if ($profiler) { $profiler->disable(); } - $viewParams = Requirements::checkAll($db); - $viewParams['headless'] = $request->query->getBoolean('headless') || $request->request->getBoolean('headless'); + $result = $handler($payload); - return $this->render('@OpenDxpAdmin/admin/install/check.html.twig', $viewParams); + return $this->render('@OpenDxpAdmin/admin/install/check.html.twig', $result->viewParams); } } diff --git a/src/Controller/Admin/LoginController.php b/src/Controller/Admin/LoginController.php index b78791fc..79044f98 100644 --- a/src/Controller/Admin/LoginController.php +++ b/src/Controller/Admin/LoginController.php @@ -17,42 +17,33 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Browser; -use Endroid\QrCode\Builder\Builder; -use Endroid\QrCode\Writer\PngWriter; -use Exception; -use OpenDxp; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; use OpenDxp\Bundle\AdminBundle\Event\AdminEvents; use OpenDxp\Bundle\AdminBundle\Event\Login\LoginRedirectEvent; -use OpenDxp\Bundle\AdminBundle\Event\Login\LostPasswordEvent; +use OpenDxp\Bundle\AdminBundle\Handler\Login\Deeplink\DeeplinkHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Login\Deeplink\DeeplinkPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Login\GenerateTwoFactorSetup\GenerateTwoFactorSetupHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Login\GenerateTwoFactorSetup\GenerateTwoFactorSetupPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Login\LoginCheck\LoginCheckPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Login\LostPassword\LostPasswordHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Login\LostPassword\LostPasswordPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Login\SaveTwoFactorSetup\SaveTwoFactorSetupHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Login\SaveTwoFactorSetup\SaveTwoFactorSetupPayload; +use OpenDxp\Bundle\AdminBundle\Factory\LoginPageFactory; use OpenDxp\Bundle\AdminBundle\Security\CsrfProtectionHandler; -use OpenDxp\Bundle\AdminBundle\System\AdminConfig; -use OpenDxp\Config; use OpenDxp\Controller\KernelControllerEventInterface; use OpenDxp\Controller\KernelResponseEventInterface; -use OpenDxp\Extension\Bundle\OpenDxpBundleManager; -use OpenDxp\Http\Request\Host\GeneralHostResolver; use OpenDxp\Http\ResponseHelper; -use OpenDxp\Logger; -use OpenDxp\Model\User; -use OpenDxp\Security\SecurityHelper; -use OpenDxp\Tool; -use OpenDxp\Tool\Authentication; -use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\SecurityRequestAttributes; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; @@ -66,21 +57,19 @@ class LoginController extends AdminAbstractController implements KernelControlle public function __construct( protected ResponseHelper $responseHelper, protected TranslatorInterface $translator, - protected OpenDxpBundleManager $bundleManager, - protected EventDispatcherInterface $eventDispatcher + protected EventDispatcherInterface $eventDispatcher, + private readonly LoginPageFactory $loginPageFactory, ) { } public function onKernelControllerEvent(ControllerEvent $event): void { - // use browser language for login page if possible $locale = 'en'; + $availableLocales = \OpenDxp\Tool\Admin::getLanguages(); - $availableLocales = Tool\Admin::getLanguages(); foreach ($event->getRequest()->getLanguages() as $userLocale) { if (in_array($userLocale, $availableLocales)) { $locale = $userLocale; - break; } } @@ -101,10 +90,9 @@ public function onKernelResponseEvent(ResponseEvent $event): void #[Route('/login/', name: 'opendxp_admin_login_fallback')] public function loginAction( Request $request, - AuthenticationUtils $authenticationUtils, CsrfProtectionHandler $csrfProtection, - Config $config - ): RedirectResponse|Response { + #[MapQueryParameter(name: 'too_many_attempts')] ?string $tooManyAttempts = null, + ): Response { $queryParams = $request->query->all(); @@ -117,58 +105,22 @@ public function loginAction( return new RedirectResponse($redirectUrl); } - // check csrf token before generating a new one with force=true if (!$csrfProtection->getCsrfToken($request->getSession())) { $csrfProtection->regenerateCsrfToken($request->getSession()); } - $user = $this->getUser(); - if ($user instanceof UserInterface) { + if ($this->getUser() instanceof UserInterface) { return $this->redirectToRoute('opendxp_admin_index'); } - $params = $this->buildLoginPageViewParams($config); - - $session_gc_maxlifetime = ini_get('session.gc_maxlifetime'); - if (empty($session_gc_maxlifetime)) { - $session_gc_maxlifetime = 120; - } - - $params['csrfTokenRefreshInterval'] = ((int)$session_gc_maxlifetime - 60) * 1000; - - if ($request->query->has('too_many_attempts')) { - $params['error'] = SecurityHelper::convertHtmlSpecialChars($request->query->get('too_many_attempts')); - } - if ($request->query->has('auth_failed')) { - $params['error'] = 'error_auth_failed'; - } - if ($request->query->has('session_expired')) { - $params['error'] = 'error_session_expired'; - } - if ($request->query->has('deeplink')) { - $params['deeplink'] = true; - } - - $params['browserSupported'] = $this->detectBrowser(); - $params['debug'] = OpenDxp::inDebugMode(); - - $params['includeTemplates'] = []; - $event = new GenericEvent($this, [ - 'parameters' => $params, - 'config' => $config, - 'request' => $request, - ]); - - $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_BEFORE_RENDER); - $params = $event->getArgument('parameters'); - - $params['login_error'] = $authenticationUtils->getLastAuthenticationError(); - - return $this->render('@OpenDxpAdmin/admin/login/login.html.twig', $params); + return $this->render( + '@OpenDxpAdmin/admin/login/login.html.twig', + $this->loginPageFactory->create($request)->forLoginPage($tooManyAttempts), + ); } #[Route('/login/csrf-token', name: 'opendxp_admin_login_csrf_token')] - public function csrfTokenAction(Request $request, CsrfProtectionHandler $csrfProtection): \Symfony\Component\HttpFoundation\JsonResponse + public function csrfTokenAction(Request $request, CsrfProtectionHandler $csrfProtection): JsonResponse { if (!$this->getAdminUser()) { $csrfProtection->regenerateCsrfToken($request->getSession()); @@ -182,21 +134,20 @@ public function csrfTokenAction(Request $request, CsrfProtectionHandler $csrfPro #[Route('/logout', name: 'opendxp_admin_logout', methods: ['POST'])] public function logoutAction(): void { - // this route will never be matched, but will be handled by the logout handler + // handled by the logout handler } /** * Dummy route used to check authentication */ #[Route('/login/login', name: 'opendxp_admin_login_check')] - public function loginCheckAction(Request $request): RedirectResponse + public function loginCheckAction(LoginCheckPayload $payload): RedirectResponse { $params = []; - if ($request->query->has('perspective')) { - $params['perspective'] = strip_tags($request->query->get('perspective')); + if ($payload->perspective !== null) { + $params['perspective'] = $payload->perspective; } - // just in case the authenticator didn't redirect return new RedirectResponse($this->generateUrl('opendxp_admin_login', $params)); } @@ -204,83 +155,15 @@ public function loginCheckAction(Request $request): RedirectResponse public function lostpasswordAction( Request $request, CsrfProtectionHandler $csrfProtection, - Config $config, - RateLimiterFactory $resetPasswordLimiter, - RouterInterface $router, - GeneralHostResolver $generalHostResolver + LostPasswordPayload $payload, + LostPasswordHandler $lostPassword, ): Response { - $params = $this->buildLoginPageViewParams($config); - $error = null; + $params = $this->loginPageFactory->create($request)->base(); - if ($request->getMethod() === 'POST' && $request->request->has('username')) { - - $username = $request->request->get('username'); - $user = User::getByName($username); - if (!$user instanceof User) { - $error = 'user_unknown'; - } + $result = $lostPassword($payload); - $limiter = $resetPasswordLimiter->create($request->getClientIp()); - - if (false === $limiter->consume(1)->isAccepted()) { - $error = 'user_reset_password_too_many_attempts'; - } - - if (!$error) { - if (!$user->isActive()) { - $error = 'user_inactive'; - } - if (!$user->getEmail()) { - $error = 'user_no_email_address'; - } - if (!$user->getPassword()) { - $error = 'user_no_password'; - } - } - - if (!$error) { - $token = Authentication::generateTokenByUser($user); - - try { - $domain = $generalHostResolver->resolve(['source' => $request]); - if (!$domain) { - throw new Exception('No main domain set in system settings, unable to generate reset password link'); - } - - $context = $router->getContext(); - $context->setHost($domain); - - $loginUrl = $this->generateUrl('opendxp_admin_login_check', [ - 'token' => $token, - 'reset' => 'true', - ], UrlGeneratorInterface::ABSOLUTE_URL); - - $event = new LostPasswordEvent($user, $loginUrl); - $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_LOSTPASSWORD); - - // only send mail if it wasn't prevented in event - if ($event->getSendMail()) { - $mail = Tool::getMail([$user->getEmail()], 'OpenDXP lost password service'); - $mail->setIgnoreDebugMode(true); - $mail->text("Login to OpenDXP and change your password using the following link. This temporary login link will expire in 24 hours: \r\n\r\n" . $loginUrl); - $mail->send(); - } - - // directly return event response - if ($event->hasResponse()) { - return $event->getResponse(); - } - } catch (Exception $e) { - Logger::error('Error sending password recovery email: ' . $e->getMessage()); - $error = 'lost_password_email_error'; - } - } - - if ($error) { - Logger::error('Lost password service: ' . $error); - //to avoid timing based enumeration - usleep(random_int(50, 200)); - } + if ($result->eventResponse !== null) { + return $result->eventResponse; } $csrfProtection->regenerateCsrfToken($request->getSession()); @@ -289,64 +172,30 @@ public function lostpasswordAction( } #[Route('/login/deeplink', name: 'opendxp_admin_login_deeplink')] - public function deeplinkAction(Request $request): Response - { - // check for deeplink - $queryString = $request->server->get('QUERY_STRING'); - - if (preg_match('/(document|asset|object)_(\d+)_([a-z]+)/', $queryString, $deeplink)) { - $deeplink = $deeplink[0]; - $perspective = strip_tags($request->query->get('perspective', '')); - if (strpos($queryString, 'token')) { - $url = $this->dispatchLoginRedirect([ - 'deeplink' => $deeplink, - 'perspective' => $perspective, - ]); - - $url .= '&' . $queryString; - - return $this->redirect($url); - } - - if ($queryString) { - $url = $this->dispatchLoginRedirect([ - 'deeplink' => 'true', - 'perspective' => $perspective, - ]); + public function deeplinkAction( + DeeplinkHandler $handler, + DeeplinkPayload $payload, + ): Response { + $result = $handler($payload); - return $this->render('@OpenDxpAdmin/admin/login/deeplink.html.twig', [ - 'tab' => $deeplink, - 'redirect' => $url, - ]); - } + if ($result->redirectUrl) { + return $this->redirect($result->redirectUrl); } - throw $this->createNotFoundException(); - } - - /** - * @return array{config: Config, pluginCssPaths: string[]} - */ - protected function buildLoginPageViewParams(Config $config): array - { - return [ - 'config' => $config, - 'adminSettings' => AdminConfig::get(), - 'pluginCssPaths' => $this->bundleManager->getCssPaths(), - ]; + return $this->render($result->template, $result->params); } #[Route('/login/2fa', name: 'opendxp_admin_2fa')] - public function twoFactorAuthenticationAction(Request $request, Config $config): Response + public function twoFactorAuthenticationAction(Request $request): Response { - $params = $this->buildLoginPageViewParams($config); + $params = $this->loginPageFactory->create($request)->base(); if ($request->hasSession()) { $session = $request->getSession(); $authException = $session->get(SecurityRequestAttributes::AUTHENTICATION_ERROR); + if ($authException instanceof AuthenticationException) { $session->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR); - $params['error'] = $authException->getMessage(); } } else { @@ -357,87 +206,55 @@ public function twoFactorAuthenticationAction(Request $request, Config $config): } #[Route('/login/2fa-setup', name: 'opendxp_admin_2fa_setup')] - public function twoFactorSetupAuthenticationAction( - Request $request, - Config $config, - GoogleAuthenticatorInterface $twoFactor - ): Response { - $params = $this->buildLoginPageViewParams($config); - $params['setup'] = true; - - $user = $this->getAdminUser(); - $proxyUser = $this->getAdminUser(true); - - if ($request->query->get('error')) { - $params['error'] = $request->query->get('error'); - } - + public function twoFactorSetupAuthenticationAction(Request $request): Response|RedirectResponse + { if ($request->isMethod('post')) { - $secret = $request->getSession()->get('2fa_secret'); - - if (!$secret) { - throw new Exception('2fa secret not found'); - } - - $user->setTwoFactorAuthentication('enabled', true); - $user->setTwoFactorAuthentication('type', 'google'); - $user->setTwoFactorAuthentication('secret', $secret); - - if (!$twoFactor->checkCode($proxyUser, $request->request->get('_auth_code'))) { - return new RedirectResponse($this->generateUrl('opendxp_admin_2fa_setup', ['error' => '2fa_wrong'])); - } + return $this->forward(self::class . '::twoFactorSetupSaveAction'); + } - $user->save(); + return $this->forward(self::class . '::twoFactorSetupGenerateAction', [], $request->query->all()); + } - return new RedirectResponse($this->generateUrl('opendxp_admin_login')); + #[Route('/login/2fa-setup-save', name: 'opendxp_admin_2fa_setup_save')] + public function twoFactorSetupSaveAction( + SaveTwoFactorSetupPayload $payload, + SaveTwoFactorSetupHandler $saveSetup, + ): RedirectResponse { + try { + $saveSetup($payload); + } catch (\Throwable) { + return new RedirectResponse($this->generateUrl('opendxp_admin_2fa_setup', ['error' => '2fa_wrong'])); } - $newSecret = $twoFactor->generateSecret(); + return new RedirectResponse($this->generateUrl('opendxp_admin_login')); + } - $request->getSession()->set('2fa_secret', $newSecret); + #[Route('/login/2fa-setup-generate', name: 'opendxp_admin_2fa_setup_generate')] + public function twoFactorSetupGenerateAction( + Request $request, + GenerateTwoFactorSetupPayload $payload, + GenerateTwoFactorSetupHandler $generateSetup, + ): Response { - $user->setTwoFactorAuthentication('enabled', true); - $user->setTwoFactorAuthentication('type', 'google'); - $user->setTwoFactorAuthentication('secret', $newSecret); + $params = $this->loginPageFactory->create($request)->base(); + $params['setup'] = true; - $url = $twoFactor->getQRContent($proxyUser); + if ($payload->error) { + $params['error'] = $payload->error; + } - $result = Builder::create() - ->writer(new PngWriter()) - ->data($url) - ->size(200) - ->build(); + $result = $generateSetup(); + $params['image'] = $result->qrDataUri; - $params['image'] = $result->getDataUri(); + $request->getSession()->set('2fa_secret', $result->secret); return $this->render('@OpenDxpAdmin/admin/login/two_factor_setup.html.twig', $params); } #[Route('/login/2fa-verify', name: 'opendxp_admin_2fa-verify')] - public function twoFactorAuthenticationVerifyAction(Request $request): void + public function twoFactorAuthenticationVerifyAction(): void { - } - - public function detectBrowser(): bool - { - $supported = false; - $browser = new Browser(); - $browserVersion = (int)$browser->getVersion(); - - if ($browser->getBrowser() == Browser::BROWSER_FIREFOX && $browserVersion >= 72) { - $supported = true; - } - if ($browser->getBrowser() == Browser::BROWSER_CHROME && $browserVersion >= 84) { - $supported = true; - } - if ($browser->getBrowser() == Browser::BROWSER_SAFARI && $browserVersion >= 13.1) { - $supported = true; - } - if ($browser->getBrowser() == Browser::BROWSER_EDGE && $browserVersion >= 90) { - return true; - } - - return $supported; + // handled by firewall } private function dispatchLoginRedirect(array $routeParams = []): string diff --git a/src/Controller/Admin/MiscController.php b/src/Controller/Admin/MiscController.php index 124125bf..0970d4f4 100644 --- a/src/Controller/Admin/MiscController.php +++ b/src/Controller/Admin/MiscController.php @@ -1,5 +1,4 @@ getControllerReferences(); + public function getAvailableControllerReferencesAction( + EmptyPayload $payload, + GetAvailableControllerReferencesHandler $handler, + ): JsonResponse { + $result = $handler($payload); - $result = array_map(fn ($controller) => [ - 'name' => $controller, - ], $controllerReferences); - - return $this->adminJson([ - 'success' => true, - 'data' => $result, - 'total' => count($result), - ]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } #[Route('/get-available-templates', name: 'opendxp_admin_misc_getavailabletemplates', methods: ['GET'])] - public function getAvailableTemplatesAction(ControllerDataProvider $provider): JsonResponse - { - $templates = $provider->getTemplates(); - - sort($templates, SORT_NATURAL | SORT_FLAG_CASE); + public function getAvailableTemplatesAction( + EmptyPayload $payload, + GetAvailableTemplatesHandler $handler, + ): JsonResponse { + $result = $handler($payload); - $result = array_map(static fn ($template) => [ - 'path' => $template, - ], $templates); - - return $this->adminJson([ - 'data' => $result, - ]); + return $this->adminJson(['data' => $result->data]); } #[Route('/json-translations-system', name: 'opendxp_admin_misc_jsontranslationssystem', methods: ['GET'])] - public function jsonTranslationsSystemAction(Request $request, TranslatorInterface $translator): Response - { - $language = $request->query->get('language'); - - /** @var Translator $translator */ - $translator->lazyInitialize('admin', $language); + public function jsonTranslationsSystemAction( + GetJsonTranslationsPayload $payload, + GetJsonTranslationsHandler $handler, + ): Response { + $result = $handler($payload); - $translations = []; - - $fallbackLanguages = []; - if (null !== Locale::getRegion($language)) { - // if language is region specific, add the primary language as fallback - $fallbackLanguages[] = Locale::getPrimaryLanguage($language); - } - if ($language != 'en') { - // add en as a fallback - $fallbackLanguages[] = 'en'; - } - - foreach (['admin', 'admin_ext'] as $domain) { - $translations = array_replace($translations, $translator->getCatalogue($language)->all($domain)); - - foreach ($fallbackLanguages as $fallbackLanguage) { - $translator->lazyInitialize($domain, $fallbackLanguage); - foreach ($translator->getCatalogue($fallbackLanguage)->all($domain) as $key => $value) { - if (empty($translations[$key])) { - $translations[$key] = $value; - } - } - } - } - - $response = new Response('opendxp.system_i18n = ' . $this->encodeJson($translations) . ';'); + $response = new Response('opendxp.system_i18n = ' . $this->encodeJson($result->translations) . ';'); $response->headers->set('Content-Type', 'text/javascript'); return $response; @@ -117,220 +89,141 @@ public function jsonTranslationsSystemAction(Request $request, TranslatorInterfa * @internal */ #[Route('/script-proxy', name: 'opendxp_admin_misc_scriptproxy', methods: ['GET'])] - public function scriptProxyAction(Request $request): Response - { - $storageFile = $request->query->get('storageFile'); - if (!$storageFile) { - throw new InvalidArgumentException('The parameter storageFile is required'); - } - - $fileExtension = pathinfo($storageFile, PATHINFO_EXTENSION); - $storage = Storage::get('admin'); - $scriptsContent = $storage->read($storageFile); - - if (!empty($scriptsContent)) { - $contentType = 'text/javascript'; - if ($fileExtension === 'css') { - $contentType = 'text/css'; - } + public function scriptProxyAction( + ScriptProxyPayload $payload, + ScriptProxyHandler $handler, + ): Response { + $result = $handler($payload); - $lifetime = 86400; + $lifetime = 86400; - $response = new Response($scriptsContent); - $response->headers->set('Cache-Control', 'max-age=' . $lifetime); - $response->headers->set('Pragma', ''); - $response->headers->set('Content-Type', $contentType); - $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT'); + $response = new Response($result->content); + $response->headers->set('Cache-Control', 'max-age=' . $lifetime); + $response->headers->set('Pragma', ''); + $response->headers->set('Content-Type', $result->contentType); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT'); - return $response; - } - - throw $this->createNotFoundException('Scripts not found'); + return $response; } #[Route('/admin-css', name: 'opendxp_admin_misc_admincss', methods: ['GET'])] - public function adminCssAction(Request $request, Config $config): Response - { - // customviews config - $cvData = \OpenDxp\Bundle\AdminBundle\CustomView\Config::get(); - - // languages - $languages = \OpenDxp\Tool::getValidLanguages(); - $adminLanguages = \OpenDxp\Tool\Admin::getLanguages(); - $languages = array_unique([...$languages, ...$adminLanguages]); + public function adminCssAction( + EmptyPayload $payload, + AdminCssHandler $handler, + ): Response { + $result = $handler($payload); $response = $this->render('@OpenDxpAdmin/admin/misc/admin_css.html.twig', [ - 'customviews' => $cvData, - 'adminSettings' => AdminConfig::get(), - 'languages' => $languages, + 'customviews' => $result->customviews, + 'adminSettings' => $result->adminSettings, + 'languages' => $result->languages, ]); + $response->headers->set('Content-Type', 'text/css; charset=UTF-8'); return $response; } - #[Route('/ping', name: 'opendxp_admin_misc_ping', methods: ['GET'])] - public function pingAction(Request $request): JsonResponse - { - $response = [ - 'success' => true, - ]; - - return $this->adminJson($response); - } - #[Route('/available-languages', name: 'opendxp_admin_misc_availablelanguages', methods: ['GET'])] - public function availableLanguagesAction(Request $request): Response - { - $locales = Tool::getSupportedLocales(); - $response = new Response('opendxp.available_languages = ' . $this->encodeJson($locales) . ';'); + public function availableLanguagesAction( + EmptyPayload $payload, + GetAvailableLanguagesHandler $handler, + ): Response { + $result = $handler($payload); + + $response = new Response('opendxp.available_languages = ' . $this->encodeJson($result->locales) . ';'); $response->headers->set('Content-Type', 'text/javascript'); return $response; } #[Route('/get-valid-filename', name: 'opendxp_admin_misc_getvalidfilename', methods: ['GET'])] - public function getValidFilenameAction(Request $request): JsonResponse - { - return $this->adminJson([ - 'filename' => \OpenDxp\Model\Element\Service::getValidKey($request->query->get('value'), $request->query->get('type')), - ]); + public function getValidFilenameAction( + GetValidFilenamePayload $payload, + GetValidFilenameHandler $handler, + ): JsonResponse { + $result = $handler($payload); + + return $this->adminJson(['filename' => $result->filename]); } + #[IsGranted(CorePermission::MaintenanceMode->value)] #[Route('/maintenance', name: 'opendxp_admin_misc_maintenance', methods: ['POST'])] - public function maintenanceAction(Request $request, Tool\MaintenanceModeHelperInterface $maintenanceModeHelper): JsonResponse - { - $this->checkPermission('maintenance_mode'); - - if ($request->query->get('activate')) { - $maintenanceModeHelper->activate($request->getSession()->getId()); - } + public function maintenanceAction( + MaintenancePayload $payload, + MaintenanceHandler $handler, + ): JsonResponse { + $handler($payload); - if ($request->query->get('deactivate')) { - $maintenanceModeHelper->deactivate(); - } - - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/country-list', name: 'opendxp_admin_misc_countrylist', methods: ['GET'])] - public function countryListAction(LocaleServiceInterface $localeService): JsonResponse - { - $countries = $localeService->getDisplayRegions(); - asort($countries); - $options = []; - - foreach ($countries as $short => $translation) { - if (strlen($short) === 2) { - $options[] = [ - 'name' => $translation, - 'code' => $short, - ]; - } - } + public function countryListAction( + EmptyPayload $payload, + GetCountryListHandler $handler, + ): JsonResponse { + $result = $handler($payload); - return $this->adminJson(['data' => $options]); + return $this->adminJson(['data' => $result->data]); } #[Route('/language-list', name: 'opendxp_admin_misc_languagelist', methods: ['GET'])] - public function languageListAction(Request $request): JsonResponse - { - $locales = Tool::getSupportedLocales(); - $options = []; - - foreach ($locales as $short => $translation) { - $options[] = [ - 'name' => $translation, - 'code' => $short, - ]; - } + public function languageListAction( + EmptyPayload $payload, + GetLanguageListHandler $handler, + ): JsonResponse { + $result = $handler($payload); - return $this->adminJson(['data' => $options]); + return $this->adminJson(['data' => $result->data]); } #[Route('/get-language-flag', name: 'opendxp_admin_misc_getlanguageflag', methods: ['GET'])] - public function getLanguageFlagAction(Request $request): BinaryFileResponse - { - $iconPath = AdminTool::getLanguageFlagFile($request->query->get('language')); + public function getLanguageFlagAction( + GetLanguageFlagPayload $payload, + GetLanguageFlagHandler $handler, + ): BinaryFileResponse { + $result = $handler($payload); - $response = new BinaryFileResponse($iconPath); + $response = new BinaryFileResponse($result->iconPath); $response->headers->set('Content-Type', 'image/svg+xml'); return $response; } #[Route('/icon-list', name: 'opendxp_admin_misc_iconlist', methods: ['GET'])] - public function iconListAction(Request $request, ?Profiler $profiler): Response - { + public function iconListAction( + GetIconListPayload $payload, + GetIconListHandler $handler, + ?Profiler $profiler, + ): Response { if ($profiler) { $profiler->disable(); } - $type = $request->query->get('type'); - $publicDir = OPENDXP_WEB_ROOT . '/bundles/opendxpadmin'; - $iconDir = $publicDir . '/img'; - $extraInfo = null; - - $icons = match ($type) { - 'color' => FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'), - 'white' => FileSystemHelper::scanDirectory($iconDir . '/flat-white-icons/'), - 'twemoji' => FileSystemHelper::scanDirectory($iconDir . '/twemoji/'), - 'flags' => $this->getFlags(), - default => [] - }; - - $source = match ($type) { - 'color', 'white' => - 'based on the ' . - 'Material Design Icons', - 'twemoji' => - 'based on the ' . - 'Twemoji icons', - default => '' - }; - - if ($type === 'twemoji') { - $extraInfo = 'ℹ Click on icon with green border to display all its related variants. Click on the letter to display flags with the clicked initial'; - } - - $iconsCss = file_get_contents($publicDir . '/css/icons.css'); - - if ($type === null) { + if ($payload->type === null) { return $this->render('@OpenDxpAdmin/admin/misc/icon_library_reload.html.twig'); } + $result = $handler($payload); + return $this->render('@OpenDxpAdmin/admin/misc/icon_list.html.twig', [ - 'icons' => $icons, - 'iconsCss' => $iconsCss, - 'type' => $type, - 'extraInfo' => $extraInfo, - 'source' => $source, + 'icons' => $result->icons, + 'iconsCss' => $result->iconsCss, + 'type' => $result->type, + 'extraInfo' => $result->extraInfo, + 'source' => $result->source, ]); } - private function getFlags(): array + #[Route('/ping', name: 'opendxp_admin_misc_ping', methods: ['GET'])] + public function pingAction(): JsonResponse { - $locales = Tool::getSupportedLocales(); - $languageOptions = []; - foreach (array_keys($locales) as $short) { - if (!empty($short)) { - $flag = AdminTool::getLanguageFlagFile($short, true, false); - if ($flag) { - $languageOptions[] = $flag; - } - } - } - - $languageOptions = array_unique($languageOptions); - sort($languageOptions); - - return $languageOptions; + return $this->adminJson(ApiResponse::ok()); } #[Route('/test', name: 'opendxp_admin_misc_test')] - public function testAction(Request $request): Response + public function testAction(): Response { return new Response('done'); } diff --git a/src/Controller/Admin/NotificationController.php b/src/Controller/Admin/NotificationController.php index abfaef49..7ca757b3 100644 --- a/src/Controller/Admin/NotificationController.php +++ b/src/Controller/Admin/NotificationController.php @@ -18,16 +18,24 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Model\Element\Service; -use OpenDxp\Model\Notification\Service\NotificationService; -use OpenDxp\Model\Notification\Service\NotificationServiceFilterParser; -use OpenDxp\Model\Notification\Service\UserService; -use OpenDxp\Model\User; +use OpenDxp\Bundle\AdminBundle\Dto\Response\ApiResponse; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\DeleteAllNotifications\DeleteAllNotificationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\DeleteNotification\DeleteNotificationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\FindAllNotifications\FindAllNotificationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\FindAllNotifications\FindAllNotificationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\FindLastUnreadNotifications\FindLastUnreadNotificationsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\FindLastUnreadNotifications\FindLastUnreadNotificationsPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\FindNotification\FindNotificationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\GetRecipients\GetRecipientsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\MarkAsReadNotification\MarkAsReadNotificationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\SendNotification\SendNotificationHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Notification\SendNotification\SendNotificationPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\EmptyPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\Translation\TranslatorInterface; -use UnexpectedValueException; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal @@ -35,163 +43,95 @@ #[Route('/notification')] class NotificationController extends AdminAbstractController { + #[IsGranted(CorePermission::NotificationsSend->value)] #[Route('/recipients', name: 'opendxp_admin_notification_recipients', methods: ['GET'])] - public function recipientsAction(UserService $service, TranslatorInterface $translator): JsonResponse - { - $this->checkPermission('notifications_send'); + public function recipientsAction( + GetRecipientsHandler $getRecipients, + EmptyPayload $payload, + ): JsonResponse { + $result = $getRecipients($payload); - $data = []; - - foreach ($service->findAll($this->getAdminUser()) as $recipient) { - $group = $translator->trans('group', [], 'admin'); - $prefix = $recipient->getType() === 'role' ? $group . ' - ' : ''; - - $data[] = [ - 'id' => $recipient->getId(), - 'text' => $prefix . $recipient->getName(), - ]; - } - - return $this->adminJson($data); + return $this->adminJson($result->data); } + #[IsGranted(CorePermission::NotificationsSend->value)] #[Route('/send', name: 'opendxp_admin_notification_send', methods: ['POST'])] - public function sendAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications_send'); - - $recipientId = $request->request->getInt('recipientId'); - $fromUser = (int) $this->getAdminUser()->getId(); - $title = $request->request->get('title', ''); - $message = $request->request->get('message', ''); - $element = null; - $elementId = $request->request->getInt('elementId'); - $elementType = $request->request->get('elementType'); - - if ($elementId && $elementType) { - $element = Service::getElementById($elementType, $elementId); - } - - if (User::getById($recipientId) instanceof User) { - $service->sendToUser($recipientId, $fromUser, $title, $message, $element); - } else { - $service->sendToGroup($recipientId, $fromUser, $title, $message, $element); - } + public function sendAction( + SendNotificationHandler $sendNotification, + SendNotificationPayload $payload, + ): JsonResponse { + $sendNotification($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/find', name: 'opendxp_admin_notification_find', methods: ['GET'])] - public function findAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); - - $id = $request->query->getInt('id'); - + public function findAction( + FindNotificationHandler $findNotification, + IdQueryPayload $payload, + ): JsonResponse { try { - $notification = $service->findAndMarkAsRead($id, $this->getAdminUser()->getId()); - } catch (UnexpectedValueException) { - return $this->adminJson( - [ - 'success' => false, - ] - ); - } - - $data = $service->format($notification); + $result = $findNotification($payload); - return $this->adminJson([ - 'success' => true, - 'data' => $data, - ]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data])); + } catch (\Throwable) { + return $this->adminJson(['success' => false]); + } } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/find-all', name: 'opendxp_admin_notification_findall', methods: ['POST'])] - public function findAllAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); - - $filter = ['recipient' => (int) $this->getAdminUser()->getId()]; - $parser = new NotificationServiceFilterParser($request); + public function findAllAction( + FindAllNotificationsHandler $findAllNotifications, + FindAllNotificationsPayload $payload, + ): JsonResponse { + $result = $findAllNotifications($payload); - foreach ($parser->parse() as $key => $val) { - $filter[$key] = $val; - } - - $options = [ - 'offset' => $request->request->getInt('start'), - 'limit' => $request->request->getInt('limit', 40), - ]; - - $result = $service->findAll($filter, $options); - - $data = []; - - foreach ($result['data'] as $notification) { - $data[] = $service->format($notification); - } - - return $this->adminJson([ - 'success' => true, - 'total' => $result['total'], - 'data' => $data, - ]); + return $this->adminJson(ApiResponse::ok(['total' => $result->total, 'data' => $result->data])); } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/find-last-unread', name: 'opendxp_admin_notification_findlastunread', methods: ['GET'])] - public function findLastUnreadAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); - - $user = $this->getAdminUser(); - $lastUpdate = $request->query->getInt('lastUpdate', time()); - $result = $service->findLastUnread((int) $user->getId(), $lastUpdate); - $unread = $service->countAllUnread((int) $user->getId()); + public function findLastUnreadAction( + FindLastUnreadNotificationsHandler $findLastUnread, + FindLastUnreadNotificationsPayload $payload, + ): JsonResponse { + $result = $findLastUnread($payload); - $data = []; - - foreach ($result['data'] as $notification) { - $data[] = $service->format($notification); - } - - return $this->adminJson([ - 'success' => true, - 'total' => $result['total'], - 'data' => $data, - 'unread' => $unread, - ]); + return $this->adminJson(ApiResponse::ok(['total' => $result->total, 'data' => $result->data, 'unread' => $result->unread])); } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/mark-as-read', name: 'opendxp_admin_notification_markasread', methods: ['PUT'])] - public function markAsReadAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); + public function markAsReadAction( + MarkAsReadNotificationHandler $handler, + IdQueryPayload $payload, + ): JsonResponse { + $handler($payload); - $id = $request->query->getInt('id'); - $service->findAndMarkAsRead($id, $this->getAdminUser()->getId()); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/delete', name: 'opendxp_admin_notification_delete', methods: ['DELETE'])] - public function deleteAction(Request $request, NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); - - $id = $request->query->getInt('id'); - $service->delete($id, $this->getAdminUser()->getId()); + public function deleteAction( + DeleteNotificationHandler $handler, + IdQueryPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Notifications->value)] #[Route('/delete-all', name: 'opendxp_admin_notification_deleteall', methods: ['DELETE'])] - public function deleteAllAction(NotificationService $service): JsonResponse - { - $this->checkPermission('notifications'); - - $user = $this->getAdminUser(); - $service->deleteAll((int) $user->getId()); + public function deleteAllAction( + DeleteAllNotificationsHandler $deleteAllNotifications, + EmptyPayload $payload, + ): JsonResponse { + $deleteAllNotifications($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } } diff --git a/src/Controller/Admin/PortalController.php b/src/Controller/Admin/PortalController.php index 78a6ede9..6874f875 100644 --- a/src/Controller/Admin/PortalController.php +++ b/src/Controller/Admin/PortalController.php @@ -1,5 +1,4 @@ dashboardHelper->getAllDashboards(); + public function dashboardListAction( + GetDashboardListHandler $getDashboardList, + EmptyPayload $payload, + ): JsonResponse { + $result = $getDashboardList($payload); - $data = []; - foreach (array_keys($dashboards) as $key) { - if ($key !== 'welcome') { - $data[] = $key; - } - } - - return $this->adminJson($data); + return $this->adminJson($result->dashboards); } #[Route('/create-dashboard', name: 'opendxp_admin_portal_createdashboard', methods: ['POST'])] - public function createDashboardAction(Request $request): JsonResponse - { - $dashboards = $this->dashboardHelper->getAllDashboards(); - $key = trim($request->request->get('key', '')); - if (isset($dashboards[$key])) { - return $this->adminJson(['success' => false, 'message' => 'name_already_in_use']); + public function createDashboardAction( + CreateDashboardHandler $createDashboard, + CreateDashboardPayload $payload, + ): JsonResponse { + try { + $createDashboard($payload); + } catch (\InvalidArgumentException $e) { + return $this->adminJson(ApiResponse::error($e->getMessage())); } - if (!empty($key)) { - $this->dashboardHelper->saveDashboard($key); - - return $this->adminJson(['success' => true]); - } - - return $this->adminJson(['success' => false, 'message' => 'empty']); + return $this->adminJson(ApiResponse::ok()); } #[Route('/delete-dashboard', name: 'opendxp_admin_portal_deletedashboard', methods: ['DELETE'])] - public function deleteDashboardAction(Request $request): JsonResponse - { - $key = $request->request->get('key'); - $this->dashboardHelper->deleteDashboard($key); + public function deleteDashboardAction( + DeleteDashboardHandler $deleteDashboard, + DeleteDashboardPayload $payload, + ): JsonResponse { + $deleteDashboard($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/get-configuration', name: 'opendxp_admin_portal_getconfiguration', methods: ['GET'])] - public function getConfigurationAction(Request $request): JsonResponse - { - $config = $this->dashboardHelper->getDashboard($request->query->get('key')); + public function getConfigurationAction( + GetDashboardConfigurationHandler $getDashboardConfiguration, + GetDashboardConfigurationPayload $payload, + ): JsonResponse { + $result = $getDashboardConfiguration($payload); - return $this->adminJson($config); + return $this->adminJson($result->config); } #[Route('/remove-widget', name: 'opendxp_admin_portal_removewidget', methods: ['DELETE'])] - public function removeWidgetAction(Request $request): JsonResponse - { - $dashboardId = $request->request->get('key'); - $config = $this->dashboardHelper->getDashboard($dashboardId); - - $newConfig = [[], []]; - $colCount = 0; - - $currentId = $request->request->has('id') ? (int) $request->request->get('id') : null; - - foreach ($config['positions'] as $col) { - foreach ($col as $row) { - if ($row['id'] !== $currentId) { - $newConfig[$colCount][] = $row; - } - } - $colCount++; - } + public function removeWidgetAction( + RemoveWidgetHandler $removeWidget, + RemoveWidgetPayload $payload, + ): JsonResponse { + $removeWidget($payload); - $config['positions'] = $newConfig; - - $this->dashboardHelper->saveDashboard($dashboardId, $config); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/add-widget', name: 'opendxp_admin_portal_addwidget', methods: ['POST'])] - public function addWidgetAction(Request $request): JsonResponse - { - $dashboardId = $request->request->get('key'); - $config = $this->dashboardHelper->getDashboard($dashboardId); - - $nextId = 0; - foreach ($config['positions'] as $col) { - foreach ($col as $row) { - $nextId = ($row['id'] > $nextId ? $row['id'] : $nextId); - } - } + public function addWidgetAction( + AddWidgetHandler $addWidget, + AddWidgetPayload $payload, + ): JsonResponse { + $result = $addWidget($payload); - $nextId += 1; - $config['positions'][0][] = [ - 'id' => $nextId, - 'type' => $request->request->get('type'), - 'config' => null, - ]; - - $this->dashboardHelper->saveDashboard($dashboardId, $config); - - return $this->adminJson(['success' => true, 'id' => $nextId]); + return $this->adminJson(ApiResponse::ok(['id' => $result->id])); } #[Route('/reorder-widget', name: 'opendxp_admin_portal_reorderwidget', methods: ['PUT'])] - public function reorderWidgetAction(Request $request): JsonResponse - { - $dashboardId = $request->request->get('key'); - $config = $this->dashboardHelper->getDashboard($dashboardId); - - $newConfig = [[], []]; - $colCount = 0; - $toMove = null; - - $currentId = $request->request->has('id') ? (int) $request->request->get('id') : null; - - foreach ($config['positions'] as $col) { - foreach ($col as $row) { - if ($row['id'] !== $currentId) { - $newConfig[$colCount][] = $row; - } else { - $toMove = $row; - } - } - $colCount++; - } - - array_splice($newConfig[$request->request->get('column')], $request->request->getInt('row'), 0, [$toMove]); - - $config['positions'] = $newConfig; - - $this->dashboardHelper->saveDashboard($dashboardId, $config); + public function reorderWidgetAction( + ReorderWidgetHandler $reorderWidget, + ReorderWidgetPayload $payload, + ): JsonResponse { + $reorderWidget($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/update-portlet-config', name: 'opendxp_admin_portal_updateportletconfig', methods: ['PUT'])] - public function updatePortletConfigAction(Request $request): JsonResponse - { - $key = $request->request->get('key'); + public function updatePortletConfigAction( + UpdatePortletConfigHandler $updatePortletConfig, + UpdatePortletConfigPayload $payload, + ): JsonResponse { + $updatePortletConfig($payload); - $currentId = $request->request->has('id') ? (int) $request->request->get('id') : null; - $configuration = $request->request->get('config'); - - $dashboard = $this->dashboardHelper->getDashboard($key); - foreach ($dashboard['positions'] as &$col) { - foreach ($col as &$portlet) { - if ($portlet['id'] === $currentId) { - $portlet['config'] = $configuration; - - break; - } - } - } - - $this->dashboardHelper->saveDashboard($key, $dashboard); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/portlet-modified-documents', name: 'opendxp_admin_portal_portletmodifieddocuments', methods: ['GET'])] - public function portletModifiedDocumentsAction(Request $request): JsonResponse - { - $list = Document::getList([ - 'limit' => 10, - 'order' => 'DESC', - 'orderKey' => 'modificationDate', - 'condition' => "userModification = '".$this->getAdminUser()->getId()."'", - ]); - - $response = []; - $response['documents'] = []; + public function portletModifiedDocumentsAction( + GetModifiedDocumentsHandler $getModifiedDocuments, + EmptyPayload $payload, + ): JsonResponse { + $result = $getModifiedDocuments($payload); - foreach ($list as $doc) { - if ($doc->isAllowed('view')) { - $response['documents'][] = [ - 'id' => $doc->getId(), - 'type' => $doc->getType(), - 'path' => $doc->getRealFullPath(), - 'date' => $doc->getModificationDate(), - ]; - } - } - - return $this->adminJson($response); + return $this->adminJson(['documents' => $result->documents]); } #[Route('/portlet-modified-assets', name: 'opendxp_admin_portal_portletmodifiedassets', methods: ['GET'])] - public function portletModifiedAssetsAction(Request $request): JsonResponse - { - $list = Asset::getList([ - 'limit' => 10, - 'order' => 'DESC', - 'orderKey' => 'modificationDate', - 'condition' => "userModification = '".$this->getAdminUser()->getId()."'", - ]); - - $response = []; - $response['assets'] = []; - - foreach ($list as $doc) { - /** - * @var Asset $doc - */ - if ($doc->isAllowed('view')) { - $response['assets'][] = [ - 'id' => $doc->getId(), - 'type' => $doc->getType(), - 'path' => $doc->getRealFullPath(), - 'date' => $doc->getModificationDate(), - ]; - } - } + public function portletModifiedAssetsAction( + GetModifiedAssetsHandler $getModifiedAssets, + EmptyPayload $payload, + ): JsonResponse { + $result = $getModifiedAssets($payload); - return $this->adminJson($response); + return $this->adminJson(['assets' => $result->assets]); } #[Route('/portlet-modified-objects', name: 'opendxp_admin_portal_portletmodifiedobjects', methods: ['GET'])] - public function portletModifiedObjectsAction(Request $request): JsonResponse - { - $list = DataObject::getList([ - 'limit' => 10, - 'order' => 'DESC', - 'orderKey' => 'modificationDate', - 'condition' => "userModification = '".$this->getAdminUser()->getId()."'", - ]); - - $response = []; - $response['objects'] = []; - - foreach ($list as $object) { - if ($object->isAllowed('view')) { - $response['objects'][] = [ - 'id' => $object->getId(), - 'type' => $object->getType(), - 'path' => $object->getRealFullPath(), - 'date' => $object->getModificationDate(), - ]; - } - } + public function portletModifiedObjectsAction( + GetModifiedObjectsHandler $getModifiedObjects, + EmptyPayload $payload, + ): JsonResponse { + $result = $getModifiedObjects($payload); - return $this->adminJson($response); + return $this->adminJson(['objects' => $result->objects]); } #[Route('/portlet-modification-statistics', name: 'opendxp_admin_portal_portletmodificationstatistics', methods: ['GET'])] - public function portletModificationStatisticsAction(Request $request): JsonResponse - { - $db = \OpenDxp\Db::get(); - - $days = 31; - $startDate = mktime(23, 59, 59, (int) date('m'), (int) date('d'), (int) date('Y')); - - $data = []; - - for ($i = 0; $i < $days; $i++) { - // documents - $end = $startDate - ($i * 86400); - $start = $end - 86399; - - $o = $db->fetchOne( - 'SELECT COUNT(*) AS count FROM objects WHERE modificationDate > ? AND modificationDate < ?', - [$start, $end] - ); - $a = $db->fetchOne( - 'SELECT COUNT(*) AS count FROM assets WHERE modificationDate > ? AND modificationDate < ?', - [$start, $end] - ); - $d = $db->fetchOne( - 'SELECT COUNT(*) AS count FROM documents WHERE modificationDate > ? AND modificationDate < ?', - [$start, $end] - ); - - $date = new DateTime(); - $date->setTimestamp($start); - - $data[] = [ - 'timestamp' => $start, - 'datetext' => $date->format('Y-m-d'), - 'objects' => (int) $o, - 'documents' => (int) $d, - 'assets' => (int) $a, - ]; - } - - $data = array_reverse($data); - - return $this->adminJson(['data' => $data]); - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } + public function portletModificationStatisticsAction( + GetModificationStatisticsHandler $getModificationStatistics, + EmptyPayload $payload, + ): JsonResponse { + $result = $getModificationStatistics($payload); - $this->dashboardHelper = new Dashboard($this->getAdminUser()); + return $this->adminJson(['data' => $result->data]); } } diff --git a/src/Controller/Admin/RecyclebinController.php b/src/Controller/Admin/RecyclebinController.php index 82c1d57c..b5a77c13 100644 --- a/src/Controller/Admin/RecyclebinController.php +++ b/src/Controller/Admin/RecyclebinController.php @@ -1,5 +1,4 @@ value)] #[Route('/recyclebin/list', name: 'opendxp_admin_recyclebin_list', methods: ['POST'])] - public function listAction(Request $request): JsonResponse - { - if ($request->query->get('xaction') === 'destroy') { - $item = Recyclebin\Item::getById(\OpenDxp\Bundle\AdminBundle\Helper\QueryParams::getRecordIdForGridRequest($request->request->get('data'))); - - if ($item) { - $item->delete(); - } - - return $this->adminJson(['success' => true, 'data' => []]); + public function listAction( + Request $request, + RecyclebinPayload $payload, + ListRecyclebinHandler $listRecyclebin, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::listDestroyAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; } - $db = \OpenDxp\Db::get(); - - $list = new Recyclebin\Item\Listing(); - $list->setLimit((int) $request->request->get('limit', 50)); - $list->setOffset((int) $request->request->get('start', 0)); - - $list->setOrderKey('date'); - $list->setOrder('DESC'); - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($request->request->all()); - if ($sortingSettings['orderKey']) { - $list->setOrderKey($sortingSettings['orderKey']); - $list->setOrder($sortingSettings['order']); - } - - $conditionFilters = []; - - if ($request->request->get('filterFullText')) { - $conditionFilters[] = '`path` LIKE ' . $list->quote('%'. $list->escapeLike($request->request->get('filterFullText')) .'%'); - } + $result = $listRecyclebin($payload); - $filters = $request->request->get('filter'); - if ($filters) { - $filters = $this->decodeJson($filters); - - foreach ($filters as $filter) { - $operator = '='; - - $filterField = $filter['property']; - $filterOperator = $filter['operator']; - - if ($filter['type'] === 'string') { - $operator = 'LIKE'; - } elseif ($filter['type'] === 'numeric') { - if ($filterOperator === 'lt') { - $operator = '<'; - } elseif ($filterOperator === 'gt') { - $operator = '>'; - } elseif ($filterOperator === 'eq') { - $operator = '='; - } - } elseif ($filter['type'] === 'date') { - if ($filterOperator === 'lt') { - $operator = '<'; - } elseif ($filterOperator === 'gt') { - $operator = '>'; - } elseif ($filterOperator === 'eq') { - $operator = '='; - } - $filter['value'] = strtotime($filter['value']); - } elseif ($filter['type'] === 'list') { - $operator = '='; - } elseif ($filter['type'] === 'boolean') { - $operator = '='; - $filter['value'] = (int) $filter['value']; - } - // system field - $value = ($filter['value'] ?? ''); - if ($operator === 'LIKE') { - $value = '%' . $value . '%'; - } - - $field = $db->quoteIdentifier($filterField); - if (($filter['field'] ?? false) === 'fullpath') { - $field = 'CONCAT(`path`,filename)'; - } - - if ($filter['type'] === 'date' && $operator === '=') { - $maxTime = $value + (86400 - 1); //specifies the top point of the range used in the condition - $condition = $field . ' BETWEEN ' . $db->quote($value) . ' AND ' . $db->quote($maxTime); - $conditionFilters[] = $condition; - } else { - $conditionFilters[] = $field . $operator . ' ' . $db->quote($value); - } - } - } - - if ($conditionFilters !== []) { - $condition = implode(' AND ', $conditionFilters); - $list->setCondition($condition); - } - - $items = $list->load(); - $data = []; - foreach ($items as $item) { - $data[] = $item->getObjectVars(); - } + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); + } - return $this->adminJson(['data' => $data, 'success' => true, 'total' => $list->getTotalCount()]); + #[IsGranted(CorePermission::Recyclebin->value)] + #[Route('/recyclebin/list-destroy', name: 'opendxp_admin_recyclebin_list_destroy', methods: ['POST'])] + public function listDestroyAction( + RecyclebinPayload $payload, + DeleteRecyclebinItemHandler $deleteRecyclebinItem, + ): JsonResponse { + $deleteRecyclebinItem($payload); + return $this->adminJson(ApiResponse::ok(['data' => []])); } + #[IsGranted(CorePermission::Recyclebin->value)] #[Route('/recyclebin/restore', name: 'opendxp_admin_recyclebin_restore', methods: ['POST'])] - public function restoreAction(Request $request): JsonResponse - { - $item = Recyclebin\Item::getById((int) $request->request->get('id')); - if (!$item) { - throw $this->createNotFoundException(); - } - $item->restore(); + public function restoreAction( + RestoreRecyclebinItemPayload $payload, + RestoreRecyclebinItemHandler $restoreRecyclebinItem, + ): JsonResponse { + $restoreRecyclebinItem($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Recyclebin->value)] #[Route('/recyclebin/flush', name: 'opendxp_admin_recyclebin_flush', methods: ['DELETE'])] - public function flushAction(): JsonResponse - { - $bin = new Element\Recyclebin(); - $bin->flush(); + public function flushAction( + EmptyPayload $payload, + FlushRecyclebinHandler $flushRecyclebin, + ): JsonResponse { + $flushRecyclebin($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/recyclebin/add', name: 'opendxp_admin_recyclebin_add', methods: ['POST'])] - public function addAction(Request $request): JsonResponse - { - try { - $element = Element\Service::getElementById($request->request->get('type'), $request->request->getInt('id')); - - if ($element) { - $list = $element::getList(['unpublished' => true]); - $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($element->getRealFullPath()) . '/%')); - $children = $list->getTotalCount(); - - if ($children <= 100) { - Recyclebin\Item::create($element, $this->getAdminUser()); - } - } - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } + public function addAction( + AddToRecyclebinPayload $payload, + AddToRecyclebinHandler $addToRecyclebin, + ): JsonResponse { + $addToRecyclebin($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } public function onKernelControllerEvent(ControllerEvent $event): void @@ -185,12 +113,8 @@ public function onKernelControllerEvent(ControllerEvent $event): void return; } - // recyclebin actions might take some time (save & restore) $timeout = 600; // 10 minutes @ini_set('max_execution_time', (string) $timeout); set_time_limit($timeout); - - // check permissions - $this->checkActionPermission($event, 'recyclebin', ['addAction']); } } diff --git a/src/Controller/Admin/Settings/ThumbnailController.php b/src/Controller/Admin/Settings/ThumbnailController.php new file mode 100644 index 00000000..fde54d7c --- /dev/null +++ b/src/Controller/Admin/Settings/ThumbnailController.php @@ -0,0 +1,91 @@ +value)] +class ThumbnailController extends AdminAbstractController +{ + #[Route('/settings/thumbnail-tree', name: 'opendxp_admin_settings_thumbnailtree', methods: ['GET', 'POST'])] + public function thumbnailTreeAction(GetThumbnailTreeHandler $getThumbnailTree): JsonResponse + { + $result = $getThumbnailTree(); + + return $this->adminJson($result->nodes); + } + + #[Route('/settings/thumbnail-downloadable', name: 'opendxp_admin_settings_thumbnaildownloadable', methods: ['GET'])] + public function thumbnailDownloadableAction(GetDownloadableThumbnailsHandler $getDownloadableThumbnails): JsonResponse + { + $result = $getDownloadableThumbnails(); + + return $this->adminJson($result->thumbnails); + } + + #[Route('/settings/thumbnail-add', name: 'opendxp_admin_settings_thumbnailadd', methods: ['POST'])] + public function thumbnailAddAction(AddThumbnailPayload $payload, AddThumbnailHandler $addThumbnail): JsonResponse + { + $result = $addThumbnail($payload); + + return $this->adminJson(ApiResponse::fromBool($result->created, ['id' => $result->id])); + } + + #[Route('/settings/thumbnail-delete', name: 'opendxp_admin_settings_thumbnaildelete', methods: ['DELETE'])] + public function thumbnailDeleteAction(DeleteThumbnailPayload $payload, DeleteThumbnailHandler $deleteThumbnail): JsonResponse + { + $deleteThumbnail($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/settings/thumbnail-get', name: 'opendxp_admin_settings_thumbnailget', methods: ['GET'])] + public function thumbnailGetAction( + #[MapQueryParameter] string $name, + GetThumbnailHandler $getThumbnail, + ): JsonResponse { + $result = $getThumbnail($name); + + return $this->adminJson($result->data); + } + + #[Route('/settings/thumbnail-update', name: 'opendxp_admin_settings_thumbnailupdate', methods: ['PUT'])] + public function thumbnailUpdateAction(UpdateThumbnailPayload $payload, UpdateThumbnailHandler $updateThumbnail): JsonResponse + { + $updateThumbnail($payload); + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/Settings/VideoThumbnailController.php b/src/Controller/Admin/Settings/VideoThumbnailController.php new file mode 100644 index 00000000..1baf77e7 --- /dev/null +++ b/src/Controller/Admin/Settings/VideoThumbnailController.php @@ -0,0 +1,107 @@ +value)] +class VideoThumbnailController extends AdminAbstractController +{ + #[Route('/settings/video-thumbnail-adapter-check', name: 'opendxp_admin_settings_videothumbnailadaptercheck', methods: ['GET'])] + public function videoThumbnailAdapterCheckAction(TranslatorInterface $translator): Response + { + $content = ''; + + if (!\OpenDxp\Video::isAvailable()) { + $content = '' . + $translator->trans('php_cli_binary_and_or_ffmpeg_binary_setting_is_missing', [], 'admin') . + ''; + } + + return new Response($content); + } + + #[Route('/settings/video-thumbnail-tree', name: 'opendxp_admin_settings_videothumbnailtree', methods: ['GET', 'POST'])] + public function videoThumbnailTreeAction(GetVideoThumbnailTreeHandler $getVideoThumbnailTree): JsonResponse + { + $result = $getVideoThumbnailTree(); + + return $this->adminJson($result->nodes); + } + + #[Route('/settings/video-thumbnail-list', name: 'opendxp_admin_settings_videothumbnail_list', methods: ['GET'])] + public function videoThumbnailListAction(GetVideoThumbnailListHandler $getVideoThumbnailList): JsonResponse + { + $result = $getVideoThumbnailList(); + + return $this->adminJson($result->thumbnails); + } + + #[Route('/settings/video-thumbnail-add', name: 'opendxp_admin_settings_videothumbnailadd', methods: ['POST'])] + public function videoThumbnailAddAction(AddVideoThumbnailPayload $payload, AddVideoThumbnailHandler $addVideoThumbnail): JsonResponse + { + $result = $addVideoThumbnail($payload); + + return $this->adminJson(ApiResponse::fromBool($result->created, ['id' => $result->id])); + } + + #[Route('/settings/video-thumbnail-delete', name: 'opendxp_admin_settings_videothumbnaildelete', methods: ['DELETE'])] + public function videoThumbnailDeleteAction(DeleteVideoThumbnailPayload $payload, DeleteVideoThumbnailHandler $deleteVideoThumbnail): JsonResponse + { + $deleteVideoThumbnail($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/settings/video-thumbnail-get', name: 'opendxp_admin_settings_videothumbnailget', methods: ['GET'])] + public function videoThumbnailGetAction( + #[MapQueryParameter] string $name, + GetVideoThumbnailHandler $getVideoThumbnail, + ): JsonResponse { + $result = $getVideoThumbnail($name); + + return $this->adminJson($result->data); + } + + #[Route('/settings/video-thumbnail-update', name: 'opendxp_admin_settings_videothumbnailupdate', methods: ['PUT'])] + public function videoThumbnailUpdateAction(UpdateVideoThumbnailPayload $payload, UpdateVideoThumbnailHandler $updateVideoThumbnail): JsonResponse + { + $updateVideoThumbnail($payload); + + return $this->adminJson(ApiResponse::ok()); + } +} diff --git a/src/Controller/Admin/SettingsController.php b/src/Controller/Admin/SettingsController.php index 1195d12a..429b9958 100644 --- a/src/Controller/Admin/SettingsController.php +++ b/src/Controller/Admin/SettingsController.php @@ -1,4 +1,5 @@ query->has('white')) { - $logo = OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/logo-claim-white.svg'; - } else { - $logo = OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/logo-claim-gray.svg'; - } + $result = $handler($payload); - $stream = fopen($logo, 'rb'); - - $storage = Tool\Storage::get('admin'); - if ($storage->fileExists(self::CUSTOM_LOGO_PATH)) { - try { - $mime = $storage->mimeType(self::CUSTOM_LOGO_PATH); - $stream = $storage->readStream(self::CUSTOM_LOGO_PATH); - } catch (Exception) { - // do nothing - } - } - - return new StreamedResponse(function () use ($stream): void { - fpassthru($stream); + return new StreamedResponse(static function () use ($result): void { + fpassthru($result->stream); }, 200, [ - 'Content-Type' => $mime, + 'Content-Type' => $result->mime, 'Content-Security-Policy' => "script-src 'none'", ]); } - /** - * @throws Exception - */ #[Route('/upload-custom-logo', name: 'opendxp_admin_settings_uploadcustomlogo', methods: ['POST'])] - public function uploadCustomLogoAction(Request $request): JsonResponse + public function uploadCustomLogoAction(Request $request, UploadCustomLogoHandler $handler): JsonResponse { $logoFile = $request->files->get('Filedata'); - if (!$logoFile instanceof UploadedFile - || !in_array($logoFile->guessExtension(), ['svg', 'png', 'jpg']) - ) { - throw new Exception('Unsupported file format.'); + if (!$logoFile instanceof UploadedFile) { + throw new BadRequestHttpException('No file uploaded.'); } - $storage = Tool\Storage::get('admin'); - $storage->writeStream(self::CUSTOM_LOGO_PATH, fopen($logoFile->getPathname(), 'rb')); - - // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in - // Ext.form.Action.Submit and mark the submission as failed + $handler($logoFile->getPathname(), $logoFile->guessExtension() ?? ''); - $response = $this->adminJson(['success' => true]); + $response = $this->adminJson(ApiResponse::ok()); $response->headers->set('Content-Type', 'text/html'); return $response; } #[Route('/delete-custom-logo', name: 'opendxp_admin_settings_deletecustomlogo', methods: ['DELETE'])] - public function deleteCustomLogoAction(Request $request): JsonResponse + public function deleteCustomLogoAction(DeleteCustomLogoHandler $handler): JsonResponse { - if (Tool\Storage::get('admin')->fileExists(self::CUSTOM_LOGO_PATH)) { - Tool\Storage::get('admin')->delete(self::CUSTOM_LOGO_PATH); - } + $handler(); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * Used by the predefined metadata grid - */ + #[IsGranted(CorePermission::AssetMetadata->value)] #[Route('/predefined-metadata', name: 'opendxp_admin_settings_metadata', methods: ['POST'])] - public function metadataAction(Request $request): JsonResponse - { - $this->checkPermission('asset_metadata'); - - if ($request->request->has('data')) { - if ($request->query->get('xaction') === 'destroy') { - $data = $this->decodeJson($request->request->get('data')); - $id = $data['id']; - $metadata = Metadata\Predefined::getById($id); - if (!$metadata->isWriteable()) { - throw new ConfigWriteException(); - } - $metadata->delete(); - - return $this->adminJson(['success' => true, 'data' => []]); - } - - if ($request->query->get('xaction') === 'update') { - $data = $this->decodeJson($request->request->get('data')); - // save type - $metadata = Metadata\Predefined::getById($data['id']); - if (!$metadata->isWriteable()) { - throw new ConfigWriteException(); - } - $metadata->setValues($data); - $existingItem = Metadata\Predefined\Listing::getByKeyAndLanguage($metadata->getName(), $metadata->getLanguage(), $metadata->getTargetSubtype()); - if ($existingItem && $existingItem->getId() !== $metadata->getId()) { - return $this->adminJson(['message' => 'predefined_metadata_definitions_error_name_exists_msg', 'success' => false]); - } - $metadata->minimize(); - $metadata->save(); - $metadata->expand(); - $responseData = $metadata->getObjectVars(); - $responseData['writeable'] = $metadata->isWriteable(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - - if ($request->query->get('xaction') === 'create') { - if (!(new Metadata\Predefined())->isWriteable()) { - throw new ConfigWriteException(); - } - $data = $this->decodeJson($request->request->get('data')); - unset($data['id']); - // save type - $metadata = Metadata\Predefined::create(); - $metadata->setValues($data); - $existingItem = Metadata\Predefined\Listing::getByKeyAndLanguage($metadata->getName(), $metadata->getLanguage(), $metadata->getTargetSubtype()); - if ($existingItem) { - return $this->adminJson(['message' => 'rule_violation', 'success' => false]); - } - $metadata->save(); - $responseData = $metadata->getObjectVars(); - $responseData['writeable'] = $metadata->isWriteable(); + public function metadataAction( + Request $request, + PredefinedMetadataPayload $payload, + GetPredefinedMetadataListHandler $getPredefinedMetadataList, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::metadataDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::metadataUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::metadataCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - } else { - // get list of types - $list = new Metadata\Predefined\Listing(); + $result = $getPredefinedMetadataList($payload); - if ($filter = $request->request->get('filter')) { - $list->setFilter(function (Metadata\Predefined $predefined) use ($filter) { - foreach ($predefined->getObjectVars() as $value) { - if (stripos((string)$value, (string) $filter) !== false) { - return true; - } - } + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); + } - return false; - }); - } + #[IsGranted(CorePermission::AssetMetadata->value)] + #[Route('/predefined-metadata-destroy', name: 'opendxp_admin_settings_metadata_destroy', methods: ['POST'])] + public function metadataDestroyAction( + PredefinedMetadataPayload $payload, + DeletePredefinedMetadataHandler $deletePredefinedMetadata, + ): JsonResponse { + $deletePredefinedMetadata($payload); - $properties = []; - foreach ($list->getDefinitions() as $metadata) { - $metadata->expand(); - $data = $metadata->getObjectVars(); - $data['writeable'] = $metadata->isWriteable(); - $properties[] = $data; - } + return $this->adminJson(ApiResponse::ok(['data' => []])); + } - return $this->adminJson(['data' => $properties, 'success' => true, 'total' => $list->getTotalCount()]); - } + #[IsGranted(CorePermission::AssetMetadata->value)] + #[Route('/predefined-metadata-update', name: 'opendxp_admin_settings_metadata_update', methods: ['POST'])] + public function metadataUpdateAction( + PredefinedMetadataPayload $payload, + UpdatePredefinedMetadataHandler $updatePredefinedMetadata, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $updatePredefinedMetadata($payload)->data])); + } - return $this->adminJson(['success' => false]); + #[IsGranted(CorePermission::AssetMetadata->value)] + #[Route('/predefined-metadata-create', name: 'opendxp_admin_settings_metadata_create', methods: ['POST'])] + public function metadataCreateAction( + PredefinedMetadataPayload $payload, + CreatePredefinedMetadataHandler $createPredefinedMetadata, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $createPredefinedMetadata($payload)->data])); } #[Route('/get-predefined-metadata', name: 'opendxp_admin_settings_getpredefinedmetadata', methods: ['GET'])] - public function getPredefinedMetadataAction(Request $request): JsonResponse - { - $type = $request->query->get('type'); - $subType = $request->query->get('subType'); - $group = $request->query->get('group'); - $list = Metadata\Predefined\Listing::getByTargetType($type, [$subType]); - $result = []; - foreach ($list as $item) { - $itemGroup = $item->getGroup() ?? ''; - if ($group === 'default' || $group === $itemGroup) { - $item->expand(); - $data = $item->getObjectVars(); - $data['writeable'] = $item->isWriteable(); - $result[] = $data; - } - } - - return $this->adminJson(['data' => $result, 'success' => true]); + public function getPredefinedMetadataAction( + GetFilteredPredefinedMetadataHandler $handler, + #[MapQueryParameter] ?string $type = null, + #[MapQueryParameter] ?string $subType = null, + #[MapQueryParameter] ?string $group = null, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $handler($type, $subType, $group)->data])); } + #[IsGranted(CorePermission::PredefinedProperties->value)] #[Route('/properties', name: 'opendxp_admin_settings_properties', methods: ['POST'])] - public function propertiesAction(Request $request): JsonResponse - { - $this->checkPermission('predefined_properties'); - - if ($request->request->has('data')) { - if ($request->query->get('xaction') === 'destroy') { - $data = $this->decodeJson($request->request->get('data')); - $id = $data['id']; - $property = Property\Predefined::getById($id); - if (!$property->isWriteable()) { - throw new ConfigWriteException(); - } - $property->delete(); - - return $this->adminJson(['success' => true, 'data' => []]); - } - - if ($request->query->get('xaction') === 'update') { - $data = $this->decodeJson($request->request->get('data')); - // save type - $property = Property\Predefined::getById($data['id']); - if (!$property->isWriteable()) { - throw new ConfigWriteException(); - } - if (is_array($data['ctype'])) { - $data['ctype'] = implode(',', $data['ctype']); - } - $property->setValues($data); - $property->save(); - $responseData = $property->getObjectVars(); - $responseData['writeable'] = $property->isWriteable(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - - if ($request->query->get('xaction') === 'create') { - if (!(new Property\Predefined())->isWriteable()) { - throw new ConfigWriteException(); - } - $data = $this->decodeJson($request->request->get('data')); - unset($data['id']); - // save type - $property = Property\Predefined::create(); - $property->setValues($data); - $property->save(); - $responseData = $property->getObjectVars(); - $responseData['writeable'] = $property->isWriteable(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - } else { - // get list of types - $list = new Property\Predefined\Listing(); + public function propertiesAction( + Request $request, + PredefinedPropertyPayload $payload, + GetPredefinedPropertiesListHandler $getPredefinedPropertiesList, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::propertiesDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::propertiesUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::propertiesCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } - if ($filter = $request->request->get('filter')) { - $list->setFilter(function (Property\Predefined $predefined) use ($filter) { - foreach ($predefined->getObjectVars() as $value) { - if ($value) { - $cellValues = is_array($value) ? $value : [$value]; + $result = $getPredefinedPropertiesList($payload); - foreach ($cellValues as $cellValue) { - if (stripos((string)$cellValue, (string) $filter) !== false) { - return true; - } - } - } - } + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); + } - return false; - }); - } + #[IsGranted(CorePermission::PredefinedProperties->value)] + #[Route('/properties-destroy', name: 'opendxp_admin_settings_properties_destroy', methods: ['POST'])] + public function propertiesDestroyAction( + PredefinedPropertyPayload $payload, + DeletePredefinedPropertyHandler $deletePredefinedProperty, + ): JsonResponse { + $deletePredefinedProperty($payload); - $properties = []; - foreach ($list->getProperties() as $property) { - $data = $property->getObjectVars(); - $data['writeable'] = $property->isWriteable(); - $properties[] = $data; - } + return $this->adminJson(ApiResponse::ok(['data' => []])); + } - return $this->adminJson(['data' => $properties, 'success' => true, 'total' => $list->getTotalCount()]); - } + #[IsGranted(CorePermission::PredefinedProperties->value)] + #[Route('/properties-update', name: 'opendxp_admin_settings_properties_update', methods: ['POST'])] + public function propertiesUpdateAction( + PredefinedPropertyPayload $payload, + UpdatePredefinedPropertyHandler $updatePredefinedProperty, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $updatePredefinedProperty($payload)->data])); + } - return $this->adminJson(['success' => false]); + #[IsGranted(CorePermission::PredefinedProperties->value)] + #[Route('/properties-create', name: 'opendxp_admin_settings_properties_create', methods: ['POST'])] + public function propertiesCreateAction( + PredefinedPropertyPayload $payload, + CreatePredefinedPropertyHandler $createPredefinedProperty, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $createPredefinedProperty($payload)->data])); } + #[IsGranted(AdminPermission::SystemAppearance->value)] #[Route('/get-admin-system', name: 'opendxp_appearance_admin_settings_get', methods: ['GET'])] - public function getAppearanceSystemAction(AdminConfig $config): JsonResponse + public function getAppearanceSystemAction(GetAppearanceSettingsHandler $handler): JsonResponse { - $this->checkPermission('system_appearance_settings'); - $config = $config->getAdminSystemSettingsConfig(); - - $response = [ - 'values' => $config, - ]; - - return $this->adminJson($response); + return $this->adminJson(['values' => $handler()->values]); } + #[IsGranted(CorePermission::SystemSettings->value)] #[Route('/get-system', name: 'opendxp_admin_settings_getsystem', methods: ['GET'])] - public function getSystemAction(Request $request, SystemSettingsConfig $config): JsonResponse + public function getSystemAction(GetSystemSettingsHandler $handler): JsonResponse { - $this->checkPermission('system_settings'); - $config = $config->getSystemSettingsConfig(); + $result = $handler(); - // If required languages is empty it's the same as if all langauges are required. Therefore, we - // need to overwrite the value with the valid languages value to have all languages required - if (empty($config['general']['required_languages'])) { - $config['general']['required_languages'] = $config['general']['valid_languages']; - } - - $valueArray = [ - 'general' => $config['general'], - 'documents' => $config['documents'], - 'assets' => $config['assets'], - 'objects' => $config['objects'], - 'email' => $config['email'], - 'writeable' => $config['writeable'], - ]; - - $locales = Tool::getSupportedLocales(); - $languageOptions = []; - $validLanguages = []; - foreach ($locales as $short => $translation) { - if (!empty($short)) { - $languageOptions[] = [ - 'language' => $short, - 'display' => $translation . " ($short)", - ]; - $validLanguages[] = $short; - } - } - - //for "wrong" legacy values - foreach ($valueArray['general']['valid_languages'] as $existingValue) { - if (!in_array($existingValue, $validLanguages)) { - $languageOptions[] = [ - 'language' => $existingValue, - 'display' => $existingValue, - ]; - } - } - - $response = [ - 'values' => $valueArray, - 'config' => [ - 'languages' => $languageOptions, - ], - ]; - - return $this->adminJson($response); + return $this->adminJson([ + 'values' => $result->values, + 'config' => ['languages' => $result->languages], + ]); } + #[IsGranted(AdminPermission::SystemAppearance->value)] #[Route('/set-appearance', name: 'opendxp_admin_settings_appearance_set', methods: ['PUT'])] public function setAppearanceSystemAction( - Request $request, - KernelInterface $kernel, - EventDispatcherInterface $eventDispatcher, - CoreCacheHandler $cache, - Filesystem $filesystem, - CacheClearer $symfonyCacheClearer, - AdminConfig $config + SaveSettingsPayload $payload, + SaveAppearanceSettingsHandler $handler, ): JsonResponse { - $this->checkPermission('system_appearance_settings'); - - $values = $this->decodeJson($request->request->get('data')); - - $config->save($values); - - // clear all caches - $this->clearSymfonyCache($request, $kernel, $eventDispatcher, $symfonyCacheClearer); - $this->stopMessengerWorkers(); + $handler($payload); - $eventDispatcher->addListener(KernelEvents::TERMINATE, function (TerminateEvent $event) use ( - $cache, $eventDispatcher, $filesystem - ): void { - // we need to clear the cache with a delay, because the cache is used by messenger:stop-workers - // to send the stop signal to all worker processes - sleep(2); - $this->clearOpenDxpCache($cache, $eventDispatcher, $filesystem); - }); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::SystemSettings->value)] #[Route('/set-system', name: 'opendxp_admin_settings_setsystem', methods: ['PUT'])] public function setSystemAction( - Request $request, - KernelInterface $kernel, - EventDispatcherInterface $eventDispatcher, - CoreCacheHandler $cache, - Filesystem $filesystem, - CacheClearer $symfonyCacheClearer, - SystemSettingsConfig $config + SaveSettingsPayload $payload, + SaveSystemSettingsHandler $handler, ): JsonResponse { - $this->checkPermission('system_settings'); - - $values = $this->decodeJson($request->request->get('data')); - - $config->save($values); + $handler($payload); - // clear all caches - $this->clearSymfonyCache($request, $kernel, $eventDispatcher, $symfonyCacheClearer); - $this->stopMessengerWorkers(); - - $eventDispatcher->addListener(KernelEvents::TERMINATE, function (TerminateEvent $event) use ( - $cache, $eventDispatcher, $filesystem - ): void { - // we need to clear the cache with a delay, because the cache is used by messenger:stop-workers - // to send the stop signal to all worker processes - sleep(2); - $this->clearOpenDxpCache($cache, $eventDispatcher, $filesystem); - }); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(new Expression('is_granted("clear_cache") or is_granted("system_settings")'))] #[Route('/clear-cache', name: 'opendxp_admin_settings_clearcache', methods: ['DELETE'])] public function clearCacheAction( - Request $request, - KernelInterface $kernel, - EventDispatcherInterface $eventDispatcher, - CoreCacheHandler $cache, - Filesystem $filesystem, - CacheClearer $symfonyCacheClearer + ClearCachePayload $payload, + ClearCacheHandler $clearCache, ): JsonResponse { - $this->checkPermissionsHasOneOf(['clear_cache', 'system_settings']); - - $result = [ - 'success' => true, - ]; - - $clearOpenDxpCache = !(bool)$request->request->get('only_symfony_cache'); - $clearSymfonyCache = !(bool)$request->request->get('only_opendxp_cache'); - - if ($clearOpenDxpCache) { - $this->clearOpenDxpCache($cache, $eventDispatcher, $filesystem); - } - - if ($clearSymfonyCache) { - $this->clearSymfonyCache($request, $kernel, $eventDispatcher, $symfonyCacheClearer); - } + $clearCache($payload); - $response = new JsonResponse($result); + $response = new JsonResponse(ApiResponse::ok()); - if ($clearSymfonyCache) { - // we send the response directly here and exit to make sure no code depending on the stale container - // is running after this + if (!$payload->onlyOpendxpCache) { + // send response before exit so the client gets a reply before the process terminates $response->sendHeaders(); $response->sendContent(); exit; @@ -500,711 +285,116 @@ public function clearCacheAction( return $response; } - private function clearOpenDxpCache( - CoreCacheHandler $cache, - EventDispatcherInterface $eventDispatcher, - Filesystem $filesystem, - ): void { - // empty document cache - $cache->clearAll(); - - if ($filesystem->exists(OPENDXP_CACHE_DIRECTORY)) { - $filesystem->remove(OPENDXP_CACHE_DIRECTORY); - } - - $filesystem->dumpFile(OPENDXP_CACHE_DIRECTORY . '/.gitkeep', ''); - - $eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR); - } - - private function clearSymfonyCache( - Request $request, - KernelInterface $kernel, - EventDispatcherInterface $eventDispatcher, - CacheClearer $symfonyCacheClearer, - ): void { - - // if no env is passed it will use the current one - $environment = $request->request->get('env', $kernel->getEnvironment()); - - if ($kernel->getEnvironment() === $environment) { - // remove terminate and exception event listeners for the current env as they break with a - // cleared container - see #2434 - foreach ($eventDispatcher->getListeners(KernelEvents::TERMINATE) as $listener) { - $eventDispatcher->removeListener(KernelEvents::TERMINATE, $listener); - } - - foreach ($eventDispatcher->getListeners(KernelEvents::EXCEPTION) as $listener) { - $eventDispatcher->removeListener(KernelEvents::EXCEPTION, $listener); - } - } - - $symfonyCacheClearer->clear($environment); - } - + #[IsGranted(CorePermission::ClearFullpageCache->value)] #[Route('/clear-output-cache', name: 'opendxp_admin_settings_clearoutputcache', methods: ['DELETE'])] - public function clearOutputCacheAction(EventDispatcherInterface $eventDispatcher): JsonResponse + public function clearOutputCacheAction(ClearOutputCacheHandler $handler): JsonResponse { - $this->checkPermission('clear_fullpage_cache'); - - // remove "output" out of the ignored tags, if a cache lifetime is specified - Cache::removeIgnoredTagOnClear('output'); - - // empty document cache - Cache::clearTags(['output', 'output_lifetime']); + $handler(); - $eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_FULLPAGE_CACHE); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::ClearTempFiles->value)] #[Route('/clear-temporary-files', name: 'opendxp_admin_settings_cleartemporaryfiles', methods: ['DELETE'])] - public function clearTemporaryFilesAction(EventDispatcherInterface $eventDispatcher): JsonResponse + public function clearTemporaryFilesAction(ClearTemporaryFilesHandler $handler): JsonResponse { - $this->checkPermission('clear_temp_files'); - - // public files - Tool\Storage::get('thumbnail')->deleteDirectory('/'); - Db::get()->executeStatement('TRUNCATE TABLE assets_image_thumbnail_cache'); - - Tool\Storage::get('asset_cache')->deleteDirectory('/'); - - // system files - FileSystemHelper::recursiveDelete(OPENDXP_SYSTEM_TEMP_DIRECTORY, false); + $handler(); - $eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_TEMPORARY_FILES); - - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/get-available-admin-languages', name: 'opendxp_admin_settings_getavailableadminlanguages', methods: ['GET'])] - public function getAvailableAdminLanguagesAction(Request $request): JsonResponse + public function getAvailableAdminLanguagesAction(GetAvailableAdminLanguagesHandler $handler): JsonResponse { - $langs = []; - $availableLanguages = Tool\Admin::getLanguages(); - $locales = Tool::getSupportedLocales(); - - foreach ($availableLanguages as $lang) { - if (array_key_exists($lang, $locales)) { - $langs[] = [ - 'language' => $lang, - 'display' => $locales[$lang], - ]; - } - } - - usort($langs, fn ($a, $b) => strcmp($a['display'], $b['display'])); - - return $this->adminJson($langs); + return $this->adminJson($handler()->langs); } #[Route('/get-available-sites', name: 'opendxp_admin_settings_getavailablesites', methods: ['GET'])] - public function getAvailableSitesAction(Request $request): JsonResponse - { + public function getAvailableSitesAction( + GetAvailableSitesHandler $handler, + #[MapQueryParameter] ?string $excludeMainSite = null, + ): JsonResponse { try { - // we need to check documents permission for listing purposes in sites ext model & url-slugs $this->checkPermission('documents'); } catch (AccessDeniedHttpException) { Logger::log('[Startup] Sites are not loaded as "documents" permission is missing'); - //return empty string to avoid error on startup return $this->adminJson([]); } - $excludeMainSite = $request->query->get('excludeMainSite'); - - $sitesList = new Model\Site\Listing(); - $sitesObjects = $sitesList->load(); - $sites = []; - if (!$excludeMainSite) { - $sites[] = [ - 'id' => 0, - 'rootId' => 1, - 'domains' => '', - 'rootPath' => '/', - 'domain' => $this->translator->trans('main_site', [], 'admin'), - ]; - } - - foreach ($sitesObjects as $site) { - if ($site->getRootDocument()) { - if ($site->getMainDomain()) { - $sites[] = [ - 'id' => $site->getId(), - 'rootId' => $site->getRootId(), - 'domains' => implode(',', $site->getDomains()), - 'rootPath' => $site->getRootPath(), - 'domain' => $site->getMainDomain(), - ]; - } - } else { - // site is useless, parent doesn't exist anymore - $site->delete(); - } - } - - return $this->adminJson($sites); + return $this->adminJson($handler(excludeMainSite: (bool) $excludeMainSite)->sites); } #[Route('/get-available-countries', name: 'opendxp_admin_settings_getavailablecountries', methods: ['GET'])] - public function getAvailableCountriesAction(LocaleServiceInterface $localeService): JsonResponse + public function getAvailableCountriesAction(GetAvailableCountriesHandler $handler): JsonResponse { - $countries = $localeService->getDisplayRegions(); - asort($countries); - - $options = []; + $result = $handler(); - foreach ($countries as $short => $translation) { - if (strlen($short) === 2) { - $options[] = [ - 'key' => $translation . ' (' . $short . ')', - 'value' => $short, - ]; - } - } - - $result = ['data' => $options, 'success' => true, 'total' => count($options)]; - - return $this->adminJson($result); + return $this->adminJson(ApiResponse::ok(['data' => $result->options, 'total' => count($result->options)])); } #[Route('/thumbnail-adapter-check', name: 'opendxp_admin_settings_thumbnailadaptercheck', methods: ['GET'])] - public function thumbnailAdapterCheckAction(Request $request, TranslatorInterface $translator): Response + public function thumbnailAdapterCheckAction(ThumbnailAdapterCheckHandler $handler): Response { - $content = ''; - - $instance = \OpenDxp\Image::getInstance(); - if ($instance instanceof \OpenDxp\Image\Adapter\GD) { - $content = '' . - $translator->trans('important_use_imagick_pecl_extensions_for_best_results_gd_is_just_a_fallback_with_less_quality', [], 'admin') . - ''; - } - - return new Response($content); + return new Response($handler()->content); } - #[Route('/thumbnail-tree', name: 'opendxp_admin_settings_thumbnailtree', methods: ['GET', 'POST'])] - public function thumbnailTreeAction(): JsonResponse - { - $this->checkPermission('thumbnails'); - - $thumbnails = []; - - $list = new Asset\Image\Thumbnail\Config\Listing(); - - $groups = []; - foreach ($list->getThumbnails() as $item) { - if ($item->getGroup()) { - if (empty($groups[$item->getGroup()])) { - $groups[$item->getGroup()] = [ - 'id' => 'group_' . $item->getName(), - 'text' => htmlspecialchars($item->getGroup()), - 'expandable' => true, - 'leaf' => false, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'group' => $item->getGroup(), - 'children' => [], - ]; - } - $groups[$item->getGroup()]['children'][] = - [ - 'id' => $item->getName(), - 'text' => $item->getName(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_thumbnails', - 'cls' => 'opendxp_treenode_disabled', - 'writeable' => $item->isWriteable(), - ]; - } else { - $thumbnails[] = [ - 'id' => $item->getName(), - 'text' => $item->getName(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_thumbnails', - 'cls' => 'opendxp_treenode_disabled', - 'writeable' => $item->isWriteable(), - ]; - } - } - - foreach ($groups as $group) { - $thumbnails[] = $group; - } - - return $this->adminJson($thumbnails); - } - - #[Route('/thumbnail-downloadable', name: 'opendxp_admin_settings_thumbnaildownloadable', methods: ['GET'])] - public function thumbnailDownloadableAction(): JsonResponse - { - $thumbnails = []; - - $list = new Asset\Image\Thumbnail\Config\Listing(); - $list->setFilter(fn (Asset\Image\Thumbnail\Config $config) => $config->isDownloadable()); - - foreach ($list->getThumbnails() as $item) { - $thumbnails[] = [ - 'id' => $item->getName(), - 'text' => $item->getName(), - ]; - } - - return $this->adminJson($thumbnails); - } - - #[Route('/thumbnail-add', name: 'opendxp_admin_settings_thumbnailadd', methods: ['POST'])] - public function thumbnailAddAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $success = false; - - $pipe = Asset\Image\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe) { - $pipe = new Asset\Image\Thumbnail\Config(); - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - $pipe->setName($request->request->get('name')); - $pipe->save(); - $success = true; - } elseif (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - - return $this->adminJson(['success' => $success, 'id' => $pipe->getName()]); - } - - #[Route('/thumbnail-delete', name: 'opendxp_admin_settings_thumbnaildelete', methods: ['DELETE'])] - public function thumbnailDeleteAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Image\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - - $pipe->delete(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/thumbnail-get', name: 'opendxp_admin_settings_thumbnailget', methods: ['GET'])] - public function thumbnailGetAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Image\Thumbnail\Config::getByName($request->query->get('name')); - $data = $pipe->getObjectVars(); - $data['writeable'] = $pipe->isWriteable(); - - return $this->adminJson($data); - } - - #[Route('/thumbnail-update', name: 'opendxp_admin_settings_thumbnailupdate', methods: ['PUT'])] - public function thumbnailUpdateAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Image\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - - $settingsData = $this->decodeJson($request->request->get('settings')); - $mediaData = $this->decodeJson($request->request->get('medias')); - $mediaOrder = $this->decodeJson($request->request->get('mediaOrder')); - - foreach ($settingsData as $key => $value) { - $setter = 'set' . ucfirst($key); - if (method_exists($pipe, $setter)) { - $pipe->$setter($value); - } - } - - $pipe->resetItems(); - - uksort($mediaData, static function ($a, $b) use ($mediaOrder) { - if ($a === 'default') { - return -1; - } - - return ($mediaOrder[$a] < $mediaOrder[$b]) ? -1 : 1; - }); - - foreach ($mediaData as $mediaName => $items) { - if (preg_match('/["<>]/', $mediaName)) { - throw new Exception('Invalid media query name'); - } - - foreach ($items as $item) { - $type = $item['type']; - unset($item['type']); - - $pipe->addItem($type, $item, $mediaName); - } - } - - $pipe->save(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/video-thumbnail-adapter-check', name: 'opendxp_admin_settings_videothumbnailadaptercheck', methods: ['GET'])] - public function videoThumbnailAdapterCheckAction(Request $request, TranslatorInterface $translator): Response - { - $content = ''; - - if (!\OpenDxp\Video::isAvailable()) { - $content = '' . - $translator->trans('php_cli_binary_and_or_ffmpeg_binary_setting_is_missing', [], 'admin') . - ''; - } - - return new Response($content); - } - - #[Route('/video-thumbnail-tree', name: 'opendxp_admin_settings_videothumbnailtree', methods: ['GET', 'POST'])] - public function videoThumbnailTreeAction(): JsonResponse - { - $this->checkPermission('thumbnails'); - - $thumbnails = []; - - $list = new Asset\Video\Thumbnail\Config\Listing(); - - $groups = []; - foreach ($list->getThumbnails() as $item) { - if ($item->getGroup()) { - if (empty($groups[$item->getGroup()])) { - $groups[$item->getGroup()] = [ - 'id' => 'group_' . $item->getName(), - 'text' => htmlspecialchars($item->getGroup()), - 'expandable' => true, - 'leaf' => false, - 'allowChildren' => true, - 'iconCls' => 'opendxp_icon_folder', - 'group' => $item->getGroup(), - 'children' => [], - ]; - } - $groups[$item->getGroup()]['children'][] = - [ - 'id' => $item->getName(), - 'text' => $item->getName(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_videothumbnails', - 'cls' => 'opendxp_treenode_disabled', - 'writeable' => $item->isWriteable(), - ]; - } else { - $thumbnails[] = [ - 'id' => $item->getName(), - 'text' => $item->getName(), - 'leaf' => true, - 'iconCls' => 'opendxp_icon_videothumbnails', - 'cls' => 'opendxp_treenode_disabled', - 'writeable' => $item->isWriteable(), - ]; - } - } - - foreach ($groups as $group) { - $thumbnails[] = $group; - } - - return $this->adminJson($thumbnails); - } - - #[Route('/video-thumbnail-list', name: 'opendxp_admin_settings_videothumbnail_list', methods: ['GET'])] - public function videoThumbnailListAction(): JsonResponse - { - $thumbnails = [ - ['id' => 'opendxp-system-treepreview', 'text' => 'original'], - ]; - $list = new Asset\Video\Thumbnail\Config\Listing(); - - foreach ($list->getThumbnails() as $item) { - $thumbnails[] = [ - 'id' => $item->getName(), - 'text' => $item->getName(), - ]; - } - - return $this->adminJson($thumbnails); - } - - #[Route('/video-thumbnail-add', name: 'opendxp_admin_settings_videothumbnailadd', methods: ['POST'])] - public function videoThumbnailAddAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $success = false; - - $pipe = Asset\Video\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe) { - $pipe = new Asset\Video\Thumbnail\Config(); - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - $pipe->setName($request->request->get('name')); - $pipe->save(); - $success = true; - } elseif (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - - return $this->adminJson(['success' => $success, 'id' => $pipe->getName()]); - } - - #[Route('/video-thumbnail-delete', name: 'opendxp_admin_settings_videothumbnaildelete', methods: ['DELETE'])] - public function videoThumbnailDeleteAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Video\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings', name: 'opendxp_admin_settings_websitesettings', methods: ['POST'])] + public function websiteSettingsAction( + Request $request, + WebsiteSettingPayload $payload, + GetWebsiteSettingsListHandler $getWebsiteSettingsList, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::websiteSettingsDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::websiteSettingsUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::websiteSettingsCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; } - $pipe->delete(); + $result = $getWebsiteSettingsList($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => $result->total])); } - #[Route('/video-thumbnail-get', name: 'opendxp_admin_settings_videothumbnailget', methods: ['GET'])] - public function videoThumbnailGetAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Video\Thumbnail\Config::getByName($request->query->get('name')); - - $data = $pipe->getObjectVars(); - $data['writeable'] = $pipe->isWriteable(); - - return $this->adminJson($data); - } - - #[Route('/video-thumbnail-update', name: 'opendxp_admin_settings_videothumbnailupdate', methods: ['PUT'])] - public function videoThumbnailUpdateAction(Request $request): JsonResponse - { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Video\Thumbnail\Config::getByName($request->request->get('name')); - - if (!$pipe->isWriteable()) { - throw new ConfigWriteException(); - } - - $settingsData = $this->decodeJson($request->request->get('settings')); - $mediaData = $this->decodeJson($request->request->get('medias')); - $mediaOrder = $this->decodeJson($request->request->get('mediaOrder')); - - foreach ($settingsData as $key => $value) { - $setter = 'set' . ucfirst($key); - if (method_exists($pipe, $setter)) { - $pipe->$setter($value); - } - } - - $pipe->resetItems(); - - uksort($mediaData, static function ($a, $b) use ($mediaOrder) { - if ($a === 'default') { - return -1; - } - - return ($mediaOrder[$a] < $mediaOrder[$b]) ? -1 : 1; - }); - - foreach ($mediaData as $mediaName => $items) { - foreach ($items as $item) { - $type = $item['type']; - unset($item['type']); - - $pipe->addItem($type, $item, htmlspecialchars($mediaName)); - } - } - - $pipe->save(); + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings-destroy', name: 'opendxp_admin_settings_websitesettings_destroy', methods: ['POST'])] + public function websiteSettingsDestroyAction( + WebsiteSettingPayload $payload, + DeleteWebsiteSettingHandler $deleteWebsiteSetting, + ): JsonResponse { + $deleteWebsiteSetting($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok(['data' => []])); } - /** - * @throws Exception - */ - #[Route('/website-settings', name: 'opendxp_admin_settings_websitesettings', methods: ['POST'])] - public function websiteSettingsAction(Request $request): JsonResponse - { - $this->checkPermission('website_settings'); - - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - - if (is_array($data)) { - foreach ($data as &$value) { - if (is_string($value)) { - $value = trim($value); - } - } - } - - if ($request->query->get('xaction') === 'destroy') { - $id = $data['id']; - $setting = WebsiteSetting::getById($id); - if ($setting instanceof WebsiteSetting) { - $setting->delete(); - - return $this->adminJson(['success' => true, 'data' => []]); - } - } elseif ($request->query->get('xaction') === 'update') { - // save routes - $setting = WebsiteSetting::getById($data['id']); - if ($setting instanceof WebsiteSetting) { - switch ($setting->getType()) { - case 'document': - case 'asset': - case 'object': - if (isset($data['data'])) { - $element = Element\Service::getElementByPath($setting->getType(), $data['data']); - $data['data'] = $element; - } - - break; - } - - $setting->setValues($data); - $setting->save(); - - $data = $this->getWebsiteSettingForEditMode($setting); - - return $this->adminJson(['data' => $data, 'success' => true]); - } - } elseif ($request->query->get('xaction') === 'create') { - unset($data['id']); - - // save route - $setting = new WebsiteSetting(); - $setting->setValues($data); - - $setting->save(); - - return $this->adminJson(['data' => $setting->getObjectVars(), 'success' => true]); - } - } else { - $list = new WebsiteSetting\Listing(); - - $list->setLimit((int) $request->request->get('limit', 50)); - $list->setOffset((int) $request->request->get('start', 0)); - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings([...$request->request->all(), ...$request->query->all()]); - if ($sortingSettings['orderKey']) { - $list->setOrderKey($sortingSettings['orderKey']); - $list->setOrder($sortingSettings['order']); - } else { - $list->setOrderKey('name'); - $list->setOrder('asc'); - } - - if ($request->request->has('filter')) { - $list->setCondition('`name` LIKE ' . $list->quote('%'.$request->request->get('filter').'%')); - } - - $totalCount = $list->getTotalCount(); - $list = $list->load(); - - $settings = []; - foreach ($list as $item) { - $resultItem = $this->getWebsiteSettingForEditMode($item); - $settings[] = $resultItem; - } - - return $this->adminJson(['data' => $settings, 'success' => true, 'total' => $totalCount]); - } - - return $this->adminJson(['success' => false]); + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings-update', name: 'opendxp_admin_settings_websitesettings_update', methods: ['POST'])] + public function websiteSettingsUpdateAction( + WebsiteSettingPayload $payload, + UpdateWebsiteSettingHandler $updateWebsiteSetting, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $updateWebsiteSetting($payload)->data])); } - /** - * @return array{id: ?int, name: string, language: string, type: string, data: mixed, siteId: ?int, creationDate: ?int, modificationDate: ?int} - */ - private function getWebsiteSettingForEditMode(WebsiteSetting $item): array - { - $resultItem = [ - 'id' => $item->getId(), - 'name' => $item->getName(), - 'language' => $item->getLanguage(), - 'type' => $item->getType(), - 'data' => null, - 'siteId' => $item->getSiteId(), - 'creationDate' => $item->getCreationDate(), - 'modificationDate' => $item->getModificationDate(), - ]; - - switch ($item->getType()) { - case 'document': - case 'asset': - case 'object': - $element = $item->getData(); - if ($element) { - $resultItem['data'] = $element->getRealFullPath(); - } - - break; - default: - $resultItem['data'] = $item->getData(); - - break; - } - - return $resultItem; + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings-create', name: 'opendxp_admin_settings_websitesettings_create', methods: ['POST'])] + public function websiteSettingsCreateAction( + WebsiteSettingPayload $payload, + CreateWebsiteSettingHandler $createWebsiteSetting, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['data' => $createWebsiteSetting($payload)->data])); } #[Route('/get-available-algorithms', name: 'opendxp_admin_settings_getavailablealgorithms', methods: ['GET'])] - public function getAvailableAlgorithmsAction(Request $request): JsonResponse + public function getAvailableAlgorithmsAction(GetAvailableAlgorithmsHandler $handler): JsonResponse { - $options = [ - [ - 'key' => 'password_hash', - 'value' => 'password_hash', - ], - ]; + $result = $handler(); - $algorithms = hash_algos(); - foreach ($algorithms as $algorithm) { - $options[] = [ - 'key' => $algorithm . ' (' . $this->translator->trans('deprecated', [], 'admin') . ')', - 'value' => $algorithm, - ]; - } - - $result = ['data' => $options, 'success' => true, 'total' => count($options)]; - - return $this->adminJson($result); - } - - /** - * deleteViews - * delete views for localized fields when languages are removed to - * prevent mysql errors - */ - protected function deleteViews(string $language, string $dbName): void - { - $db = \OpenDxp\Db::get(); - $views = $db->fetchAllAssociative(sprintf('SHOW FULL TABLES IN %s WHERE TABLE_TYPE LIKE "VIEW"', $db->quoteIdentifier($dbName))); - - foreach ($views as $view) { - if (preg_match('/^object_localized_[0-9]+_' . $language . '$/', $view['Tables_in_' . $dbName])) { - $db->executeStatement(sprintf('DROP VIEW %s', $db->quoteIdentifier($view['Tables_in_' . $dbName]))); - } - } + return $this->adminJson(ApiResponse::ok(['data' => $result->options, 'total' => count($result->options)])); } } diff --git a/src/Controller/Admin/TagsController.php b/src/Controller/Admin/TagsController.php index 60ea39c9..4a080c1d 100644 --- a/src/Controller/Admin/TagsController.php +++ b/src/Controller/Admin/TagsController.php @@ -1,5 +1,4 @@ value)] #[Route('/add', name: 'opendxp_admin_tags_add', methods: ['POST'])] - public function addAction(Request $request): JsonResponse - { - $this->checkPermission('tags_configuration'); + public function addAction( + AddTagPayload $payload, + AddTagHandler $addTag, + ): JsonResponse { + $result = $addTag($payload); - try { - $tag = new Tag(); - $tag->setName(strip_tags($request->request->get('text', ''))); - $tag->setParentId((int)$request->request->get('parentId')); - $tag->save(); - - return $this->adminJson(['success' => true, 'id' => $tag->getId()]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } + return $this->adminJson(ApiResponse::ok(['id' => $result->id])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::TagsConfiguration->value)] #[Route('/delete', name: 'opendxp_admin_tags_delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse - { - $this->checkPermission('tags_configuration'); + public function deleteAction( + DeleteTagPayload $payload, + DeleteTagHandler $deleteTag, + ): JsonResponse { + $deleteTag($payload); - $tag = Tag::getById((int) $request->request->get('id')); - if ($tag) { - $tag->delete(); - - return $this->adminJson(['success' => true]); - } - - throw $this->createNotFoundException('Tag with ID ' . $request->request->get('id') . ' not found.'); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::TagsConfiguration->value)] #[Route('/update', name: 'opendxp_admin_tags_update', methods: ['PUT'])] - public function updateAction(Request $request): JsonResponse - { - $this->checkPermission('tags_configuration'); - - $tag = Tag::getById((int) $request->request->get('id')); - if ($tag) { - $parentId = $request->request->get('parentId'); - if ($parentId || $parentId === '0') { - $tag->setParentId((int)$parentId); - } - if ($request->request->has('text')) { - $tag->setName(strip_tags($request->request->get('text', ''))); - } + public function updateAction( + UpdateTagPayload $payload, + UpdateTagHandler $updateTag, + ): JsonResponse { + $updateTag($payload); - $tag->save(); - - return $this->adminJson(['success' => true]); - } - - throw $this->createNotFoundException('Tag with ID ' . $request->request->get('id') . ' not found.'); + return $this->adminJson(ApiResponse::ok()); } #[Route('/tree-get-children-by-id', name: 'opendxp_admin_tags_treegetchildrenbyid', methods: ['GET'])] - public function treeGetChildrenByIdAction(Request $request): JsonResponse - { - $showSelection = $request->query->get('showSelection') === 'true'; - $assignmentCId = (int)$request->query->get('assignmentCId'); - $assignmentCType = strip_tags($request->query->get('assignmentCType', '')); - - $recursiveChildren = false; - $assignedTagIds = []; - if ($assignmentCId && $assignmentCType) { - $assignedTags = Tag::getTagsForElement($assignmentCType, $assignmentCId); - - foreach ($assignedTags as $assignedTag) { - $assignedTagIds[$assignedTag->getId()] = $assignedTag; - } - } - - $tagList = new Tag\Listing(); - if ($request->query->get('node')) { - $tagList->setCondition('parentId = ?', (int)$request->query->get('node')); - } else { - $tagList->setCondition('ISNULL(parentId) OR parentId = 0'); - } - $tagList->setOrderKey('name'); + public function treeGetChildrenByIdAction( + GetTagTreeChildrenPayload $payload, + GetTagTreeChildrenHandler $getTagTreeChildren, + ): JsonResponse { + $result = $getTagTreeChildren($payload); - if (!empty($request->query->get('filter'))) { - $filterIds = [0]; - $filterTagList = new Tag\Listing(); - $filterTagList->setCondition('LOWER(`name`) LIKE ?', ['%' . $filterTagList->escapeLike(mb_strtolower($request->query->get('filter'))) . '%']); - foreach ($filterTagList->load() as $filterTag) { - if ($filterTag->getParentId() === 0) { - $filterIds[] = $filterTag->getId(); - } else { - $ids = explode('/', $filterTag->getIdPath()); - if (isset($ids[1])) { - $filterIds[] = (int)$ids[1]; - } - } - } - - $filterIds = array_unique($filterIds); - $tagList->setCondition('id IN('.implode(',', $filterIds).')'); - $recursiveChildren = true; - } - - $tags = []; - foreach ($tagList->load() as $tag) { - $tags[] = $this->convertTagToArray($tag, $showSelection, $assignedTagIds, true, $recursiveChildren); - } - - return $this->adminJson($tags); - } - - protected function convertTagToArray(Tag $tag, bool $showSelection, array $assignedTagIds, bool $loadChildren = false, bool $recursiveChildren = false): array - { - $hasChildren = $tag->hasChildren(); - - $tagArray = [ - 'id' => $tag->getId(), - 'text' => $tag->getName(), - 'path' => $tag->getNamePath(), - 'expandable' => $hasChildren, - 'leaf' => !$hasChildren, - 'iconCls' => 'opendxp_icon_element_tags', - 'qtipCfg' => [ - 'title' => 'ID: ' . $tag->getId(), - ], - ]; - - if ($showSelection) { - $tagArray['checked'] = isset($assignedTagIds[$tag->getId()]); - } - - if ($hasChildren && $loadChildren) { - $children = $tag->getChildren(); - $loadChildren = $recursiveChildren; - foreach ($children as $child) { - $tagArray['children'][] = $this->convertTagToArray($child, $showSelection, $assignedTagIds, $loadChildren, $recursiveChildren); - } - } - - return $tagArray; + return $this->adminJson($result->tags); } #[Route('/load-tags-for-element', name: 'opendxp_admin_tags_loadtagsforelement', methods: ['GET'])] - public function loadTagsForElementAction(Request $request): JsonResponse - { - $assignmentId = (int)$request->query->get('assignmentCId'); - $assignmentType = strip_tags($request->query->get('assignmentCType', '')); - - $assignedTagArray = []; - if ($assignmentId && $assignmentType) { - $assignedTags = Tag::getTagsForElement($assignmentType, $assignmentId); - - foreach ($assignedTags as $assignedTag) { - $assignedTagArray[] = $this->convertTagToArray($assignedTag, false, []); - } + public function loadTagsForElementAction( + LoadTagsForElementPayload $payload, + GetTagsForElementHandler $getTagsForElement, + ): JsonResponse { + if (!$payload->assignmentCId || !$payload->assignmentCType) { + return $this->adminJson([]); } - return $this->adminJson($assignedTagArray); + $result = $getTagsForElement($payload); + + return $this->adminJson($result->tags); } #[Route('/add-tag-to-element', name: 'opendxp_admin_tags_addtagtoelement', methods: ['PUT'])] - public function addTagToElementAction(Request $request): JsonResponse - { - $assignmentElementId = (int)$request->request->get('assignmentElementId'); - $assignmentElementType = strip_tags($request->request->get('assignmentElementType', '')); - $tagId = (int)$request->request->get('tagId'); + public function addTagToElementAction( + AddTagToElementPayload $payload, + AddTagToElementHandler $addTagToElement, + ): JsonResponse { + $result = $addTagToElement($payload); - $tag = Tag::getById($tagId); - if ($tag) { - Tag::addTagToElement($assignmentElementType, $assignmentElementId, $tag); - - return $this->adminJson(['success' => true, 'id' => $tag->getId()]); - } - - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['id' => $result->id])); } #[Route('/remove-tag-from-element', name: 'opendxp_admin_tags_removetagfromelement', methods: ['DELETE'])] - public function removeTagFromElementAction(Request $request): JsonResponse - { - $assignmentElementId = (int)$request->request->get('assignmentElementId'); - $assignmentElementType = strip_tags($request->request->get('assignmentElementType', '')); - $tagId = (int)$request->request->get('tagId'); + public function removeTagFromElementAction( + RemoveTagFromElementPayload $payload, + RemoveTagFromElementHandler $removeTagFromElement, + ): JsonResponse { + $result = $removeTagFromElement($payload); - $tag = Tag::getById($tagId); - if ($tag) { - Tag::removeTagFromElement($assignmentElementType, $assignmentElementId, $tag); - - return $this->adminJson(['success' => true, 'id' => $tag->getId()]); - } - - return $this->adminJson(['success' => false]); + return $this->adminJson(ApiResponse::ok(['id' => $result->id])); } #[Route('/get-batch-assignment-jobs', name: 'opendxp_admin_tags_getbatchassignmentjobs', methods: ['GET'])] - public function getBatchAssignmentJobsAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse - { - $elementId = (int)$request->query->get('elementId'); - $elementType = strip_tags($request->query->get('elementType', '')); - - $idList = []; - switch ($elementType) { - case 'object': - $object = \OpenDxp\Model\DataObject::getById($elementId); - if ($object) { - $idList = $this->getSubObjectIds($object, $eventDispatcher); - } - - break; - case 'asset': - $asset = \OpenDxp\Model\Asset::getById($elementId); - if ($asset) { - $idList = $this->getSubAssetIds($asset, $eventDispatcher); - } - - break; - case 'document': - $document = \OpenDxp\Model\Document::getById($elementId); - if ($document) { - $idList = $this->getSubDocumentIds($document, $eventDispatcher); - } + public function getBatchAssignmentJobsAction( + GetBatchAssignmentJobsPayload $payload, + GetBatchAssignmentJobsHandler $getBatchAssignmentJobs, + ): JsonResponse { + $result = $getBatchAssignmentJobs($payload); - break; - } - - $size = 2; - $offset = 0; - $idListParts = []; - while ($offset < count($idList)) { - $idListParts[] = array_slice($idList, $offset, $size); - $offset += $size; - } - - return $this->adminJson(['success' => true, 'idLists' => $idListParts, 'totalCount' => count($idList)]); - } - - /** - * @return int[] - */ - private function getSubObjectIds(\OpenDxp\Model\DataObject\AbstractObject $object, EventDispatcherInterface $eventDispatcher): array - { - $childrenList = new \OpenDxp\Model\DataObject\Listing(); - $condition = '`path` LIKE ?'; - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $userIds[] = $this->getAdminUser()->getId(); - $condition .= ' AND ( - (SELECT `view` FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,`key`),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - OR - (SELECT `view` FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,`key`))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - )'; - } - - $childrenList->setCondition($condition, $childrenList->escapeLike($object->getRealFullPath()) . '/%'); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => [], - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); - /** @var \OpenDxp\Model\DataObject\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - return $childrenList->loadIdList(); - } - - /** - * @return int[] - */ - private function getSubAssetIds(\OpenDxp\Model\Asset $asset, EventDispatcherInterface $eventDispatcher): array - { - $childrenList = new \OpenDxp\Model\Asset\Listing(); - $condition = '`path` LIKE ?'; - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $userIds[] = $this->getAdminUser()->getId(); - $condition .= ' AND ( - (SELECT `view` FROM users_workspaces_asset WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - OR - (SELECT `view` FROM users_workspaces_asset WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - )'; - } - - $childrenList->setCondition($condition, $childrenList->escapeLike($asset->getRealFullPath()) . '/%'); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => [], - ]); - - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); - /** @var \OpenDxp\Model\Asset\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - return $childrenList->loadIdList(); - } - - /** - * @return int[] - */ - private function getSubDocumentIds(\OpenDxp\Model\Document $document, EventDispatcherInterface $eventDispatcher): array - { - $childrenList = new \OpenDxp\Model\Document\Listing(); - $condition = '`path` LIKE ?'; - if (!$this->getAdminUser()->isAdmin()) { - $userIds = $this->getAdminUser()->getRoles(); - $userIds[] = $this->getAdminUser()->getId(); - $condition .= ' AND ( - (SELECT `view` FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,`key`),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - OR - (SELECT `view` FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,`key`))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 - )'; - } - - $childrenList->setCondition($condition, $childrenList->escapeLike($document->getRealFullPath()) . '/%'); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => [], - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::DOCUMENT_LIST_BEFORE_LIST_LOAD); - /** @var \OpenDxp\Model\Document\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - return $childrenList->loadIdList(); + return $this->adminJson(ApiResponse::ok(['idLists' => $result->idListParts, 'totalCount' => $result->totalCount])); } #[Route('/do-batch-assignment', name: 'opendxp_admin_tags_dobatchassignment', methods: ['PUT'])] - public function doBatchAssignmentAction(Request $request): JsonResponse - { - $cType = strip_tags($request->request->get('elementType', '')); - $assignedTags = json_decode($request->request->get('assignedTags')); - $elementIds = json_decode($request->request->get('childrenIds')); - $doCleanupTags = $request->request->get('removeAndApply') === 'true'; - - Tag::batchAssignTagsToElement($cType, $elementIds, $assignedTags, $doCleanupTags); + public function doBatchAssignmentAction( + DoBatchAssignmentPayload $payload, + DoBatchAssignmentHandler $doBatchAssignment, + ): JsonResponse { + $doBatchAssignment($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } } diff --git a/src/Controller/Admin/TranslationController.php b/src/Controller/Admin/TranslationController.php index 69e87ff7..886d2330 100644 --- a/src/Controller/Admin/TranslationController.php +++ b/src/Controller/Admin/TranslationController.php @@ -1,5 +1,4 @@ request->get('domain', Translation::DOMAIN_DEFAULT); - $admin = $domain == Translation::DOMAIN_ADMIN; - - $dialect = $request->request->get('csvSettings'); - $session = Session::getSessionBag($request->getSession(), 'opendxp_importconfig'); - $tmpFile = $session->get('translation_import_file'); - - if ($dialect) { - $dialect = json_decode($dialect); - } - - $this->checkPermission(($admin ? 'admin_' : '') . 'translations'); + public function importAction( + ImportTranslationsHandler $importTranslations, + ImportTranslationsPayload $payload, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - $merge = $request->query->get('merge'); - $overwrite = !$merge; - - $allowedLanguages = $this->getAdminUser()->getAllowedLanguagesForEditingWebsiteTranslations(); - if ($admin) { - $allowedLanguages = Tool\Admin::getLanguages(); - } - - $delta = Translation::importTranslationsFromFile( - $tmpFile, - $domain, - $overwrite, - $allowedLanguages, - $dialect - ); - - if (is_file($tmpFile)) { - @unlink($tmpFile); - } + $result = $importTranslations($payload); - $result = [ - 'success' => true, - ]; - - if ($merge) { - $enrichedDelta = []; - foreach ($delta as $item) { - $lg = $item['lg']; - $currentLocale = $localeService->findLocale(); - $item['lgname'] = Locale::getDisplayLanguage($lg, $currentLocale); - $item['icon'] = $this->generateUrl('opendxp_admin_misc_getlanguageflag', ['language' => $lg]); - $item['current'] = $item['text']; - $enrichedDelta[] = $item; - } - - $result['delta'] = base64_encode(json_encode($enrichedDelta)); + $extra = []; + if ($payload->enrichDelta) { + $extra['delta'] = base64_encode(json_encode($result->delta)); } - $response = $this->adminJson($result); + $response = $this->adminJson(ApiResponse::ok($extra)); // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in // Ext.form.Action.Submit and mark the submission as failed $response->headers->set('Content-Type', 'text/html'); @@ -109,371 +80,119 @@ public function importAction(Request $request, LocaleServiceInterface $localeSer } #[Route('/upload-import', name: 'opendxp_admin_translation_uploadimportfile', methods: ['POST'])] - public function uploadImportFileAction(Request $request, Filesystem $filesystem): JsonResponse - { - /** @var UploadedFile $file */ - $file = $request->files->get('Filedata'); - - $tmpData = file_get_contents($file->getPathname()); - - //store data for further usage - $filename = uniqid('import_translations-', false); - $importFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $filename; - $filesystem->dumpFile($importFile, $tmpData); - - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($importFile): void { - $session->set('translation_import_file', $importFile); + public function uploadImportFileAction( + Request $request, + UploadTranslationImportFileHandler $uploadTranslationImportFile, + UploadTranslationImportFilePayload $payload, + ): JsonResponse { + $result = $uploadTranslationImportFile($payload); + + Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($result): void { + $session->set('translation_import_file', $result->importFile); }, 'opendxp_importconfig'); - // determine csv settings - $dialect = Tool\Admin::determineCsvDialect($importFile); - - //ignore if line terminator is already hex otherwise generate hex for string - if (!empty($dialect->lineterminator) && empty(preg_match('/[a-f0-9]{2}/i', $dialect->lineterminator))) { - $dialect->lineterminator = bin2hex($dialect->lineterminator); - } - - return $this->adminJson([ - 'success' => true, - 'config' => [ - 'csvSettings' => $dialect, - ], - ]); + return $this->adminJson(ApiResponse::ok(['config' => [ + 'csvSettings' => $result->dialect, + ]])); } #[Route('/export', name: 'opendxp_admin_translation_export', methods: ['GET'])] - public function exportAction(Request $request): Response - { - $domain = $request->query->get('domain', Translation::DOMAIN_DEFAULT); - $admin = $domain == Translation::DOMAIN_ADMIN; - - $this->checkPermission(($admin ? 'admin_' : '') . 'translations'); - - $translation = new Translation(); - $translation->setDomain($domain); - $tableName = $translation->getDao()->getDatabaseTableName(); - - $list = new Translation\Listing(); - $list->setDomain($domain); + public function exportAction( + ExportTranslationsHandler $exportTranslations, + ExportTranslationsPayload $payload, + ): Response { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - $joins = []; + $result = $exportTranslations($payload); - $list->setOrder('asc'); - $list->setOrderKey($tableName . '.key', false); - - $filterParameters = [ - 'filter' => $request->query->get('filter'), - 'searchString' => $request->query->get('searchString'), - ]; - - $conditions = $this->getGridFilterCondition($filterParameters, $tableName, false, $admin); - if ($conditions !== []) { - $list->setCondition($conditions['condition'], $conditions['params']); - } - - $filters = $this->getGridFilterCondition($filterParameters, $tableName, true, $admin); - - if ($filters) { - $joins = [...$joins, ...$filters['joins']]; - } - - $this->extendTranslationQuery($joins, $list, $tableName, $filters); - - try { - $list->load(); - } catch (SyntaxErrorException) { - throw new InvalidArgumentException('Check your arguments.'); - } - - $translations = []; - $translationObjects = $list->getTranslations(); - - // fill with one dummy translation if the store is empty - if ($translationObjects === []) { - if ($admin) { - $t = new Translation(); - $t->setDomain(Translation::DOMAIN_ADMIN); - $languages = Tool\Admin::getLanguages(); - } else { - $t = new Translation(); - $languages = $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); - } - - foreach ($languages as $language) { - $t->addTranslation($language, ''); - } - - $translationObjects[] = $t; - } - - foreach ($translationObjects as $t) { - $row = $t->getTranslations(); - $row = Element\Service::escapeCsvRecord($row); - $translations[] = ['key' => $t->getKey(), 'creationDate' => $t->getCreationDate(), 'modificationDate' => $t->getModificationDate(), ...$row]; - } - - //header column - $columns = array_keys($translations[0]); - - if ($admin) { - $languages = Tool\Admin::getLanguages(); - } else { - $languages = $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); - } - - //add language columns which have no translations yet - foreach ($languages as $l) { - if (!in_array($l, $columns)) { - $columns[] = $l; - } - } - - //remove invalid languages - foreach ($columns as $key => $column) { - if (strtolower(trim($column)) !== 'key' && !in_array($column, $languages)) { - unset($columns[$key]); - } - } - $columns = array_values($columns); - - $headerRow = []; - foreach ($columns as $key => $value) { - $headerRow[] = '"' . $value . '"'; - } - $csv = implode(';', $headerRow) . "\r\n"; - - foreach ($translations as $t) { - $tempRow = []; - foreach ($columns as $key) { - $value = $t[$key] ?? null; - //clean value of evil stuff such as " and linebreaks - if (is_string($value)) { - $value = Tool\Text::removeLineBreaks($value); - $value = str_replace('"', '"', $value); - - $tempRow[$key] = '"' . $value . '"'; - } else { - $tempRow[$key] = $value; - } - } - $csv .= implode(';', $tempRow) . "\r\n"; - } - - $response = new Response("\xEF\xBB\xBF" . $csv); + $response = new Response("\xEF\xBB\xBF" . $result->csv); $response->headers->set('Content-Encoding', 'UTF-8'); $response->headers->set('Content-Type', 'text/csv; charset=UTF-8'); - $response->headers->set('Content-Disposition', 'attachment; filename: "export_' . $domain . '_translations.csv"'); + $response->headers->set('Content-Disposition', 'attachment; filename: "export_' . $result->domain . '_translations.csv"'); ini_set('display_errors', '0'); //to prevent warning messages in csv return $response; } #[Route('/add-admin-translation-keys', name: 'opendxp_admin_translation_addadmintranslationkeys', methods: ['POST'])] - public function addAdminTranslationKeysAction(Request $request): JsonResponse - { - $keys = $request->request->get('keys'); - - if ($keys) { - $availableLanguages = Tool\Admin::getLanguages(); - $data = $this->decodeJson($keys); - foreach ($data as $translationData) { - $t = null; // reset - - try { - $t = Translation::getByKey($translationData, Translation::DOMAIN_ADMIN); - } catch (Exception $e) { - Logger::log((string) $e); - } - if (!$t instanceof Translation) { - $t = new Translation(); - $t->setDomain(Translation::DOMAIN_ADMIN); - $t->setKey($translationData); - $t->setCreationDate(time()); - $t->setModificationDate(time()); - - foreach ($availableLanguages as $lang) { - $t->addTranslation($lang, ''); - } - - try { - $t->save(); - } catch (Exception $e) { - Logger::log((string) $e); - } - } - } - } + public function addAdminTranslationKeysAction( + AddAdminTranslationKeysHandler $addAdminTranslationKeys, + AddAdminTranslationKeysPayload $payload, + ): JsonResponse { + $addAdminTranslationKeys($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/translations', name: 'opendxp_admin_translation_translations', methods: ['POST'])] - public function translationsAction(Request $request, TranslatorInterface $translator): JsonResponse - { - $domain = $request->request->get('domain', Translation::DOMAIN_DEFAULT); - $admin = $domain === Translation::DOMAIN_ADMIN; - $validLanguages = $admin ? Tool\Admin::getLanguages() : $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); - - $this->checkPermission(($admin ? 'admin_' : '') . 'translations'); - - $translation = new Translation(); - $translation->setDomain($domain); - $tableName = $translation->getDao()->getDatabaseTableName(); - - if ($request->request->has('data')) { - $data = $this->decodeJson($request->request->get('data')); - - if ($request->query->get('xaction') === 'destroy') { - $t = Translation::getByKey($data['key'], $domain); - if ($t instanceof Translation) { - $t->delete(); - } - - return $this->adminJson([ - 'success' => true, - 'data' => [], - ]); - } - - if ($request->query->get('xaction') === 'update') { - $t = Translation::getByKey($data['key'], $domain); - - foreach ($data as $key => $value) { - $key = preg_replace('/^_/', '', $key, 1); - if (!in_array($key, ['key', 'type'])) { - $t->addTranslation($key, $value); - } - } - - if ($data['key']) { - $t->setKey($data['key']); - } - - if ($data['type']) { - $t->setType($data['type']); - } - $t->setModificationDate(time()); - $t->save(); - - return $this->adminJson([ - 'success' => true, - 'data' => [ - 'key' => $t->getKey(), - 'creationDate' => $t->getCreationDate(), - 'modificationDate' => $t->getModificationDate(), - 'type' => $t->getType(), - ...$this->prefixTranslations($t->getTranslations()), - ], - ]); - } - - if ($request->query->get('xaction') === 'create') { - $t = Translation::getByKey($data['key'], $domain); - if ($t) { - return $this->adminJson([ - 'message' => 'identifier_already_exists', - 'success' => false, - ]); - } - - $t = new Translation(); - $t->setDomain($domain); - $t->setKey($data['key']); - $t->setCreationDate(time()); - $t->setModificationDate(time()); - $t->setType($data['type'] ?? null); - - foreach ($validLanguages as $lang) { - $t->addTranslation($lang, ''); - } - - $t->save(); - - return $this->adminJson([ - 'success' => true, - 'data' => [ - 'key' => $t->getKey(), - 'creationDate' => $t->getCreationDate(), - 'modificationDate' => $t->getModificationDate(), - 'type' => $t->getType(), - ...$this->prefixTranslations($t->getTranslations()), - ], - ]); - } - } - - // get list of types - $list = new Translation\Listing(); - $list->setDomain($domain); - $list->setOrder('asc'); - $list->setOrderKey($tableName . '.key', false); - $list->setLanguages($validLanguages); - - $sortingSettings = \OpenDxp\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings( - [...$request->request->all(), ...$request->query->all()] - ); - - $joins = []; - - if ($orderKey = $sortingSettings['orderKey']) { - if (in_array(trim($orderKey, '_'), $validLanguages)) { - $orderKey = trim($orderKey, '_'); - $joins[] = [ - 'language' => $orderKey, - ]; - $list->setOrderKey($orderKey); - } elseif ($list->isValidOrderKey($sortingSettings['orderKey'])) { - $list->setOrderKey($tableName . '.' . $sortingSettings['orderKey'], false); - } - } - if ($sortingSettings['order']) { - $list->setOrder($sortingSettings['order']); - } - - $list->setLimit((int) $request->request->get('limit', 50)); - $list->setOffset((int) $request->request->get('start', 0)); - - $filterParameters = [ - 'filter' => $request->request->get('filter'), - 'searchString' => $request->request->get('searchString'), - ]; + public function translationsAction( + Request $request, + TranslationPayload $payload, + GetTranslationsHandler $getTranslations, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); + + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::translationsDestroyAction', [], $request->query->all()), + 'update' => $this->forward(self::class . '::translationsUpdateAction', [], $request->query->all()), + 'create' => $this->forward(self::class . '::translationsCreateAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } + + $result = $getTranslations($payload); + + return $this->adminJson(ApiResponse::ok(['data' => $result->translations, 'total' => $result->total])); + } - $conditions = $this->getGridFilterCondition($filterParameters, $tableName, false, $admin); - $filters = $this->getGridFilterCondition($filterParameters, $tableName, true, $admin); + #[Route('/translations-destroy', name: 'opendxp_admin_translation_translations_destroy', methods: ['POST'])] + public function translationsDestroyAction( + TranslationPayload $payload, + DeleteTranslationHandler $deleteTranslation, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - if ($filters) { - $joins = [...$joins, ...$filters['joins']]; - } + $deleteTranslation($payload); - if ($conditions !== []) { - $list->setCondition($conditions['condition'], $conditions['params']); - } + return $this->adminJson(ApiResponse::ok(['data' => []])); + } - $this->extendTranslationQuery($joins, $list, $tableName, $filters); - - $translations = []; - $searchString = $request->request->get('searchString'); - foreach ($list->getTranslations() as $t) { - //Reload translation to get complete data, - //if translation fetched based on the text not key - if ($searchString && !strpos($searchString, (string) $t->getKey()) && !$t = Translation::getByKey($t->getKey(), $domain)) { - continue; - } - - $translations[] = [ - ...$this->prefixTranslations($t->getTranslations()), - 'key' => $t->getKey(), - 'creationDate' => $t->getCreationDate(), - 'modificationDate' => $t->getModificationDate(), - 'type' => $t->getType(), - ]; - } + #[Route('/translations-update', name: 'opendxp_admin_translation_translations_update', methods: ['POST'])] + public function translationsUpdateAction( + TranslationPayload $payload, + UpdateTranslationHandler $updateTranslation, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); + + $result = $updateTranslation($payload); + + return $this->adminJson(ApiResponse::ok(['data' => [ + 'key' => $result->key, + 'creationDate' => $result->creationDate, + 'modificationDate' => $result->modificationDate, + 'type' => $result->type, + ...$this->prefixTranslations($result->translations), + ]])); + } - return $this->adminJson([ - 'success' => true, - 'data' => $translations, - 'total' => $list->getTotalCount(), - ]); + #[Route('/translations-create', name: 'opendxp_admin_translation_translations_create', methods: ['POST'])] + public function translationsCreateAction( + TranslationPayload $payload, + CreateTranslationHandler $createTranslation, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); + + $result = $createTranslation($payload); + + return $this->adminJson(ApiResponse::ok(['data' => [ + 'key' => $result->key, + 'creationDate' => $result->creationDate, + 'modificationDate' => $result->modificationDate, + 'type' => $result->type, + ...$this->prefixTranslations($result->translations), + ]])); } protected function prefixTranslations(array $translations): array @@ -486,171 +205,14 @@ protected function prefixTranslations(array $translations): array return $prefixedTranslations; } - protected function extendTranslationQuery(array $joins, Translation\Listing $list, string $tableName, array $filters): void - { - if ($joins) { - $list->onCreateQueryBuilder( - function (DoctrineQueryBuilder $select) use ( - $joins, - $tableName, - $filters - ): void { - $db = \OpenDxp\Db::get(); - - $alreadyJoined = []; - - foreach ($joins as $join) { - $fieldname = $join['language']; - - if (isset($alreadyJoined[$fieldname])) { - continue; - } - $alreadyJoined[$fieldname] = 1; - - $select->addSelect($fieldname . '.text AS ' . $fieldname); - $select->leftJoin( - $tableName, - $tableName, - $fieldname, - '(' - . $fieldname . '.key = ' . $tableName . '.key' - . ' and ' . $fieldname . '.language = ' . $db->quote($fieldname) - . ')' - ); - } - - $havings = $filters['conditions']; - if ($havings) { - $havings = implode(' AND ', $havings); - $select->having($havings); - } - } - ); - } - } - - protected function getGridFilterCondition(array $filterParameters, string $tableName, bool $languageMode = false, bool $admin = false): array - { - $placeHolderCount = 0; - $joins = []; - $conditions = []; - $validLanguages = $admin ? Tool\Admin::getLanguages() : $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); - - $db = \OpenDxp\Db::get(); - $conditionFilters = []; - - $filterJson = $filterParameters['filter']; - if ($filterJson) { - $propertyField = 'property'; - $operatorField = 'operator'; - - $filters = $this->decodeJson($filterJson); - foreach ($filters as $filter) { - $operator = '='; - $field = null; - $value = null; - - $fieldname = $filter[$propertyField]; - if (in_array(ltrim($fieldname, '_'), $validLanguages)) { - $fieldname = ltrim($fieldname, '_'); - } - $fieldname = str_replace('--', '', $fieldname); - if (!$languageMode && in_array($fieldname, $validLanguages)) { - continue; - } - if ($languageMode && !in_array($fieldname, $validLanguages)) { - continue; - } - - if (!$languageMode) { - $fieldname = $tableName . '.' . $fieldname; - } - - if (!empty($filter['value'])) { - if ($filter['type'] === 'string') { - $operator = 'LIKE'; - $field = $fieldname; - $value = '%' . $filter['value'] . '%'; - } elseif ($filter['type'] === 'date' || - (in_array($fieldname, ['modificationDate', 'creationDate']))) { - if ($filter[$operatorField] === 'lt') { - $operator = '<'; - } elseif ($filter[$operatorField] === 'gt') { - $operator = '>'; - } elseif ($filter[$operatorField] === 'eq') { - $operator = '='; - $fieldname = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))"; - } - $filter['value'] = strtotime($filter['value']); - $field = $fieldname; - $value = $filter['value']; - } - } - - if ($field && $value) { - $condition = $db->quoteIdentifier($field) . ' ' . $operator . ' ' . $db->quote($value); - - if ($languageMode) { - $conditions[$fieldname] = $condition; - $joins[] = [ - 'language' => $fieldname, - ]; - } else { - $placeHolderName = self::PLACEHOLDER_NAME . $placeHolderCount; - $placeHolderCount++; - $conditionFilters[] = [ - 'condition' => $field . ' ' . $operator . ' :' . $placeHolderName, - 'field' => $placeHolderName, - 'value' => $value, - ]; - } - } - } - } - - if (!empty($filterParameters['searchString'])) { - $conditionFilters[] = [ - 'condition' => '(lower(' . $tableName . '.key) LIKE :filterTerm OR lower(' . $tableName . '.text) LIKE :filterTerm)', - 'field' => 'filterTerm', - 'value' => '%' . mb_strtolower($filterParameters['searchString']) . '%', - ]; - } - - if ($languageMode) { - return [ - 'joins' => $joins, - 'conditions' => $conditions, - ]; - } - - if ($conditionFilters !== []) { - $conditions = []; - $params = []; - foreach ($conditionFilters as $conditionFilter) { - $conditions[] = $conditionFilter['condition']; - $params[$conditionFilter['field']] = $conditionFilter['value']; - } - - $conditionFilters = [ - 'condition' => implode(' AND ', $conditions), - 'params' => $params, - ]; - } - - return $conditionFilters; - } - #[Route('/cleanup', name: 'opendxp_admin_translation_cleanup', methods: ['DELETE'])] - public function cleanupAction(Request $request): JsonResponse - { - $domain = $request->request->get('domain', Translation::DOMAIN_DEFAULT); - $list = new Translation\Listing(); - $list->setDomain($domain); - $list->cleanup(); - - \OpenDxp\Cache::clearTags(['translator', 'translate']); + public function cleanupAction( + CleanupTranslationsHandler $cleanupTranslations, + CleanupTranslationsPayload $payload, + ): JsonResponse { + $cleanupTranslations($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } /** @@ -660,154 +222,47 @@ public function cleanupAction(Request $request): JsonResponse * ----------------------------------------------------------------------------------- */ #[Route('/content-export-jobs', name: 'opendxp_admin_translation_contentexportjobs', methods: ['POST'])] - public function contentExportJobsAction(Request $request): JsonResponse - { - $data = $this->decodeJson($request->request->get('data')); - $elements = []; - $jobs = []; - $exportId = uniqid('', false); - $source = $request->request->get('source', ''); - $target = $request->request->get('target', ''); - $type = $request->request->get('type'); - $jobUrl = $request->request->get('job_url', $request->getBaseUrl() . '/admin/translation/' . $type . '-export'); - - $source = str_replace('_', '-', $source); - $target = str_replace('_', '-', $target); - - if ($data && is_array($data)) { - foreach ($data as $element) { - $elements[$element['type'] . '_' . $element['id']] = [ - 'id' => $element['id'], - 'type' => $element['type'], - ]; - - $el = null; - - if ($element['children']) { - $el = Element\Service::getElementById($element['type'], (int) $element['id']); - $baseClass = ELement\Service::getBaseClassNameForElement($element['type']); - $listClass = '\\OpenDxp\\Model\\' . $baseClass . '\\Listing'; - $list = new $listClass(); - $list->setUnpublished(true); - if ($el instanceof DataObject\AbstractObject) { - // inlcude variants - $list->setObjectTypes( - [DataObject::OBJECT_TYPE_VARIANT, - DataObject::OBJECT_TYPE_OBJECT, - DataObject::OBJECT_TYPE_FOLDER, ] - ); - } - $list->setCondition( - 'path LIKE ?', - [$list->escapeLike($el->getRealFullPath() . ($el->getRealFullPath() !== '/' ? '/' : '')) . '%'] - ); - $children = $list->load(); - - foreach ($children as $child) { - $childId = $child->getId(); - $elements[$element['type'] . '_' . $childId] = [ - 'id' => $childId, - 'type' => $element['type'], - ]; - - if (isset($element['relations']) && $element['relations']) { - $childDependencies = $child->getDependencies()->getRequires(); - foreach ($childDependencies as $cd) { - if ($cd['type'] === 'object' || $cd['type'] === 'document') { - $elements[$cd['type'] . '_' . $cd['id']] = $cd; - } - } - } - } - } - - if (isset($element['relations']) && $element['relations']) { - if (!$el instanceof Element\ElementInterface) { - $el = Element\Service::getElementById($element['type'], (int) $element['id']); - } - - $dependencies = $el->getDependencies()->getRequires(); - foreach ($dependencies as $dependency) { - if ($dependency['type'] === 'object' || $dependency['type'] === 'document') { - $elements[$dependency['type'] . '_' . $dependency['id']] = $dependency; - } - } - } - } - } - - $elements = array_values($elements); - - $elementsPerJob = (int)$request->request->get('elements_per_job', 10); - - // make sure elements per job is not 0 - if (!$elementsPerJob) { - $elementsPerJob = 1; - } - - // one job = X elements - $elements = array_chunk($elements, $elementsPerJob); - foreach ($elements as $chunk) { - $jobs[] = [[ - 'url' => $jobUrl, - 'method' => 'POST', - 'params' => [ - 'id' => $exportId, - 'source' => $source, - 'target' => $target, - 'data' => $this->encodeJson($chunk), - ], - ]]; - } + public function contentExportJobsAction( + BuildContentExportJobsHandler $buildContentExportJobs, + BuildContentExportJobsPayload $payload, + ): JsonResponse { + $result = $buildContentExportJobs($payload); - return $this->adminJson([ - 'success' => true, - 'jobs' => $jobs, - 'id' => $exportId, - ]); + return $this->adminJson(ApiResponse::ok(['jobs' => $result->jobs, 'id' => $result->exportId])); } #[Route('/merge-item', name: 'opendxp_admin_translation_mergeitem', methods: ['PUT'])] - public function mergeItemAction(Request $request): JsonResponse - { - $domain = $request->request->get('domain', Translation::DOMAIN_DEFAULT); - - $dataList = json_decode($request->request->get('data'), true); - - foreach ($dataList as $data) { - $t = Translation::getByKey($data['key'], $domain, true); - $newValue = htmlspecialchars_decode($data['current']); - $t->addTranslation($data['lg'], $newValue); - $t->setModificationDate(time()); - $t->save(); - } + public function mergeItemAction( + MergeTranslationItemsHandler $mergeTranslationItems, + MergeTranslationItemsPayload $payload, + ): JsonResponse { + $mergeTranslationItems($payload); - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/get-website-translation-languages', name: 'opendxp_admin_translation_getwebsitetranslationlanguages', methods: ['GET'])] - public function getWebsiteTranslationLanguagesAction(Request $request): JsonResponse - { + public function getWebsiteTranslationLanguagesAction( + GetWebsiteTranslationLanguagesHandler $getWebsiteTranslationLanguages, + EmptyPayload $payload, + ): JsonResponse { + $result = $getWebsiteTranslationLanguages($payload); + return $this->adminJson([ - 'view' => $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(), + 'view' => $result->view, //when no view language is defined, all languages are editable. if one view language is defined, it //may be possible that no edit language is set intentionally - 'edit' => $this->getAdminUser()->getAllowedLanguagesForEditingWebsiteTranslations(), + 'edit' => $result->edit, ]); } #[Route('/get-translation-domains', name: 'opendxp_admin_translation_gettranslationdomains', methods: ['GET'])] - public function getTranslationDomainsAction(Request $request): JsonResponse - { - $translation = new Translation(); - - $domains = array_map( - static fn ($domain) => ['name' => $domain], - $translation->getDao()->getAvailableDomains(), - ); + public function getTranslationDomainsAction( + GetTranslationDomainsHandler $getTranslationDomains, + EmptyPayload $payload, + ): JsonResponse { + $result = $getTranslationDomains($payload); - return $this->adminJson(['domains' => $domains]); + return $this->adminJson(['domains' => $result->domains]); } } diff --git a/src/Controller/Admin/User/RoleController.php b/src/Controller/Admin/User/RoleController.php new file mode 100644 index 00000000..2426c962 --- /dev/null +++ b/src/Controller/Admin/User/RoleController.php @@ -0,0 +1,87 @@ +value)] + #[Route('/user/role-tree-get-children-by-id', name: 'opendxp_admin_user_roletreegetchildrenbyid', methods: ['GET'])] + public function roleTreeGetChildrenByIdAction( + GetRoleTreeChildrenPayload $payload, + GetRoleTreeChildrenHandler $getRoleTreeChildren, + ): JsonResponse { + return $this->adminJson($getRoleTreeChildren($payload)); + } + + #[IsGranted(CorePermission::Users->value)] + #[Route('/user/role-get', name: 'opendxp_admin_user_roleget', methods: ['GET'])] + public function roleGetAction( + GetRolePayload $payload, + GetRoleHandler $getRole, + ): JsonResponse { + $result = $getRole($payload); + + return $this->adminJson(ApiResponse::ok([ + 'role' => $result->role, + 'permissions' => $result->permissions, + 'classes' => $result->classes, + 'docTypes' => $result->docTypes, + 'availablePermissions' => $result->availablePermissions, + 'availablePerspectives' => $result->availablePerspectives, + 'validLanguages' => $result->validLanguages, + ])); + } + + #[IsGranted(CorePermission::Users->value)] + #[Route('/user/get-roles', name: 'opendxp_admin_user_getroles', methods: ['GET'])] + public function getRolesAction( + GetRolesPayload $payload, + GetRolesHandler $getRoles, + ): JsonResponse { + $roles = $getRoles($payload); + + return $this->adminJson(ApiResponse::ok(['total' => count($roles), 'data' => $roles])); + } + + #[IsGranted(CorePermission::ShareConfigurations->value)] + #[Route('/user/get-roles-for-sharing', name: 'opendxp_admin_user_getrolesforsharing', methods: ['GET'])] + public function getRolesForSharingAction( + GetRolesPayload $payload, + GetRolesHandler $getRoles, + ): JsonResponse { + $roles = $getRoles($payload); + + return $this->adminJson(ApiResponse::ok(['total' => count($roles), 'data' => $roles])); + } +} diff --git a/src/Controller/Admin/User/UserProfileController.php b/src/Controller/Admin/User/UserProfileController.php new file mode 100644 index 00000000..c13559c2 --- /dev/null +++ b/src/Controller/Admin/User/UserProfileController.php @@ -0,0 +1,98 @@ +adminJson(ApiResponse::ok()); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } + + #[Route('/user/update-current-user', name: 'opendxp_admin_user_updatecurrentuser', methods: ['PUT'])] + public function updateCurrentUserAction( + Request $request, + UpdateCurrentUserPayload $payload, + UpdateCurrentUserHandler $updateCurrentUser, + ): JsonResponse { + if (!$request->request->has('id')) { + return $this->adminJson(false); + } + + $updateCurrentUser($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/user/get-current-user', name: 'opendxp_admin_user_getcurrentuser', methods: ['GET'])] + public function getCurrentUserAction( + GetCurrentUserPayload $payload, + GetCurrentUserHandler $getCurrentUser, + ): Response { + $result = $getCurrentUser($payload); + + $response = new Response('opendxp.currentuser = ' . $this->encodeJson($result->userData)); + $response->headers->set('Content-Type', 'text/javascript'); + + return $response; + } + + #[Route('/user/reset-my-2fa-secret', name: 'opendxp_admin_user_reset_my_2fa_secret', methods: ['PUT'])] + public function resetMy2FaSecretAction( + EmptyPayload $payload, + ResetMy2FaSecretHandler $resetMy2FaSecret, + ): JsonResponse { + + $resetMy2FaSecret($payload); + + return $this->adminJson(ApiResponse::ok()); + } + + #[Route('/user/get-default-key-bindings', name: 'opendxp_admin_user_getdefaultkeybindings', methods: ['GET'])] + public function getDefaultKeyBindingsAction(): JsonResponse + { + return $this->adminJson(ApiResponse::ok(['data' => UserHelper::getDefaultKeyBindings()])); + } +} diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index bdee9899..963ff5a5 100644 --- a/src/Controller/Admin/UserController.php +++ b/src/Controller/Admin/UserController.php @@ -1,5 +1,4 @@ value)] #[Route('/user/tree-get-children-by-id', name: 'opendxp_admin_user_treegetchildrenbyid', methods: ['GET'])] - public function treeGetChildrenByIdAction(Request $request): JsonResponse - { - $list = new User\Listing(); - $list->setCondition('parentId = ?', (int)$request->query->get('node')); - $list->setOrder('ASC'); - $list->setOrderKey('name'); - $list->load(); - - $users = []; - foreach ($list->getUsers() as $user) { - if ($user->getId() && $user->getName() !== 'system') { - $users[] = $this->getTreeNodeConfig($user); - } - } - - return $this->adminJson($users); - } - - protected function getTreeNodeConfig(User|User\Folder $user): array - { - $tmpUser = [ - 'id' => $user->getId(), - 'text' => $user->getName(), - 'elementType' => 'user', - 'type' => $user->getType(), - 'qtipCfg' => [ - 'title' => 'ID: ' . $user->getId(), - ], - ]; - - // set type specific settings - if ($user instanceof User\Folder) { - $tmpUser['leaf'] = false; - $tmpUser['iconCls'] = 'opendxp_icon_folder'; - $tmpUser['expanded'] = true; - $tmpUser['allowChildren'] = true; - - if ($user->hasChildren()) { - $tmpUser['expanded'] = false; - } else { - $tmpUser['loaded'] = true; - } - } else { - $tmpUser['leaf'] = true; - $tmpUser['iconCls'] = 'opendxp_icon_user'; - if (!$user->getActive()) { - $tmpUser['cls'] = ' opendxp_unpublished'; - } - $tmpUser['allowChildren'] = false; - $tmpUser['admin'] = $user->isAdmin(); - } - - return $tmpUser; + public function treeGetChildrenByIdAction( + GetUserTreeChildrenHandler $getUserTreeChildren, + GetUserTreeChildrenPayload $payload, + ): JsonResponse { + return $this->adminJson($getUserTreeChildren($payload)); } + #[IsGranted(CorePermission::Users->value)] #[Route('/user/add', name: 'opendxp_admin_user_add', methods: ['POST'])] - public function addAction(Request $request): JsonResponse - { - try { - $type = $request->request->get('type'); - - $className = User\Service::getClassNameForType($type); - $user = $className::create([ - 'parentId' => $request->request->getInt('parentId'), - 'name' => trim($request->request->get('name', '')), - 'password' => '', - 'active' => $request->request->getBoolean('active'), - ]); - - if ($request->request->has('rid')) { - $rid = (int)$request->request->get('rid'); - $rObject = $className::getById($rid); - if ($rObject && ($type === 'user' || $type === 'role')) { - $user->setParentId($rObject->getParentId()); - if ($rObject->getClasses()) { - $user->setClasses(implode(',', $rObject->getClasses())); - } - if ($rObject->getDocTypes()) { - $user->setDocTypes(implode(',', $rObject->getDocTypes())); - } - $keys = ['asset', 'document', 'object']; - foreach ($keys as $key) { - $getter = 'getWorkspaces' . ucfirst($key); - $setter = 'setWorkspaces' . ucfirst($key); - $workspaces = $rObject->$getter(); - $clonedWorkspaces = []; - if (is_array($workspaces)) { - /** @var User\Workspace\AbstractWorkspace $workspace */ - foreach ($workspaces as $workspace) { - $vars = $workspace->getObjectVars(); - if ($key === 'object') { - $workspaceClass = \OpenDxp\Model\User\Workspace\DataObject::class; - } else { - $workspaceClass = '\\OpenDxp\\Model\\User\\Workspace\\' . ucfirst($key); - } - $newWorkspace = new $workspaceClass(); - foreach ($vars as $varKey => $varValue) { - $newWorkspace->setObjectVar($varKey, $varValue); - } - $newWorkspace->setUserId($user->getId()); - $clonedWorkspaces[] = $newWorkspace; - } - } - - $user->$setter($clonedWorkspaces); - } - $user->setPerspectives($rObject->getPerspectives()); - $user->setPermissions($rObject->getPermissions()); - if ($type === 'user') { - $user->setAdmin(false); - if ($this->getAdminUser()->isAdmin()) { - $user->setAdmin($rObject->getAdmin()); - } - $user->setActive($rObject->getActive()); - $user->setRoles($rObject->getRoles()); - $user->setWelcomeScreen($rObject->getWelcomescreen()); - $user->setMemorizeTabs($rObject->getMemorizeTabs()); - $user->setCloseWarning($rObject->getCloseWarning()); - } - $user->setWebsiteTranslationLanguagesView($rObject->getWebsiteTranslationLanguagesView()); - $user->setWebsiteTranslationLanguagesEdit($rObject->getWebsiteTranslationLanguagesEdit()); - $user->save(); - } - } - - return $this->adminJson([ - 'success' => true, - 'id' => $user->getId(), - ]); - } catch (Exception $e) { - return $this->adminJson(['success' => false, 'message' => $e->getMessage()]); - } - } - - /** - * @throws Exception - */ - protected function populateChildNodes(User\AbstractUser $node, array &$currentList, bool $roleMode): array - { - $currentUser = \OpenDxp\Tool\Admin::getCurrentUser(); - - $list = $roleMode ? new User\Role\Listing() : new User\Listing(); - $list->setCondition('parentId = ?', $node->getId()); - $list->setOrder('ASC'); - $list->setOrderKey('name'); - $list->load(); - - $childList = $roleMode ? $list->getRoles() : $list->getUsers(); - - foreach ($childList as $user) { - if ($user->getId() === $currentUser->getId()) { - throw new Exception('Cannot delete current user'); - } - if ($user->getId() && $currentUser->getId() && $user->getName() !== 'system') { - $currentList[] = $user; - $this->populateChildNodes($user, $currentList, $roleMode); - } - } + public function addAction( + AddUserHandler $addUser, + AddUserPayload $payload, + ): JsonResponse { + $result = $addUser($payload); - return $currentList; + return $this->adminJson(ApiResponse::ok(['id' => $result->id])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/delete', name: 'opendxp_admin_user_delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse - { - $user = User\AbstractUser::getById((int)$request->request->get('id')); - - // only admins are allowed to delete admins and folders - // because a folder might contain an admin user, so it is simply not allowed for users with the "users" permission - if (($user instanceof User\Folder && !$this->getAdminUser()->isAdmin()) || ($user instanceof User && $user->isAdmin() && !$this->getAdminUser()->isAdmin())) { - throw new Exception('You are not allowed to delete this user'); - } - - if ($user instanceof User\Role\Folder) { - $list = [$user]; - $this->populateChildNodes($user, $list, true); - $listCount = count($list); - for ($i = $listCount - 1; $i >= 0; $i--) { - // iterate over the list from the so that nothing can get "lost" - $user = $list[$i]; - $user->delete(); - } - } elseif ($user->getId()) { - $user->delete(); - } + public function deleteAction( + DeleteUserHandler $deleteUser, + DeleteUserPayload $payload, + ): JsonResponse { + $deleteUser($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/update', name: 'opendxp_admin_user_update', methods: ['PUT'])] - public function updateAction(Request $request, TranslatorInterface $translator): JsonResponse - { - /** @var User|User\Role|null $user */ - $user = User\UserRole::getById($request->request->getInt('id')); - $currentUserIsAdmin = $this->getAdminUser()->isAdmin(); - - if (!$user) { - throw $this->createNotFoundException(); - } - - if ($user instanceof User && $user->isAdmin() && !$currentUserIsAdmin) { - throw $this->createAccessDeniedHttpException('Only admin users are allowed to modify admin users'); - } - - if ($request->request->has('data')) { - $values = $this->decodeJson($request->request->get('data'), true); - - if (!empty($values['password'])) { - if (strlen($values['password']) < 10) { - throw new Exception('Passwords have to be at least 10 characters long'); - } - $values['password'] = Tool\Authentication::getPasswordHash($user->getName(), $values['password']); - } - - // check if there are permissions transmitted, if so reset them all to false (they will be set later) - foreach ($values as $key => $value) { - if (str_starts_with($key, 'permission_')) { - $user->setAllAclToFalse(); - - break; - } - } - - if ($user instanceof User && isset($values['2fa_required'])) { - $user->setTwoFactorAuthentication('required', (bool) $values['2fa_required']); - } - - $user->setValues($values); - - // only admins are allowed to create admin users - // if the logged in user isn't an admin, set admin always to false - if ($user instanceof User && !$currentUserIsAdmin) { - $user->setAdmin(false); - } - - // check for permissions - $availableUserPermissionsList = new User\Permission\Definition\Listing(); - $availableUserPermissions = $availableUserPermissionsList->load(); - - foreach ($availableUserPermissions as $permission) { - if (isset($values['permission_' . $permission->getKey()])) { - $user->setPermission($permission->getKey(), (bool) $values['permission_' . $permission->getKey()]); - } - } - - // check for workspaces - if ($request->request->has('workspaces')) { - $processedPaths = ['object' => [], 'asset' => [], 'document' => []]; //array to find if there are multiple entries for a path - $workspaces = $this->decodeJson($request->request->get('workspaces'), true); - foreach ($workspaces as $type => $spaces) { - $newWorkspaces = []; - foreach ($spaces as $space) { - if (in_array($space['path'], $processedPaths[$type])) { - throw new Exception('Error saving workspaces as multiple entries found for path "' . $space['path'] .'" in '.$translator->trans((string)$type, [], 'admin') . 's'); - } - - $element = Element\Service::getElementByPath($type, $space['path']); - if ($element) { - $className = '\\OpenDxp\\Model\\User\\Workspace\\' . Element\Service::getBaseClassNameForElement($type); - $workspace = new $className(); - $workspace->setValues($space); - - $workspace->setCid($element->getId()); - $workspace->setCpath($element->getRealFullPath()); - $workspace->setUserId($user->getId()); - - $newWorkspaces[] = $workspace; - $processedPaths[$type][] = $space['path']; - } - } - $user->{'setWorkspaces' . ucfirst($type)}($newWorkspaces); - } - } - } - - if ($user instanceof User && $request->request->has('keyBindings')) { - $keyBindings = json_decode($request->request->get('keyBindings'), true); - $tmpArray = []; - foreach ($keyBindings as $item) { - $tmpArray[] = json_decode($item, true); - } - $tmpArray = array_values(array_filter($tmpArray)); - $tmpArray = json_encode($tmpArray); - - $user->setKeyBindings($tmpArray); - } - - $user->save(); + public function updateAction( + UpdateUserHandler $updateUser, + UpdateUserPayload $payload, + ): JsonResponse { + $updateUser($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/get', name: 'opendxp_admin_user_get', methods: ['GET'])] - public function getAction(Request $request): JsonResponse - { - $userId = (int)$request->query->get('id'); - if ($userId < 1) { - throw $this->createNotFoundException(); - } - - $user = User::getById($userId); - - if (!$user) { - throw $this->createNotFoundException(); - } - - if ($user->isAdmin() && !$this->getAdminUser()->isAdmin()) { - throw $this->createAccessDeniedHttpException('Only admin users are allowed to modify admin users'); - } - - // workspaces - $types = ['asset', 'document', 'object']; - foreach ($types as $type) { - /** @var Workspace\Document[]|Workspace\Asset[]|Workspace\DataObject[] $workspaces */ - $workspaces = $user->{'getWorkspaces' . ucfirst($type)}(); - foreach ($workspaces as $wKey => $workspace) { - $el = Element\Service::getElementById($type, $workspace->getCid()); - if ($el) { - $workspaceVars = $workspace->getObjectVars(); - $workspaceVars['path'] = $el->getRealFullPath(); - $workspaces[$wKey] = $workspaceVars; - } - } - $user->{'setWorkspaces' . ucfirst($type)}($workspaces); - } - - // object <=> user dependencies - $userObjects = DataObject\Service::getObjectsReferencingUser($user->getId()); - $userObjectData = []; - $hasHidden = false; - - foreach ($userObjects as $o) { - if ($o->isAllowed('list')) { - $userObjectData[] = [ - 'path' => $o->getRealFullPath(), - 'id' => $o->getId(), - 'subtype' => $o->getClass()->getName(), - ]; - } else { - $hasHidden = true; - } - } - - // get available permissions - $availableUserPermissionsList = new User\Permission\Definition\Listing(); - $availableUserPermissionsList->setOrderKey('category'); - $availableUserPermissions = $availableUserPermissionsList->load(); - - $availableUserPermissionsData = []; - foreach ($availableUserPermissions as $availableUserPermission) { - $availableUserPermissionsData[] = $availableUserPermission->getObjectVars(); - } - - // get available roles - $list = new User\Role\Listing(); - $list->setCondition('`type` = ?', ['role']); - $list->load(); - - $roles = []; - foreach ($list->getItems() as $role) { - $roles[] = [$role->getId(), $role->getName()]; - } - - // unset confidential informations - $userData = $user->getObjectVars(); - $userData['roles'] = $user->getRoles(); - $userData['docTypes'] = $user->getDocTypes(); - $contentLanguages = Tool\Admin::reorderWebsiteLanguages($user, Tool::getValidLanguages()); - $userData['contentLanguages'] = $contentLanguages; - $userData['twoFactorAuthentication']['isActive'] = ($user->getTwoFactorAuthentication('enabled') || $user->getTwoFactorAuthentication('secret')); - unset($userData['password'], $userData['twoFactorAuthentication']['secret']); - $userData['hasImage'] = $user->hasImage(); - - $availablePerspectives = Config::getAvailablePerspectives(null); - - return $this->adminJson([ - 'success' => true, - 'user' => $userData, - 'roles' => $roles, - 'permissions' => $user->generatePermissionList(), - 'availablePermissions' => $availableUserPermissionsData, - 'availablePerspectives' => $availablePerspectives, - 'validLanguages' => Tool::getValidLanguages(), - 'validLocales' => Tool::getSupportedJSLocales(), - 'objectDependencies' => [ - 'hasHidden' => $hasHidden, - 'dependencies' => $userObjectData, - ], - ]); + public function getAction( + GetUserHandler $getUser, + GetUserPayload $payload, + ): JsonResponse { + $result = $getUser($payload); + + return $this->adminJson(ApiResponse::ok([ + 'user' => $result->userData, + 'roles' => $result->roles, + 'permissions' => $result->permissions, + 'availablePermissions' => $result->availablePermissions, + 'availablePerspectives' => $result->availablePerspectives, + 'validLanguages' => $result->validLanguages, + 'validLocales' => $result->validLocales, + 'objectDependencies' => $result->objectDependencies, + ])); } #[Route('/user/get-minimal', name: 'opendxp_admin_user_getminimal', methods: ['GET'])] - public function getMinimalAction(Request $request): JsonResponse - { - $user = User::getById((int)$request->query->get('id')); - - if (!$user) { - throw $this->createNotFoundException(); - } - - $minimalUserData['id'] = $user->getId(); - $minimalUserData['admin'] = $user->isAdmin(); - $minimalUserData['active'] = $user->isActive(); - $minimalUserData['permissionInfo']['assets'] = $user->isAllowed('assets'); - $minimalUserData['permissionInfo']['documents'] = $user->isAllowed('documents'); - $minimalUserData['permissionInfo']['objects'] = $user->isAllowed('objects'); - - return $this->adminJson($minimalUserData); - } - - #[Route('/user/upload-current-user-image', name: 'opendxp_admin_user_uploadcurrentuserimage', methods: ['POST'])] - public function uploadCurrentUserImageAction(Request $request): JsonResponse - { - $user = $this->getAdminUser(); - - if ($user !== null) { - - if ($user->getId() === (int)$request->query->get('id')) { - return $this->uploadImageAction($request); - } - - Logger::warn('prevented save current user, because ids do not match. '); - - return $this->adminJson(false); - } - - return $this->adminJson(false); - } - - #[Route('/user/update-current-user', name: 'opendxp_admin_user_updatecurrentuser', methods: ['PUT'])] - public function updateCurrentUserAction(Request $request, ValidatorInterface $validator): JsonResponse - { - //TODO Can be completely validated with Symfony Validator - $user = $this->getAdminUser(); - - $isPasswordReset = Tool\Session::useBag($request->getSession(), static fn (AttributeBagInterface $adminSession) => (bool) $adminSession->get('password_reset')); - - if ($user !== null && $request->request->has('id')) { - if ($user->getId() === (int) $request->request->get('id')) { - $values = $this->decodeJson($request->request->get('data'), true); - - unset($values['name'], $values['id'], $values['admin'], $values['permissions'], $values['roles'], $values['active']); - - if (!empty($values['new_password'])) { - $oldPasswordCheck = false; - - if ($isPasswordReset) { - // if the user want to reset the password, the old password isn't required - $oldPasswordCheck = true; - } elseif (!empty($values['old_password'])) { - $errors = $validator->validate($values['old_password'], [new UserPassword()]); - - if (count($errors) === 0) { - $oldPasswordCheck = true; - } - } - - if (strlen($values['new_password']) < 10) { - throw new Exception('Passwords have to be at least 10 characters long'); - } - - if ($oldPasswordCheck && $values['new_password'] == $values['retype_password']) { - - if (Tool\Authentication::verifyPassword($user, $values['new_password'])) { - throw new Exception('The new password cannot be the same as the old one'); - } - - $values['password'] = Tool\Authentication::getPasswordHash($user->getName(), $values['new_password']); - } else { - if (!$oldPasswordCheck) { - return $this->adminJson(['success' => false, 'message' => 'incorrect_password']); - } - - return $this->adminJson(['success' => false, 'message' => 'password_cannot_be_changed']); - } - } - - $user->setValues($values); - - if ($request->request->has('keyBindings')) { - $keyBindings = json_decode($request->request->get('keyBindings'), true); - $tmpArray = []; - foreach ($keyBindings as $item) { - $tmpArray[] = json_decode($item, true); - } - $tmpArray = array_values(array_filter($tmpArray)); - $tmpArray = json_encode($tmpArray); - - $user->setKeyBindings($tmpArray); - } - - $user->save(); - - return $this->adminJson(['success' => true]); - } - Logger::warn('prevented save current user, because ids do not match. '); - - return $this->adminJson(false); - } - - return $this->adminJson(false); - } - - #[Route('/user/get-current-user', name: 'opendxp_admin_user_getcurrentuser', methods: ['GET'])] - public function getCurrentUserAction(Request $request): Response - { - $user = $this->getAdminUser(); - - $list = new User\Permission\Definition\Listing(); - $definitions = $list->load(); - - foreach ($definitions as $definition) { - $user->setPermission($definition->getKey(), $user->isAllowed($definition->getKey())); - } - - // unset confidential informations - $userData = $user->getObjectVars(); - $contentLanguages = Tool\Admin::reorderWebsiteLanguages($user, Tool::getValidLanguages()); - $userData['contentLanguages'] = $contentLanguages; - $userData['keyBindings'] = UserHelper::getDefaultKeyBindings($user); - - unset($userData['password']); - $userData['twoFactorAuthentication'] = $user->getTwoFactorAuthentication(); - unset($userData['twoFactorAuthentication']['secret']); - $userData['twoFactorAuthentication']['isActive'] = $user->getTwoFactorAuthentication('enabled') && $user->getTwoFactorAuthentication('secret'); - $userData['hasImage'] = $user->hasImage(); - - $userData['isPasswordReset'] = Tool\Session::useBag($request->getSession(), fn (AttributeBagInterface $adminSession) => $adminSession->get('password_reset')); - - $userData['validLocales'] = Tool::getSupportedJSLocales(); - - $response = new Response('opendxp.currentuser = ' . $this->encodeJson($userData)); - $response->headers->set('Content-Type', 'text/javascript'); - - return $response; - } - - // ROLES - - #[Route('/user/role-tree-get-children-by-id', name: 'opendxp_admin_user_roletreegetchildrenbyid', methods: ['GET'])] - public function roleTreeGetChildrenByIdAction(Request $request): JsonResponse - { - $list = new User\Role\Listing(); - $list->setCondition('parentId = ?', (int)$request->query->get('node')); - $list->load(); - - $roles = []; - foreach ($list->getItems() as $role) { - $roles[] = $this->getRoleTreeNodeConfig($role); - } - - return $this->adminJson($roles); - } - - protected function getRoleTreeNodeConfig(User\Role|User\Role\Folder $role): array - { - $tmpUser = [ - 'id' => $role->getId(), - 'text' => $role->getName(), - 'elementType' => 'role', - 'qtipCfg' => [ - 'title' => 'ID: ' . $role->getId(), - ], - ]; - - // set type specific settings - if ($role instanceof User\Role\Folder) { - $tmpUser['leaf'] = false; - $tmpUser['iconCls'] = 'opendxp_icon_folder'; - $tmpUser['expanded'] = true; - $tmpUser['allowChildren'] = true; - - if ($role->hasChildren()) { - $tmpUser['expanded'] = false; - } else { - $tmpUser['loaded'] = true; - } - } else { - $tmpUser['leaf'] = true; - $tmpUser['iconCls'] = 'opendxp_icon_roles'; - $tmpUser['allowChildren'] = false; - } - - return $tmpUser; - } - - #[Route('/user/role-get', name: 'opendxp_admin_user_roleget', methods: ['GET'])] - public function roleGetAction(Request $request): JsonResponse - { - $role = User\Role::getById((int)$request->query->get('id')); - - if (!$role) { - throw $this->createNotFoundException(); - } - - // workspaces - $types = ['asset', 'document', 'object']; - foreach ($types as $type) { - /** @var Workspace\Document[]|Workspace\Asset[]|Workspace\DataObject[] $workspaces */ - $workspaces = $role->{'getWorkspaces' . ucfirst($type)}(); - foreach ($workspaces as $wKey => $workspace) { - $el = Element\Service::getElementById($type, $workspace->getCid()); - if ($el) { - $workspaceVars = $workspace->getObjectVars(); - $workspaceVars['path'] = $el->getRealFullPath(); - $workspaces[$wKey] = $workspaceVars; - } - } - $role->{'setWorkspaces' . ucfirst($type)}($workspaces); - } - - $replaceFn = (static fn ($value) => $value->getObjectVars()); - - // get available permissions - $availableUserPermissionsList = new User\Permission\Definition\Listing(); - $availableUserPermissionsList->setOrderKey('category'); - $availableUserPermissions = $availableUserPermissionsList->load(); - $availableUserPermissions = array_map($replaceFn, $availableUserPermissions); - - $availablePerspectives = Config::getAvailablePerspectives(null); + public function getMinimalAction( + GetMinimalUserHandler $getMinimalUser, + GetMinimalUserPayload $payload, + ): JsonResponse { + $result = $getMinimalUser($payload); return $this->adminJson([ - 'success' => true, - 'role' => $role->getObjectVars(), - 'permissions' => $role->generatePermissionList(), - 'classes' => $role->getClasses(), - 'docTypes' => $role->getDocTypes(), - 'availablePermissions' => $availableUserPermissions, - 'availablePerspectives' => $availablePerspectives, - 'validLanguages' => Tool::getValidLanguages(), + 'id' => $result->id, + 'admin' => $result->admin, + 'active' => $result->active, + 'permissionInfo' => $result->permissionInfo, ]); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/upload-image', name: 'opendxp_admin_user_uploadimage', methods: ['POST'])] - public function uploadImageAction(Request $request): JsonResponse - { - $requestUserId = $request->query->has('id') ? (int)$request->query->get('id') : null; - $userObj = User::getById($this->getUserId($requestUserId)); - - if (!$userObj) { - throw $this->createNotFoundException(); - } - - if ($userObj->isAdmin() && !$this->getAdminUser()->isAdmin()) { - throw $this->createAccessDeniedHttpException('Only admin users are allowed to modify admin users'); - } - - //Check if uploaded file is an image - $avatarFile = $request->files->get('Filedata'); - - $assetType = Asset::getTypeFromMimeMapping($avatarFile->getMimeType(), $avatarFile->getFileName()); - - if (!$avatarFile instanceof UploadedFile || $assetType !== 'image') { - throw new Exception('Unsupported file format.'); - } - - $userObj->setImage($avatarFile->getPathname()); + public function uploadImageAction( + UploadUserImageHandler $uploadUserImage, + UploadUserImagePayload $payload, + ): JsonResponse { + $uploadUserImage($payload); // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in // Ext.form.Action.Submit and mark the submission as failed - - $response = $this->adminJson(['success' => true]); + $response = $this->adminJson(ApiResponse::ok()); $response->headers->set('Content-Type', 'text/html'); return $response; } - /** - * @throws Exception - */ #[Route('/user/delete-image', name: 'opendxp_admin_user_deleteimage', methods: ['DELETE'])] - public function deleteImageAction(Request $request): JsonResponse - { - $requestUserId = $request->query->has('id') ? (int)$request->query->get('id') : null; - $userObj = User::getById($this->getUserId($requestUserId)); - - if (!$userObj) { - throw $this->createNotFoundException(); - } - - $adminUser = $this->getAdminUser(); - - if (!$adminUser->isAdmin()) { - if ($userObj->isAdmin()) { - throw $this->createAccessDeniedHttpException('Only admin users are allowed to modify admin users'); - } - - if ($adminUser->getId() !== $userObj->getId()) { - throw $this->createAccessDeniedHttpException('Only admin users are allowed to modify users other than themselves'); - } - } - - $userObj->setImage(null); + public function deleteImageAction( + DeleteUserImageHandler $deleteUserImage, + DeleteUserImagePayload $payload, + ): JsonResponse { + $deleteUserImage($payload); - return $this->adminJson(['success' => true]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/user/disable-2fa', name: 'opendxp_admin_user_disable2fasecret', methods: ['DELETE'])] - public function disable2FaSecretAction(Request $request): JsonResponse - { - $user = $this->getAdminUser(); - $success = false; - - if (!$user->getTwoFactorAuthentication('required')) { - $user->setTwoFactorAuthentication([]); - $user->save(); - - $success = true; + public function disable2FaSecretAction( + Disable2FaHandler $disable2Fa, + Disable2FaPayload $payload, + ): JsonResponse { + try { + $disable2Fa($payload); + } catch (\Throwable $e) { + return $this->adminJson(ApiResponse::error($e->getMessage())); } - return $this->adminJson([ - 'success' => $success, - ]); + return $this->adminJson(ApiResponse::ok()); } + #[IsGranted(CorePermission::Users->value)] #[Route('/user/reset-2fa-secret', name: 'opendxp_admin_user_reset2fasecret', methods: ['PUT'])] - public function reset2FaSecretAction(Request $request): JsonResponse - { - $user = User::getById((int)$request->request->get('id')); - - if (!$user) { - throw $this->createNotFoundException(); - } - - $user->setTwoFactorAuthentication('enabled', false); - $user->setTwoFactorAuthentication('secret', ''); - $user->save(); - - return $this->adminJson([ - 'success' => true, - ]); - } - - #[Route('/user/reset-my-2fa-secret', name: 'opendxp_admin_user_reset_my_2fa_secret', methods: ['PUT'])] - public function resetMy2FaSecretAction(Request $request): JsonResponse - { - $user = $this->getAdminUser(); - $user->setTwoFactorAuthentication('required', true); - $user->setTwoFactorAuthentication('enabled', false); - $user->setTwoFactorAuthentication('secret', ''); - $user->save(); + public function reset2FaSecretAction( + Reset2FaSecretHandler $reset2FaSecret, + Reset2FaSecretPayload $payload, + ): JsonResponse { + $reset2FaSecret($payload); - return $this->adminJson([ - 'success' => true, - ]); + return $this->adminJson(ApiResponse::ok()); } #[Route('/user/get-image', name: 'opendxp_admin_user_getimage', methods: ['GET'])] - public function getImageAction(Request $request): StreamedResponse - { - $requestUserId = $request->query->has('id') ? (int)$request->query->get('id') : null; - $userObj = User::getById($this->getUserId($requestUserId)); - - if (!$userObj) { - throw $this->createNotFoundException(); - } - $stream = $userObj->getImage(); - - return new StreamedResponse(function () use ($stream): void { - fpassthru($stream); + public function getImageAction( + GetUserImageHandler $handler, + GetUserImagePayload $payload, + ): StreamedResponse { + $result = $handler($payload); + + return new StreamedResponse(function () use ($result): void { + fpassthru($result->image); }, 200, [ 'Content-Type' => 'image/png', ]); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/get-token-login-link', name: 'opendxp_admin_user_gettokenloginlink', methods: ['GET'])] - public function getTokenLoginLinkAction(Request $request, TranslatorInterface $translator): JsonResponse - { - $user = User::getById((int) $request->query->get('id')); - - if (!$user) { - return $this->adminJson([ - 'success' => false, - 'message' => $translator->trans('login_token_invalid_user_error', [], 'admin'), - ], Response::HTTP_NOT_FOUND); - } - - if ($user->isAdmin() && !$this->getAdminUser()->isAdmin()) { - return $this->adminJson([ - 'success' => false, - 'message' => $translator->trans('login_token_as_admin_non_admin_user_error', [], 'admin'), - ], Response::HTTP_FORBIDDEN); - } - - if (empty($user->getPassword())) { - return $this->adminJson([ - 'success' => false, - 'message' => $translator->trans('login_token_no_password_error', [], 'admin'), - ], Response::HTTP_FORBIDDEN); + public function getTokenLoginLinkAction( + GetTokenLoginLinkHandler $getTokenLoginLink, + GetTokenLoginLinkPayload $payload, + ): JsonResponse { + try { + $result = $getTokenLoginLink($payload); + } catch (\Throwable $e) { + return $this->adminJson(ApiResponse::error($e->getMessage())); } - $token = Tool\Authentication::generateTokenByUser($user); - $link = $this->generateCustomUrl([ - 'token' => $token, - ]); - - return $this->adminJson([ - 'success' => true, - 'link' => $link, - ]); + return $this->adminJson(ApiResponse::ok(['link' => $result->link])); } + #[IsGranted(CorePermission::Users->value)] #[Route('/user/search', name: 'opendxp_admin_user_search', methods: ['GET'])] - public function searchAction(Request $request): JsonResponse - { - $q = '%' . $request->query->get('query') . '%'; - - $list = new User\Listing(); - $list->setCondition('name LIKE ? OR firstname LIKE ? OR lastname LIKE ? OR email LIKE ? OR id = ?', [$q, $q, $q, $q, (int)$request->query->get('query')]); - $list->setOrder('ASC'); - $list->setOrderKey('name'); - - $users = []; - foreach ($list->getUsers() as $user) { - if ($user->getId() && $user->getName() !== 'system') { - $users[] = [ - 'id' => $user->getId(), - 'name' => $user->getName(), - 'email' => $user->getEmail(), - 'firstname' => $user->getFirstname(), - 'lastname' => $user->getLastname(), - ]; - } - } - - return $this->adminJson([ - 'success' => true, - 'users' => $users, - ]); - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - // check permissions - $unrestrictedActions = [ - 'getCurrentUserAction', 'updateCurrentUserAction', 'getAvailablePermissionsAction', 'getMinimalAction', - 'getImageAction', 'uploadCurrentUserImageAction', 'disable2FaSecretAction', 'renew2FaSecretAction', - 'getUsersForSharingAction', 'getRolesForSharingAction', 'deleteImageAction', - ]; - - $this->checkActionPermission($event, 'users', $unrestrictedActions); + public function searchAction( + SearchUsersHandler $searchUsers, + SearchUsersPayload $payload, + ): JsonResponse { + return $this->adminJson(ApiResponse::ok(['users' => $searchUsers($payload)])); } + #[IsGranted(CorePermission::ShareConfigurations->value)] #[Route('/user/get-users-for-sharing', name: 'opendxp_admin_user_getusersforsharing', methods: ['GET'])] - public function getUsersForSharingAction(Request $request): JsonResponse - { - $this->checkPermission('share_configurations'); - - return $this->getUsersAction($request); - } - - #[Route('/user/get-roles-for-sharing', name: 'opendxp_admin_user_getrolesforsharing', methods: ['GET'])] - public function getRolesForSharingAction(Request $request): JsonResponse - { - $this->checkPermission('share_configurations'); - - return $this->getRolesAction($request); + public function getUsersForSharingAction( + GetUsersHandler $getUsers, + GetUsersPayload $payload, + ): JsonResponse { + return $this->getUsersAction($getUsers, $payload); } + #[IsGranted(CorePermission::Users->value)] #[Route('/user/get-users', name: 'opendxp_admin_user_getusers', methods: ['GET'])] - public function getUsersAction(Request $request): JsonResponse - { - $users = []; - - // get available user - $list = new User\Listing(); - - $conditions = [ 'type = "user"' ]; - - if (!$request->query->get('include_current_user')) { - $conditions[] = 'id != ' . $this->getAdminUser()->getId(); - } - - $list->setCondition(implode(' AND ', $conditions)); - - $list->load(); - $userList = $list->getUsers(); - - foreach ($userList as $user) { - if (!$request->query->get('permission') || $user->isAllowed($request->query->get('permission'))) { - $users[] = [ - 'id' => $user->getId(), - 'label' => $user->getUsername(), - ]; - } - } - - return $this->adminJson(['success' => true, 'total' => count($users), 'data' => $users]); - } - - #[Route('/user/get-roles', name: 'opendxp_admin_user_getroles', methods: ['GET'])] - public function getRolesAction(Request $request): JsonResponse - { - $roles = []; - $list = new User\Role\Listing(); - - $list->setCondition('`type` = "role"'); - $list->load(); - $roleList = $list->getRoles(); - - foreach ($roleList as $role) { - if (!$request->query->has('permission') || in_array($request->query->get('permission'), $role->getPermissions())) { - $roles[] = [ - 'id' => $role->getId(), - 'label' => $role->getName(), - ]; - } - } - - return $this->adminJson(['success' => true, 'total' => count($roles), 'data' => $roles]); - } + public function getUsersAction( + GetUsersHandler $getUsers, + GetUsersPayload $payload, + ): JsonResponse { + $users = $getUsers($payload); - #[Route('/user/get-default-key-bindings', name: 'opendxp_admin_user_getdefaultkeybindings', methods: ['GET'])] - public function getDefaultKeyBindingsAction(Request $request): JsonResponse - { - return $this->adminJson(['success' => true, 'data' => UserHelper::getDefaultKeyBindings()]); + return $this->adminJson(ApiResponse::ok(['total' => count($users), 'data' => $users])); } - /** - * @throws Exception - */ + #[IsGranted(CorePermission::Users->value)] #[Route('/user/invitationlink', name: 'opendxp_admin_user_invitationlink', methods: ['POST'])] public function invitationLinkAction( - Request $request, - TranslatorInterface $translator, - RouterInterface $router, - GeneralHostResolver $generalHostResolver + SendInvitationLinkHandler $sendInvitationLink, + SendInvitationLinkPayload $payload, ): JsonResponse { + $result = $sendInvitationLink($payload); - $success = false; - $message = ''; - - if ($username = $request->request->get('username')) { - $user = User::getByName($username); - if ($user instanceof User) { - if (!$user->isActive()) { - $message .= 'User inactive
'; - } - - if (!$user->getEmail()) { - $message .= 'User has no email address
'; - } - } else { - $message .= 'User unknown
'; - } - - if (empty($message)) { - //generate random password if user has no password - if (!$user->getPassword()) { - $user->setPassword(bin2hex(random_bytes(16))); - $user->save(); - } - - $token = Tool\Authentication::generateTokenByUser($user); - - $domain = $generalHostResolver->resolve(['source' => $request]); - if (!$domain) { - throw new Exception('No main domain set in system settings, unable to generate login invitation link'); - } - - $context = $router->getContext(); - $context->setHost($domain); - - $loginUrl = $this->generateCustomUrl([ - 'token' => $token, - 'reset' => true, - ]); - - try { - $mail = Tool::getMail([$user->getEmail()], 'OpenDXP login invitation for ' . Tool::getHostname()); - $mail->setIgnoreDebugMode(true); - $mail->text("Login to OpenDXP and change your password using the following link. This temporary login link will expire in 24 hours: \r\n\r\n" . $loginUrl); - $mail->send(); - - $success = true; - $message = sprintf($translator->trans('invitation_link_sent', [], 'admin_ext'), $user->getEmail()); - } catch (Exception) { - $message .= 'could not send email'; - } - } - } - - return $this->adminJson([ - 'success' => $success, - 'message' => $message, - ]); - } - - protected function getUserId(?int $requestedUserId): int - { - $currentAdminUserId = $this->getAdminUser()->getId(); - - if ($requestedUserId !== null) { - - if ($currentAdminUserId !== $requestedUserId) { - $this->checkPermission('users'); - } - - return $requestedUserId; - } - - return $currentAdminUserId; - } - - /** - * @param int $referenceType //UrlGeneratorInterface::ABSOLUTE_URL, ABSOLUTE_PATH, RELATIVE_PATH, NETWORK_PATH - * - * @return string The generated URL - */ - private function generateCustomUrl(array $params, string $fallbackUrl = 'opendxp_admin_login_check', int $referenceType = UrlGeneratorInterface::ABSOLUTE_URL): string - { - try { - $adminEntryPointRoute = $this->getParameter('opendxp_admin.custom_admin_route_name'); - - //try to generate invitation link for custom admin point - $loginUrl = $this->generateUrl($adminEntryPointRoute, $params, $referenceType); - } catch (Exception) { - //use default login check for invitation link - $loginUrl = $this->generateUrl($fallbackUrl, $params, $referenceType); - } - - return $loginUrl; + return $this->adminJson(ApiResponse::fromBool($result->success, ['message' => $result->message])); } } diff --git a/src/Controller/Admin/WorkflowController.php b/src/Controller/Admin/WorkflowController.php index ec898955..14c9195d 100644 --- a/src/Controller/Admin/WorkflowController.php +++ b/src/Controller/Admin/WorkflowController.php @@ -1,5 +1,4 @@ getWorkflowIfExists($this->element, (string) $request->request->get('workflowName')); - - if (empty($workflow)) { - $wfConfig = [ - 'message' => 'workflow not found', - ]; - } else { - //this is the default returned workflow data - $wfConfig = [ - 'message' => '', - 'notes_enabled' => false, - 'notes_required' => false, - 'additional_fields' => [], - ]; - - $enabledTransitions = $workflow->getEnabledTransitions($this->element); - $transition = null; - foreach ($enabledTransitions as $_transition) { - if ($_transition->getName() === $request->request->get('transitionName')) { - $transition = $_transition; - } - } + $result = $getWorkflowForm($payload); - if (!$transition instanceof Transition) { - $wfConfig['message'] = sprintf('transition %s currently not allowed', (string) $request->request->get('transitionName')); - } else { - $wfConfig['notes_required'] = $transition->getNotesCommentRequired(); - $wfConfig['additional_fields'] = []; - } - } + $wfConfig = [ + 'message' => $result->message, + 'notes_enabled' => $result->notesEnabled, + 'notes_required' => $result->notesRequired, + 'additional_fields' => $result->additionalFields, + ]; } catch (Exception $e) { - $wfConfig['message'] = $e->getMessage(); + $wfConfig = ['message' => $e->getMessage()]; } return $this->adminJson($wfConfig); } #[Route('/submit-workflow-transition', name: 'opendxp_admin_workflow_submitworkflowtransition', methods: ['POST'])] - public function submitWorkflowTransitionAction(Request $request, Registry $workflowRegistry, Manager $workflowManager): JsonResponse - { - $workflowOptions = $request->request->all('workflow'); - $workflow = $workflowRegistry->get($this->element, $request->request->get('workflowName')); - - if ($workflow->can($this->element, $request->request->get('transition'))) { - try { - $workflowManager->applyWithAdditionalData($workflow, $this->element, $request->request->get('transition'), $workflowOptions, true); - - $data = [ - 'success' => true, - 'callback' => 'reloadObject', - ]; - } catch (ValidationException $e) { - $reason = ''; - if (count($e->getSubItems()) > 0) { - $reason = ''; - } - - $data = [ - 'success' => false, - 'message' => $e->getMessage(), - 'reasons' => [$reason], + public function submitWorkflowTransitionAction( + SubmitWorkflowTransitionPayload $payload, + SubmitWorkflowTransitionHandler $submitWorkflowTransition, + ): JsonResponse { + try { + $result = $submitWorkflowTransition($payload); - ]; - } catch (Exception $e) { - $data = [ - 'success' => false, - 'message' => 'error performing action on this element', - 'reasons' => [$e->getMessage()], - ]; + if ($result->blocked) { + return $this->adminJson(ApiResponse::error('transition failed', ['reasons' => $result->blockerReasons])); } - } else { - $blockTransitionList = $workflow->buildTransitionBlockerList($this->element, $request->request->get('transition')); - $reasons = array_map(static fn ($blockTransitionItem) => $blockTransitionItem->getMessage(), iterator_to_array($blockTransitionList->getIterator(), true)); + return $this->adminJson(ApiResponse::ok(['callback' => 'reloadObject'])); + } catch (ValidationException $e) { + $reason = ''; + if (count($e->getSubItems()) > 0) { + $reason = ''; + } - $data = [ - 'success' => false, - 'message' => 'transition failed', - 'reasons' => $reasons, - ]; + return $this->adminJson(ApiResponse::error($e->getMessage(), ['reasons' => [$reason]])); + } catch (Exception $e) { + return $this->adminJson(ApiResponse::error('error performing action on this element', ['reasons' => [$e->getMessage()]])); } - - return $this->adminJson($data); } #[Route('/submit-global-action', name: 'opendxp_admin_workflow_submitglobal', methods: ['POST'])] public function submitGlobalAction( - Request $request, - Registry $workflowRegistry, - Manager $workflowManager + SubmitGlobalActionPayload $payload, + SubmitGlobalActionHandler $submitGlobalAction, ): JsonResponse { - $workflowOptions = $request->request->all('workflow'); - $workflow = $workflowRegistry->get($this->element, $request->request->get('workflowName')); - - $globalAction = $workflowManager->getGlobalAction( - $request->request->get('workflowName'), - $request->request->get('transition') - ); - $saveSubject = !$globalAction || $globalAction->getSaveSubject(); - try { - $workflowManager->applyGlobalAction( - $workflow, - $this->element, $request->request->get('transition'), - $workflowOptions, $saveSubject - ); + $submitGlobalAction($payload); - $data = [ - 'success' => true, - 'callback' => 'reloadObject', - ]; + return $this->adminJson(ApiResponse::ok(['callback' => 'reloadObject'])); } catch (ValidationException $e) { $reason = ''; if (count($e->getSubItems()) > 0) { $reason = ''; } - $data = [ - 'success' => false, - 'message' => $e->getMessage(), - 'reasons' => [$reason], - - ]; + return $this->adminJson(ApiResponse::error($e->getMessage(), ['reasons' => [$reason]])); } catch (Exception $e) { - $data = [ - 'success' => false, - 'message' => 'error performing action on this element', - 'reasons' => [$e->getMessage()], - ]; + return $this->adminJson(ApiResponse::error('error performing action on this element', ['reasons' => [$e->getMessage()]])); } - - return $this->adminJson($data); } - /** - * Returns the JSON needed by the workflow elements detail tab store - * - * @throws Exception - */ #[Route('/get-workflow-details', name: 'opendxp_admin_workflow_getworkflowdetailsstore')] - public function getWorkflowDetailsStore(Request $request, Manager $workflowManager, StatusInfo $placeStatusInfo, RouterInterface $router, ActionsButtonService $actionsButtonService): JsonResponse - { - $data = []; - - foreach ($workflowManager->getAllWorkflowsForSubject($this->element) as $workflow) { - $workflowConfig = $workflowManager->getWorkflowConfig($workflow->getName()); - - $svg = null; - $msg = ''; - - try { - $svg = $this->getWorkflowSvg($workflow); - } catch (InvalidArgumentException $e) { - $msg = $e->getMessage(); - } - - $url = $router->generate( - 'opendxp_admin_workflow_show_graph', - [ - 'cid' => $request->query->get('cid'), - 'ctype' => $request->query->get('ctype'), - 'workflow' => $workflow->getName(), - ] - ); - - $allowedTransitions = $actionsButtonService->getAllowedTransitions($workflow, $this->element); - $globalActions = $actionsButtonService->getGlobalActions($workflow, $this->element); - - $data[] = [ - 'workflowName' => $this->translator->trans($workflowConfig->getLabel(), [], 'admin'), - 'placeInfo' => $placeStatusInfo->getAllPalacesHtml($this->element, $workflow->getName()), - 'graph' => $msg ?: '
'.$svg.'
', - 'allowedTransitions' => $allowedTransitions, - 'globalActions' => $globalActions, - ]; - } + public function getWorkflowDetailsStore( + GetWorkflowDetailsPayload $payload, + GetWorkflowDetailsHandler $getWorkflowDetails, + ): JsonResponse { + $result = $getWorkflowDetails($payload); - return $this->adminJson([ - 'data' => $data, - 'success' => true, - 'total' => count($data), - ]); + return $this->adminJson(ApiResponse::ok(['data' => $result->data, 'total' => count($result->data)])); } - /** - * Returns the JSON needed by the workflow elements detail tab store - * - * @throws Exception - */ #[Route('/show-graph', name: 'opendxp_admin_workflow_show_graph', methods: ['GET'])] - public function showGraph(Request $request, Manager $workflowManager): Response - { - $workflow = $workflowManager->getWorkflowByName($request->query->get('workflow')); + public function showGraph( + ShowGraphPayload $payload, + GetWorkflowSvgHandler $getWorkflowSvg, + ): Response { + $svg = $getWorkflowSvg($payload); - $response = new Response($this->getWorkflowSvg($workflow)); + $response = new Response($svg); $response->headers->set('Content-Type', 'image/svg+xml'); return $response; } - /** - * Get custom HTML for the workflow transition submit modal, depending whether it is configured or not. - * - * @throws Exception - */ #[Route('/modal-custom-html', name: 'opendxp_admin_workflow_modal_custom_html', methods: ['POST'])] - public function getModalCustomHtml(Request $request, Registry $workflowRegistry, Manager $manager): JsonResponse + public function getModalCustomHtml( + GetModalCustomHtmlPayload $payload, + GetModalCustomHtmlHandler $getModalCustomHtml, + ): JsonResponse { - $workflow = $workflowRegistry->get($this->element, $request->request->get('workflowName')); - - if ($request->request->get('isGlobalAction') === 'true') { - $globalAction = $manager->getGlobalAction($workflow->getName(), $request->request->get('transition')); - if ($globalAction) { - return $this->customHtmlResponse($globalAction->getCustomHtmlService()); - } - } elseif ($workflow->can($this->element, $request->request->get('transition'))) { - $enabledTransitions = $workflow->getEnabledTransitions($this->element); - $transition = null; - foreach ($enabledTransitions as $_transition) { - if ($_transition->getName() === $request->request->get('transition')) { - $transition = $_transition; - } - } - - if ($transition instanceof Transition) { - return $this->customHtmlResponse($transition->getCustomHtmlService()); - } - } - - $data = [ - 'success' => false, - 'message' => 'error validating the action on this element, element cannot peform this action', - ]; - - return new JsonResponse($data); - } - - private function customHtmlResponse(?CustomHtmlServiceInterface $customHtmlService): JsonResponse - { - $data = [ - 'success' => true, - 'customHtml' => [], - ]; - - if ($customHtmlService) { - foreach (['top', 'center', 'bottom'] as $position) { - $data['customHtml'][$position] = $customHtmlService->renderHtmlForRequestedPosition($this->element, $position); - } - } - - return new JsonResponse($data); - } - - /** - * @throws Exception - */ - private function getWorkflowSvg(WorkflowInterface $workflow): string - { - $marking = $workflow->getMarking($this->element); - - $php = Console::getExecutable('php'); - $dot = Console::getExecutable('dot'); - - if (!$php) { - throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['php'], 'admin')); - } - - if (!$dot) { - throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['dot'], 'admin')); - } - - $cmd = $php . ' ' . OPENDXP_PROJECT_ROOT . '/bin/console opendxp:workflow:dump ${WNAME} ${WPLACES} | ${DOT} -Tsvg'; - $params = [ - 'WNAME' => $workflow->getName(), - 'WPLACES' => implode(' ', array_keys($marking->getPlaces())), - 'DOT' => $dot, - ]; - - Console::addLowProcessPriority($cmd); - $process = Process::fromShellCommandline($cmd); - $process->run(null, $params); - - return $process->getOutput(); - } - - /** - * @template T of Document|Asset|DataObject - * - * @param T $element - * - * @return T - */ - protected function getLatestVersion(mixed $element): mixed - { - if ( - $element instanceof Document\Folder - || $element instanceof Asset\Folder - || $element instanceof DataObject\Folder - || $element instanceof Document\Hardlink - || $element instanceof Document\Link - ) { - return $element; - } - - //TODO move this maybe to a service method, since this is also used in DataObjectController and DocumentControllers - if ($element instanceof Document\PageSnippet) { - $latestVersion = $element->getLatestVersion(); - if ($latestVersion) { - $latestDoc = $latestVersion->loadData(); - if ($latestDoc instanceof Document\PageSnippet) { - $element = $latestDoc; - } - } - } - - if ($element instanceof DataObject\Concrete) { - $latestVersion = $element->getLatestVersion(); - if ($latestVersion) { - $latestObj = $latestVersion->loadData(); - if ($latestObj instanceof ConcreteObject) { - $element = $latestObj; - } - } - } - - return $element; - } - - /** - * @throws Exception - */ - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $request = $event->getRequest(); - - $ctype = $request->request->get('ctype') ?? $request->query->get('ctype'); - $cid = $request->request->get('cid') ?? $request->query->get('cid'); - - $this->element = match ($ctype) { - 'document' => Document::getById((int) $cid), - 'asset' => Asset::getById((int) $cid), - 'object' => ConcreteObject::getById((int) $cid), - default => null, - }; + try { + $result = $getModalCustomHtml($payload); - if ($this->element === null) { - throw new Exception('Cannot load element' . $cid . ' of type \'' . $ctype . '\''); + return $this->adminJson(ApiResponse::ok(['customHtml' => $result->customHtml])); + } catch (Exception $e) { + return $this->adminJson(ApiResponse::error($e->getMessage())); } - - //get the latest available version of the element - - $this->element = $this->getLatestVersion($this->element); - $this->element->setUserModification($this->getAdminUser()->getId()); } } diff --git a/src/Controller/AdminAbstractController.php b/src/Controller/AdminAbstractController.php index 2808a29f..82302267 100644 --- a/src/Controller/AdminAbstractController.php +++ b/src/Controller/AdminAbstractController.php @@ -15,6 +15,7 @@ namespace OpenDxp\Bundle\AdminBundle\Controller; +use JsonSerializable; use OpenDxp\Controller\Traits\JsonHelperTrait; use OpenDxp\Controller\UserAwareController; use OpenDxp\Model\User; @@ -33,6 +34,10 @@ abstract class AdminAbstractController extends UserAwareController */ protected function adminJson(mixed $data, int $status = 200, array $headers = [], array $context = [], bool $useAdminSerializer = true): JsonResponse { + if ($data instanceof JsonSerializable) { + $data = $data->jsonSerialize(); + } + return $this->jsonResponse($data, $status, $headers, $context, $useAdminSerializer); } diff --git a/src/Controller/GDPR/AdminController.php b/src/Controller/GDPR/AdminController.php index 7b87f545..bdaf2839 100644 --- a/src/Controller/GDPR/AdminController.php +++ b/src/Controller/GDPR/AdminController.php @@ -18,37 +18,22 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\GDPR; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\GDPR\DataProvider\Manager; -use OpenDxp\Controller\KernelControllerEventInterface; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\GetDataProviders\GetDataProvidersHandler; +use OpenDxp\Bundle\AdminBundle\Payload\Common\EmptyPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ -class AdminController extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(AdminPermission::GdprDataExtractor->value)] +class AdminController extends AdminAbstractController { #[Route('/get-data-providers', name: 'opendxp_admin_gdpr_admin_getdataproviders', methods: ['GET'])] - public function getDataProvidersAction(Manager $manager): JsonResponse + public function getDataProvidersAction(GetDataProvidersHandler $handler, EmptyPayload $payload): JsonResponse { - $response = []; - foreach ($manager->getServices() as $service) { - $response[] = [ - 'name' => $service->getName(), - 'jsClass' => $service->getJsClassName(), - ]; - } - - return $this->adminJson($response); - } - - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'gdpr_data_extractor'); + return $this->adminJson($handler($payload)->providers); } } diff --git a/src/Controller/GDPR/AssetController.php b/src/Controller/GDPR/AssetController.php index b3f84d01..7bcbfffe 100644 --- a/src/Controller/GDPR/AssetController.php +++ b/src/Controller/GDPR/AssetController.php @@ -16,64 +16,33 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\GDPR; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\GDPR\DataProvider\Assets; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Model\Asset; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\Asset\ExportAsset\ExportAssetHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\Asset\SearchAssets\SearchAssetsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\SearchDataPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/asset')] -class AssetController extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(AdminPermission::GdprDataExtractor->value)] +class AssetController extends AdminAbstractController { - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'gdpr_data_extractor'); - } - #[Route('/search-assets', name: 'opendxp_admin_gdpr_asset_searchasset', methods: ['GET'])] - public function searchAssetAction(Request $request, Assets $service): JsonResponse + public function searchAssetAction(SearchAssetsHandler $handler, SearchDataPayload $payload): JsonResponse { - $allParams = $request->query->all(); - - $result = $service->searchData( - (int)$allParams['id'], - strip_tags($allParams['firstname']), - strip_tags($allParams['lastname']), - strip_tags($allParams['email']), - (int)$allParams['start'], - (int)$allParams['limit'], - $allParams['sort'] ?? null - ); - - return $this->adminJson($result); + return $this->adminJson($handler($payload)->data); } - /** - * @throws Exception - */ #[Route('/export', name: 'opendxp_admin_gdpr_asset_exportassets', methods: ['GET'])] - public function exportAssetsAction(Request $request, Assets $service): Response + public function exportAssetsAction(ExportAssetHandler $handler, IdQueryPayload $payload): Response { - $asset = Asset::getById((int) $request->query->get('id')); - if (!$asset) { - throw $this->createNotFoundException('Asset not found'); - } - if (!$asset->isAllowed('view')) { - throw $this->createAccessDeniedException('Export denied'); - } - - return $service->doExportData($asset); + return $handler($payload)->response; } } diff --git a/src/Controller/GDPR/DataObjectController.php b/src/Controller/GDPR/DataObjectController.php index 8a32dc2d..f94374c8 100644 --- a/src/Controller/GDPR/DataObjectController.php +++ b/src/Controller/GDPR/DataObjectController.php @@ -16,71 +16,38 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\GDPR; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\GDPR\DataProvider\DataObjects; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Model\DataObject; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\DataObject\ExportDataObject\ExportDataObjectHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\DataObject\SearchDataObjects\SearchDataObjectsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\SearchDataPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/data-object')] -class DataObjectController extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(AdminPermission::GdprDataExtractor->value)] +class DataObjectController extends AdminAbstractController { - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'gdpr_data_extractor'); - } - #[Route('/search-data-objects', name: 'opendxp_admin_gdpr_dataobject_searchdataobjects', methods: ['GET'])] - public function searchDataObjectsAction(Request $request, DataObjects $service): JsonResponse + public function searchDataObjectsAction(SearchDataObjectsHandler $handler, SearchDataPayload $payload): JsonResponse { - $allParams = $request->query->all(); - - $result = $service->searchData( - (int)$allParams['id'], - strip_tags($allParams['firstname']), - strip_tags($allParams['lastname']), - strip_tags($allParams['email']), - (int)$allParams['start'], - (int)$allParams['limit'], - $allParams['sort'] ?? null - ); - - return $this->adminJson($result); + return $this->adminJson($handler($payload)->data); } - /** - * @throws Exception - */ #[Route('/export', name: 'opendxp_admin_gdpr_dataobject_exportdataobject', methods: ['GET'])] - public function exportDataObjectAction(Request $request, DataObjects $service): JsonResponse + public function exportDataObjectAction(ExportDataObjectHandler $handler, IdQueryPayload $payload): JsonResponse { - $object = DataObject::getById((int) $request->query->get('id')); - - if (!$object) { - throw $this->createNotFoundException('Object not found'); - } - - if (!$object->isAllowed('view')) { - throw $this->createAccessDeniedException('Export denied'); - } - - $exportResult = $service->doExportData($object); + $result = $handler($payload); - $json = $this->encodeJson($exportResult, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); + $json = $this->encodeJson($result->data, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); return new JsonResponse($json, 200, [ - 'Content-Disposition' => 'attachment; filename="export-data-object-' . $object->getId() . '.json"', + 'Content-Disposition' => 'attachment; filename="export-data-object-' . $result->objectId . '.json"', ], true); } } diff --git a/src/Controller/GDPR/OpenDxpUsersController.php b/src/Controller/GDPR/OpenDxpUsersController.php index 4a8d1a61..f283e62c 100644 --- a/src/Controller/GDPR/OpenDxpUsersController.php +++ b/src/Controller/GDPR/OpenDxpUsersController.php @@ -17,12 +17,14 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\GDPR; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Bundle\AdminBundle\GDPR\DataProvider\OpenDxpUsers; -use OpenDxp\Controller\KernelControllerEventInterface; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\OpenDxpUsers\ExportUserData\ExportUserDataHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\OpenDxpUsers\SearchUsers\SearchUsersHandler; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\SearchDataPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * Class OpenDxpController @@ -30,45 +32,25 @@ * @internal */ #[Route('/opendxp-users')] -class OpenDxpUsersController extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(AdminPermission::GdprDataExtractor->value)] +class OpenDxpUsersController extends AdminAbstractController { - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'gdpr_data_extractor'); - } - #[Route('/search-users', name: 'opendxp_admin_gdpr_opendxpusers_searchusers', methods: ['GET'])] - public function searchUsersAction(Request $request, OpenDxpUsers $openDxpUsers): JsonResponse + public function searchUsersAction(SearchUsersHandler $handler, SearchDataPayload $payload): JsonResponse { - $allParams = $request->query->all(); - - $result = $openDxpUsers->searchData( - (int)$allParams['id'], - strip_tags($allParams['firstname']), - strip_tags($allParams['lastname']), - strip_tags($allParams['email']), - (int)$allParams['start'], - (int)$allParams['limit'], - $allParams['sort'] ?? null - ); - - return $this->adminJson($result); + return $this->adminJson($handler($payload)->data); } #[Route('/export-user-data', name: 'opendxp_admin_gdpr_opendxpusers_exportuserdata', methods: ['GET'])] - public function exportUserDataAction(Request $request, OpenDxpUsers $openDxpUsers): JsonResponse + public function exportUserDataAction(ExportUserDataHandler $handler, IdQueryPayload $payload): JsonResponse { $this->checkPermission('users'); - $userData = $openDxpUsers->getExportData((int)$request->query->get('id')); + $result = $handler($payload); - $json = $this->encodeJson($userData, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); + $json = $this->encodeJson($result->data, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); return new JsonResponse($json, 200, [ - 'Content-Disposition' => 'attachment; filename="export-userdata-' . $userData['id'] . '.json"', + 'Content-Disposition' => 'attachment; filename="export-userdata-' . $result->data['id'] . '.json"', ], true); } } diff --git a/src/Controller/GDPR/SentMailController.php b/src/Controller/GDPR/SentMailController.php index ed04216e..3e6e3a0a 100644 --- a/src/Controller/GDPR/SentMailController.php +++ b/src/Controller/GDPR/SentMailController.php @@ -17,46 +17,30 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\GDPR; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; -use OpenDxp\Controller\KernelControllerEventInterface; -use OpenDxp\Model\Tool\Email\Log; +use OpenDxp\Bundle\AdminBundle\Handler\GDPR\SentMail\ExportSentMail\ExportSentMailHandler; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ #[Route('/sent-mail')] -class SentMailController extends AdminAbstractController implements KernelControllerEventInterface +#[IsGranted(AdminPermission::GdprDataExtractor->value)] +class SentMailController extends AdminAbstractController { - public function onKernelControllerEvent(ControllerEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $this->checkActionPermission($event, 'gdpr_data_extractor'); - } - #[Route('/export', name: 'opendxp_admin_gdpr_sentmail_exportdataobject', methods: ['GET'])] - public function exportDataObjectAction(Request $request): JsonResponse + public function exportDataObjectAction(ExportSentMailHandler $handler, IdQueryPayload $payload): JsonResponse { $this->checkPermission('emails'); + $result = $handler($payload); - $sentMail = Log::getById((int) $request->query->get('id')); - if (!$sentMail) { - throw $this->createNotFoundException(); - } - - $sentMailArray = (array)$sentMail; - $sentMailArray['htmlBody'] = $sentMail->getHtmlLog(); - $sentMailArray['textBody'] = $sentMail->getTextLog(); - - $json = $this->encodeJson($sentMailArray, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); + $json = $this->encodeJson($result->data, [], JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_PRETTY_PRINT); return new JsonResponse($json, 200, [ - 'Content-Disposition' => 'attachment; filename="export-mail-' . $sentMail->getId() . '.json"', + 'Content-Disposition' => 'attachment; filename="export-mail-' . $result->mailId . '.json"', ], true); } } diff --git a/src/Controller/Traits/ApplySchedulerDataTrait.php b/src/Controller/Traits/ApplySchedulerDataTrait.php deleted file mode 100644 index b61aa440..00000000 --- a/src/Controller/Traits/ApplySchedulerDataTrait.php +++ /dev/null @@ -1,51 +0,0 @@ -request->has('scheduler')) { - $tasks = []; - $tasksData = $this->decodeJson($request->request->get('scheduler')); - - if (!empty($tasksData)) { - foreach ($tasksData as $taskData) { - $taskData['userId'] = $adminUser?->getId(); - - $task = new Task($taskData); - $tasks[] = $task; - } - } - - if ($element->isAllowed('settings') && method_exists($element, 'setScheduledTasks')) { - $element->setScheduledTasks($tasks); - } - } - } -} diff --git a/src/Controller/Traits/DocumentTreeConfigTrait.php b/src/Controller/Traits/DocumentTreeConfigTrait.php index c78b5157..7c91e3db 100644 --- a/src/Controller/Traits/DocumentTreeConfigTrait.php +++ b/src/Controller/Traits/DocumentTreeConfigTrait.php @@ -16,7 +16,6 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Traits; -use Exception; use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; use OpenDxp\Model\Element\ElementInterface; use Symfony\Contracts\Service\Attribute\Required; @@ -28,8 +27,6 @@ */ trait DocumentTreeConfigTrait { - use AdminStyleTrait; - protected ElementServiceInterface $elementService; #[Required] @@ -38,9 +35,6 @@ public function setElementService(ElementServiceInterface $elementService): void $this->elementService = $elementService; } - /** - * @throws Exception - */ public function getTreeNodeConfig(ElementInterface $element): array { return $this->elementService->getElementTreeNodeConfig($element); diff --git a/src/Controller/Traits/UserNameTrait.php b/src/Controller/Traits/UserNameTrait.php deleted file mode 100644 index 065985d6..00000000 --- a/src/Controller/Traits/UserNameTrait.php +++ /dev/null @@ -1,64 +0,0 @@ -translator = $translator; - } - - /** - * @param int|null $userId The User ID. - * - * @return array{userName: string, fullName: string} - */ - protected function getUserName(?int $userId = null): array - { - if ($userId === null) { - return [ - 'userName' => '', - 'fullName' => $this->translator->trans('user_unknown', [], 'admin'), - ]; - } - - $user = User::getById($userId); - - if (empty($user)) { - return [ - 'userName' => '', - 'fullName' => $this->translator->trans('user_unknown', [], 'admin'), - ]; - } - - return [ - 'userName' => $user->getName(), - 'fullName' => (empty($user->getFullName()) ? $user->getName() : $user->getFullName()), - ]; - } -} diff --git a/src/Dto/Admin/AdminSettingsDto.php b/src/Dto/Admin/AdminSettingsDto.php new file mode 100644 index 00000000..f1273332 --- /dev/null +++ b/src/Dto/Admin/AdminSettingsDto.php @@ -0,0 +1,115 @@ + $this->success]; + if ($this->message !== null) { + $data['message'] = $this->message; + } + + return array_merge($data, $this->extra); + } +} diff --git a/src/Enricher/DataObject/CustomLayoutEnricher.php b/src/Enricher/DataObject/CustomLayoutEnricher.php new file mode 100644 index 00000000..5b8d1522 --- /dev/null +++ b/src/Enricher/DataObject/CustomLayoutEnricher.php @@ -0,0 +1,54 @@ +getClass()->getFieldDefinitions()), true); + + if (is_array($layoutArray)) { + $this->injectValuesForCustomLayout($layoutArray, $classFieldDefinitions); + } + + $data['layout'] = $layoutArray; + } + + private function injectValuesForCustomLayout(array &$layout, array $classFieldDefinitions): void + { + foreach ($layout['children'] as &$child) { + if ($child['datatype'] === 'layout') { + $this->injectValuesForCustomLayout($child, $classFieldDefinitions); + } else { + foreach ($classFieldDefinitions[$child['name']] as $key => $value) { + if (array_key_exists($key, $child) && ($child[$key] === null || $child[$key] === '' || (is_array($child[$key]) && empty($child[$key])))) { + $child[$key] = $value; + } + } + } + } + } +} diff --git a/src/Enricher/DataObject/DraftEnricher.php b/src/Enricher/DataObject/DraftEnricher.php new file mode 100644 index 00000000..0af4d05e --- /dev/null +++ b/src/Enricher/DataObject/DraftEnricher.php @@ -0,0 +1,40 @@ +getId(), ['force' => true]); + if ($fresh->getModificationDate() < $draftVersion->getDate()) { + $data['draft'] = [ + 'id' => $draftVersion->getId(), + 'modificationDate' => $draftVersion->getDate(), + 'isAutoSave' => $draftVersion->isAutoSave(), + ]; + } + } +} diff --git a/src/Enricher/Document/DocumentMetaEnricher.php b/src/Enricher/Document/DocumentMetaEnricher.php new file mode 100644 index 00000000..791707b2 --- /dev/null +++ b/src/Enricher/Document/DocumentMetaEnricher.php @@ -0,0 +1,36 @@ +getId(), ['force' => true]); + $data['versionDate'] = $fresh->getModificationDate(); + $data['userPermissions'] = $document->getUserPermissions(); + $data['idPath'] = ElementService::getIdPath($document); + $data['php'] = [ + 'classes' => [$document::class, ...array_values(class_parents($document))], + 'interfaces' => array_values(class_implements($document)), + ]; + } +} diff --git a/src/Enricher/Document/DraftEnricher.php b/src/Enricher/Document/DraftEnricher.php new file mode 100644 index 00000000..45a1ae4c --- /dev/null +++ b/src/Enricher/Document/DraftEnricher.php @@ -0,0 +1,40 @@ +getId(), ['force' => true]); + if ($fresh->getModificationDate() < $draftVersion->getDate()) { + $data['draft'] = [ + 'id' => $draftVersion->getId(), + 'modificationDate' => $draftVersion->getDate(), + 'isAutoSave' => $draftVersion->isAutoSave(), + ]; + } + } +} diff --git a/src/Enricher/Document/PropertiesEnricher.php b/src/Enricher/Document/PropertiesEnricher.php new file mode 100644 index 00000000..b01d5399 --- /dev/null +++ b/src/Enricher/Document/PropertiesEnricher.php @@ -0,0 +1,29 @@ +getProperties()); + } +} diff --git a/src/Enricher/Document/TranslationEnricher.php b/src/Enricher/Document/TranslationEnricher.php new file mode 100644 index 00000000..e01bbd4c --- /dev/null +++ b/src/Enricher/Document/TranslationEnricher.php @@ -0,0 +1,34 @@ +getTranslations($document); + $unlinkTranslations = $service->getTranslations($document, 'unlink'); + $language = $document->getProperty('language'); + unset($translations[$language], $unlinkTranslations[$language]); + $data['translations'] = $translations; + $data['unlinkTranslations'] = $unlinkTranslations; + } +} diff --git a/src/Enricher/Element/AdminStyleEnricher.php b/src/Enricher/Element/AdminStyleEnricher.php new file mode 100644 index 00000000..4df29305 --- /dev/null +++ b/src/Enricher/Element/AdminStyleEnricher.php @@ -0,0 +1,66 @@ +dispatch($event, AdminEvents::RESOLVE_ELEMENT_ADMIN_STYLE); + $this->applyStyle($event->getAdminStyle(), $data); + } + + public function forTree(ElementInterface $element, array &$data): void + { + $event = new ElementAdminStyleEvent($element, new AdminStyle($element), ElementAdminStyleEvent::CONTEXT_TREE); + OpenDxp::getEventDispatcher()->dispatch($event, AdminEvents::RESOLVE_ELEMENT_ADMIN_STYLE); + $this->applyStyle($event->getAdminStyle(), $data); + } + + private function applyStyle(AdminStyle $adminStyle, array &$data): void + { + $iconClass = $adminStyle->getElementIconClass(); + $data['iconCls'] = $iconClass !== false ? $iconClass : null; + + if (!$data['iconCls']) { + $icon = $adminStyle->getElementIcon(); + $data['icon'] = $icon !== false ? $icon : null; + } else { + $data['icon'] = null; + } + + $cssClass = $adminStyle->getElementCssClass(); + if ($cssClass !== false) { + $data['cls'] = ($data['cls'] ?? '') . $cssClass . ' '; + } + + $data['qtipCfg'] = $adminStyle->getElementQtipConfig(); + + $text = $adminStyle->getElementText(); + if ($text !== null) { + $data['text'] = $text; + } + } +} diff --git a/src/Enricher/Element/UserNamesEnricher.php b/src/Enricher/Element/UserNamesEnricher.php new file mode 100644 index 00000000..582ec63b --- /dev/null +++ b/src/Enricher/Element/UserNamesEnricher.php @@ -0,0 +1,59 @@ +resolveUserName($element->getUserOwner()); + $modificationName = $element->getUserOwner() === $element->getUserModification() + ? $ownerName + : $this->resolveUserName($element->getUserModification()); + + $data['userOwnerUsername'] = $ownerName['userName']; + $data['userOwnerFullname'] = $ownerName['fullName']; + $data['userModificationUsername'] = $modificationName['userName']; + $data['userModificationFullname'] = $modificationName['fullName']; + } + + private function resolveUserName(?int $userId): array + { + $unknown = ['userName' => '', 'fullName' => $this->translator->trans('user_unknown', [], 'admin')]; + + if ($userId === null) { + return $unknown; + } + + $user = User::getById($userId); + if (empty($user)) { + return $unknown; + } + + return [ + 'userName' => $user->getName(), + 'fullName' => empty($user->getFullName()) ? $user->getName() : $user->getFullName(), + ]; + } +} diff --git a/src/Event/AdminEvents.php b/src/Event/AdminEvents.php index 49f03276..e361343c 100644 --- a/src/Event/AdminEvents.php +++ b/src/Event/AdminEvents.php @@ -338,11 +338,36 @@ class AdminEvents public const string DOCUMENT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA = 'opendxp.admin.document.treeGetChildrenById.preSendData'; /** - * Fired before the edit lock is handled. + * Fired before the edit lock is handled for an asset. + * + * Arguments: + * - data | array | editLock behaviour — set data['task'] = 'overwrite' to force-acquire the lock + * - object | Asset | the current asset + * + * @Event("Symfony\Component\EventDispatcher\GenericEvent") + * + * @var string + */ + public const string ASSET_GET_IS_LOCKED = 'opendxp.admin.asset.get.isLocked'; + + /** + * Fired before the edit lock is handled for a document. + * + * Arguments: + * - data | array | editLock behaviour — set data['task'] = 'overwrite' to force-acquire the lock + * - object | Document | the current document + * + * @Event("Symfony\Component\EventDispatcher\GenericEvent") + * + * @var string + */ + public const string DOCUMENT_GET_IS_LOCKED = 'opendxp.admin.document.get.isLocked'; + + /** + * Fired before the edit lock is handled for a data object. * - * Subject: \OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObjectController * Arguments: - * - data | array | editLock behaviour, this can be modified + * - data | array | editLock behaviour — set data['task'] = 'overwrite' to force-acquire the lock * - object | AbstractObject | the current object * * @Event("Symfony\Component\EventDispatcher\GenericEvent") diff --git a/src/EventListener/Traits/ControllerTypeTrait.php b/src/EventListener/Traits/ControllerTypeTrait.php deleted file mode 100644 index caf1843b..00000000 --- a/src/EventListener/Traits/ControllerTypeTrait.php +++ /dev/null @@ -1,54 +0,0 @@ -getController(); - - if (!is_array($callable) || count($callable) === 0) { - return null; - } - - $controller = $callable[0]; - if ($controller instanceof $type) { - return $controller; - } - - return null; - } - - /** - * Test if event controller is of the given type - */ - protected function isControllerType(ControllerEvent $event, string $type): bool - { - $controller = $this->getControllerType($event, $type); - - return $controller && $controller instanceof $type; - } -} diff --git a/src/Exception/Asset/AssetNotFoundException.php b/src/Exception/Asset/AssetNotFoundException.php new file mode 100644 index 00000000..3ac83203 --- /dev/null +++ b/src/Exception/Asset/AssetNotFoundException.php @@ -0,0 +1,28 @@ +elementId; + } + + public function getElementType(): string + { + return $this->elementType; + } + + public function getEditLock(): Editlock + { + return $this->editLock; + } +} diff --git a/src/Exception/NotFoundException.php b/src/Exception/NotFoundException.php new file mode 100644 index 00000000..513dc8ae --- /dev/null +++ b/src/Exception/NotFoundException.php @@ -0,0 +1,22 @@ +userContext->getAdminUser() ?? throw new \RuntimeException('Admin user not available'); + + return new Dashboard($user); + } +} diff --git a/src/Factory/ElementServiceFactory.php b/src/Factory/ElementServiceFactory.php new file mode 100644 index 00000000..32bf1861 --- /dev/null +++ b/src/Factory/ElementServiceFactory.php @@ -0,0 +1,44 @@ +userContext->getAdminUser()); + } + + public function createDataObjectService(): DataObject\Service + { + return new DataObject\Service($this->userContext->getAdminUser()); + } + + public function createDocumentService(): Document\Service + { + return new Document\Service($this->userContext->getAdminUser()); + } +} diff --git a/src/Factory/LoginPageFactory.php b/src/Factory/LoginPageFactory.php new file mode 100644 index 00000000..49419176 --- /dev/null +++ b/src/Factory/LoginPageFactory.php @@ -0,0 +1,45 @@ +config, + bundleManager: $this->bundleManager, + eventDispatcher: $this->eventDispatcher, + authenticationUtils: $this->authenticationUtils, + request: $request, + ); + } +} diff --git a/src/Handler/Admin/Settings/SettingsHandler.php b/src/Handler/Admin/Settings/SettingsHandler.php new file mode 100644 index 00000000..5ff1ecc3 --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsHandler.php @@ -0,0 +1,138 @@ +userContext->getAdminUser(); + $dto = $this->factory->createSettings($payload, $user); + + $settings = [ + 'instanceId' => $dto->instanceId, + 'version' => $dto->version, + 'build' => $dto->build, + 'debug' => $dto->debug, + 'devmode' => $dto->devMode, + 'disableMinifyJs' => $dto->disableMinifyJs, + 'environment' => $dto->environment, + 'sessionId' => $dto->sessionId, + + 'language' => $dto->language, + 'websiteLanguages' => $dto->websiteLanguages, + 'requiredLanguages' => $dto->requiredLanguages, + + 'showCloseConfirmation' => true, + 'debug_admin_translations' => $dto->debugAdminTranslations, + 'document_generatepreviews' => $dto->generateDocumentPreviews, + 'asset_disable_tree_preview' => $dto->disableAssetTreePreview, + 'asset_hide_edit' => $dto->hideEditImage, + 'asset_tree_paging_limit' => $dto->assetTreePagingLimit, + 'asset_default_upload_path' => $dto->assetDefaultUploadPath, + 'chromium' => $dto->chromiumAvailable, + 'videoconverter' => $dto->videoConverterAvailable, + 'main_domain' => $dto->mainDomain, + 'custom_admin_entrypoint_url' => $dto->customAdminEntrypointUrl, + 'timezone' => $dto->timezone, + 'tile_layer_url_template' => $dto->tileLayerUrlTemplate, + 'geocoding_url_template' => $dto->geocodingUrlTemplate, + 'reverse_geocoding_url_template' => $dto->reverseGeocodingUrlTemplate, + 'document_tree_paging_limit' => $dto->documentTreePagingLimit, + 'object_tree_paging_limit' => $dto->objectTreePagingLimit, + 'hostname' => $dto->hostname, + 'dependency' => $dto->dependencyEnabled, + 'document_auto_save_interval' => $dto->documentAutoSaveInterval, + 'object_auto_save_interval' => $dto->objectAutoSaveInterval, + + 'perspective' => $dto->perspective, + 'availablePerspectives' => $dto->availablePerspectives, + 'disabledPortlets' => $dto->disabledPortlets, + + 'image-thumbnails-writeable' => $dto->imageThumbnailsWriteable, + 'video-thumbnails-writeable' => $dto->videoThumbnailsWriteable, + 'document-types-writeable' => $dto->documentTypesWriteable, + 'predefined-properties-writeable' => $dto->predefinedPropertiesWriteable, + 'predefined-asset-metadata-writeable' => $dto->predefinedAssetMetadataWriteable, + 'perspectives-writeable' => $dto->perspectivesWriteable, + 'custom-views-writeable' => $dto->customViewsWriteable, + 'class-definition-writeable' => $dto->classDefinitionWriteable, + 'object-custom-layout-writeable' => $dto->objectCustomLayoutWriteable, + 'select-options-writeable' => $dto->selectOptionsWriteable, + + 'asset_search_types' => $dto->assetSearchTypes, + 'document_types_configuration' => $dto->documentTypesConfiguration, + 'document_search_types' => $dto->documentSearchTypes, + 'document_valid_types' => $dto->documentValidTypes, + 'document_email_search_types' => $dto->documentEmailSearchTypes, + 'select_options_provider_class' => $dto->selectOptionsProviderClass, + + 'upload_max_filesize' => $dto->uploadMaxFilesize, + 'session_gc_maxlifetime' => $dto->sessionGcMaxlifetime, + + 'maintenance_active' => $dto->maintenanceActive, + 'maintenance_mode' => $dto->maintenanceMode, + + 'mail' => $dto->mailConfigured, + 'mailDefaultAddress' => $dto->mailDefaultAddress, + + 'customviews' => $dto->customViews, + + 'notifications_enabled' => $dto->notificationsEnabled, + 'checknewnotification_enabled' => $dto->checkNewNotificationEnabled, + 'checknewnotification_interval' => $dto->checkNewNotificationInterval, + + 'csrfToken' => $dto->csrfToken, + ]; + + $event = new IndexActionSettingsEvent($settings); + $this->eventDispatcher->dispatch($event, AdminEvents::INDEX_ACTION_SETTINGS); + + return new SettingsResult( + templateParams: [ + 'config' => $this->config, + 'systemSettings' => SystemSettingsConfig::get(), + 'adminSettings' => AdminConfig::get(), + 'perspectiveConfig' => new PerspectiveConfig(), + 'runtimePerspective' => $dto->perspective, + 'pluginJsPaths' => $this->bundleManager->getJsPaths(), + 'pluginCssPaths' => $this->bundleManager->getCssPaths(), + 'settings' => $event->getSettings(), + ], + template: $event->getTemplate(), + ); + } +} diff --git a/src/Handler/Admin/Settings/SettingsPayload.php b/src/Handler/Admin/Settings/SettingsPayload.php new file mode 100644 index 00000000..e1e3b268 --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsPayload.php @@ -0,0 +1,36 @@ +getSession()->getId(), + locale: $request->getLocale(), + ); + } +} diff --git a/src/Handler/Admin/Settings/SettingsResult.php b/src/Handler/Admin/Settings/SettingsResult.php new file mode 100644 index 00000000..231cabbf --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsResult.php @@ -0,0 +1,25 @@ +factory->createStatistics(); + $data = [ + 'instance_id' => $dto->instanceId, + 'revision' => $dto->revision, + 'version' => $dto->version, + 'major_version' => $dto->majorVersion, + 'php_version' => $dto->phpVersion, + 'db_version' => $dto->dbVersion, + 'bundles' => $dto->bundles, + ]; + } catch (\Throwable) { + $data = []; + } + + try { + $this->httpClient->request( + 'POST', + 'https://metrics.opendxp.io/statistics', + ['json' => $data], + ); + } catch (GuzzleException) { + // fail silently + } + } +} diff --git a/src/Handler/Asset/AssetResult.php b/src/Handler/Asset/AssetResult.php new file mode 100644 index 00000000..b8b67949 --- /dev/null +++ b/src/Handler/Asset/AssetResult.php @@ -0,0 +1,27 @@ +assetId); + + if (!$asset) { + throw new NotFoundHttpException('Asset not found: ' . $payload->assetId); + } + + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException('not allowed to publish'); + } + + $asset->clearThumbnails(true); + $asset->save(); + } +} diff --git a/src/Handler/Asset/ClearAssetThumbnail/ClearAssetThumbnailPayload.php b/src/Handler/Asset/ClearAssetThumbnail/ClearAssetThumbnailPayload.php new file mode 100644 index 00000000..30843c7d --- /dev/null +++ b/src/Handler/Asset/ClearAssetThumbnail/ClearAssetThumbnailPayload.php @@ -0,0 +1,35 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/Asset/Copy/CopyAsset/CopyAssetHandler.php b/src/Handler/Asset/Copy/CopyAsset/CopyAssetHandler.php new file mode 100644 index 00000000..6f9d30a5 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetHandler.php @@ -0,0 +1,78 @@ +sourceId; + $targetId = $payload->targetId; + $type = $payload->type; + $sourceParentId = $payload->sourceParentId; + $targetParentId = $payload->targetParentId; + $sessionParentId = $payload->sessionParentId; + $source = Asset::getById($sourceId); + + if ($source === null) { + throw new NotFoundHttpException('Source asset not found'); + } + + if ($sourceParentId !== null && $targetParentId !== null) { + $sourceParent = Asset::getById($sourceParentId) ?? throw new NotFoundHttpException('Source parent not found'); + $resolvedTargetParentId = $sessionParentId ?? $targetParentId; + $targetParent = Asset::getById($resolvedTargetParentId) ?? throw new NotFoundHttpException('Target parent not found'); + $targetPath = preg_replace('@^' . $sourceParent->getRealFullPath() . '@', $targetParent . '/', $source->getRealPath()); + $target = Asset::getByPath($targetPath); + } else { + $target = Asset::getById($targetId); + } + + if ($target === null) { + throw new NotFoundHttpException('Target not found'); + } + + if (!$target->isAllowed('create')) { + Logger::error('could not execute copy/paste because of missing permissions on target [ ' . $targetId . ' ]'); + throw new AccessDeniedHttpException(); + } + + $assetService = $this->serviceFactory->createAssetService(); + + if ($type === 'child') { + $newAsset = $assetService->copyAsChild($target, $source); + + return new CopyAssetResult($newAsset); + } + + if ($type === 'replace') { + $assetService->copyContents($target, $source); + } + + return new CopyAssetResult(); + } +} diff --git a/src/Handler/Asset/Copy/CopyAsset/CopyAssetPayload.php b/src/Handler/Asset/Copy/CopyAsset/CopyAssetPayload.php new file mode 100644 index 00000000..5125a34e --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetPayload.php @@ -0,0 +1,53 @@ +getSession(), 'opendxp_copy'); + $sessionBag = $session->get($request->request->getString('transactionId')); + + return new static( + sourceId: (int) $request->request->getString('sourceId'), + targetId: (int) $request->request->getString('targetId'), + type: $request->request->getString('type'), + sourceParentId: $request->request->has('targetParentId') ? (int) $request->request->getString('sourceParentId') : null, + targetParentId: $request->request->has('targetParentId') ? (int) $request->request->getString('targetParentId') : null, + sessionParentId: isset($sessionBag['parentId']) ? (int) $sessionBag['parentId'] : null, + transactionId: $request->request->getString('transactionId'), + saveParentId: $request->request->getString('saveParentId'), + ); + } +} diff --git a/src/Handler/Asset/Copy/CopyAsset/CopyAssetResult.php b/src/Handler/Asset/Copy/CopyAsset/CopyAssetResult.php new file mode 100644 index 00000000..7aa12a76 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetResult.php @@ -0,0 +1,27 @@ +type === 'recursive') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_asset_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => 'child', + 'transactionId' => $transactionId, + 'saveParentId' => true, + ], + ]]; + + $asset = Asset::getById($payload->sourceId) ?? throw new AssetNotFoundException($payload->sourceId); + + if ($asset->hasChildren()) { + $list = new Asset\Listing(); + $list->setCondition('`path` LIKE ?', [$list->escapeLike($asset->getRealFullPath()) . '/%']); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('ASC'); + + foreach ($list->loadIdList() as $id) { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_asset_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $id, + 'targetParentId' => $payload->targetId, + 'sourceParentId' => $payload->sourceId, + 'type' => 'child', + 'transactionId' => $transactionId, + ], + ]]; + } + } + } elseif ($payload->type === 'child' || $payload->type === 'replace') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_asset_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => $payload->type, + 'transactionId' => $transactionId, + ], + ]]; + } + + return new CopyInfoResult($transactionId, $pasteJobs); + } +} diff --git a/src/Handler/Asset/Copy/CopyInfo/CopyInfoPayload.php b/src/Handler/Asset/Copy/CopyInfo/CopyInfoPayload.php new file mode 100644 index 00000000..42ff0b3f --- /dev/null +++ b/src/Handler/Asset/Copy/CopyInfo/CopyInfoPayload.php @@ -0,0 +1,39 @@ +query->getString('type') ?: null, + sourceId: $request->query->getInt('sourceId'), + targetId: $request->query->getString('targetId') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Copy/CopyInfo/CopyInfoResult.php b/src/Handler/Asset/Copy/CopyInfo/CopyInfoResult.php new file mode 100644 index 00000000..f6a6e2c1 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,26 @@ +parentId; + $name = $payload->name; + $adminUser = $this->userContext->getAdminUser(); + $parentAsset = Asset::getById($parentId); + + if (!$parentAsset->isAllowed('create')) { + throw new AccessDeniedHttpException('prevented creating asset because of missing permissions'); + } + + if (Asset::getByPath($parentAsset->getRealFullPath() . '/' . $name)) { + throw new BadRequestHttpException('Asset with same path+key already exists'); + } + + Asset::create($parentId, [ + 'filename' => $name, + 'type' => 'folder', + 'userOwner' => $adminUser->getId(), + 'userModification' => $adminUser->getId(), + ]); + } +} diff --git a/src/Handler/Asset/CreateAssetFolder/CreateAssetFolderPayload.php b/src/Handler/Asset/CreateAssetFolder/CreateAssetFolderPayload.php new file mode 100644 index 00000000..ad445064 --- /dev/null +++ b/src/Handler/Asset/CreateAssetFolder/CreateAssetFolderPayload.php @@ -0,0 +1,37 @@ +request->getInt('parentId'), + name: $request->request->getString('name'), + ); + } +} diff --git a/src/Handler/Asset/DeleteAsset/DeleteAssetHandler.php b/src/Handler/Asset/DeleteAsset/DeleteAssetHandler.php new file mode 100644 index 00000000..a0902b19 --- /dev/null +++ b/src/Handler/Asset/DeleteAsset/DeleteAssetHandler.php @@ -0,0 +1,72 @@ +type; + $id = $payload->id; + $amount = $payload->amount; + $adminUser = $this->userContext->getAdminUser(); + if ($type === 'children') { + $parentAsset = Asset::getById($id); + + $list = new Asset\Listing(); + $list->setCondition('`path` LIKE ?', [Helper::escapeLike($parentAsset->getRealFullPath()) . '/%']); + $list->setLimit($amount); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('DESC'); + + $deleted = []; + foreach ($list as $asset) { + $deleted[$asset->getId()] = $asset->getRealFullPath(); + if ($asset->isAllowed('delete') && !$asset->isLocked()) { + $asset->delete(); + } + } + + return new DeleteAssetResult($deleted); + } + + $asset = Asset::getById($id); + if ($asset && $asset->isAllowed('delete')) { + if ($asset->isLocked()) { + throw new BadRequestHttpException('prevented deleting asset, because it is locked: ID: ' . $asset->getId()); + } + + $asset->delete(); + + return new DeleteAssetResult(); + } + + throw new AccessDeniedHttpException(); + } +} diff --git a/src/Handler/Asset/DeleteAsset/DeleteAssetPayload.php b/src/Handler/Asset/DeleteAsset/DeleteAssetPayload.php new file mode 100644 index 00000000..c501b408 --- /dev/null +++ b/src/Handler/Asset/DeleteAsset/DeleteAssetPayload.php @@ -0,0 +1,39 @@ +request->getString('type'), + id: $request->request->getInt('id'), + amount: $request->request->getInt('amount'), + ); + } +} diff --git a/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php b/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php new file mode 100644 index 00000000..e5c9bd98 --- /dev/null +++ b/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php @@ -0,0 +1,26 @@ +id; + $selectedIds = $payload->selectedIds; + $offset = $payload->offset; + $limit = $payload->limit; + $jobId = $payload->jobId; + $adminUser = $this->userContext->getAdminUser(); + $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/download-zip-' . $jobId . '.zip'; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + $zip = new ZipArchive(); + $zipState = is_file($zipFile) ? $zip->open($zipFile) : $zip->open($zipFile, ZipArchive::CREATE); + + if ($zipState !== true) { + throw new \RuntimeException('Failed to open zip archive: ' . $zipFile); + } + + $parentPath = $asset->getRealFullPath(); + if ($asset->getId() === 1) { + $parentPath = ''; + } + + $db = \OpenDxp\Db::get(); + $conditionFilters = []; + + if (!empty($selectedIds)) { + $selectedIdList = explode(',', $selectedIds); + $quotedSelectedIds = []; + foreach ($selectedIdList as $selectedId) { + if ($selectedId) { + $quotedSelectedIds[] = $db->quote($selectedId); + } + } + $conditionFilters[] = 'id IN (' . implode(',', $quotedSelectedIds) . ')'; + } + $conditionFilters[] = "`type` != 'folder' AND `path` like " . $db->quote(Helper::escapeLike($parentPath) . '/%'); + if (!$adminUser->isAdmin()) { + $userIds = $adminUser->getRoles(); + $userIds[] = $adminUser->getId(); + $conditionFilters[] = ' ( + (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`, filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + OR + (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`, filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + )'; + } + + $assetList = new Asset\Listing(); + $assetList->setCondition(implode(' AND ', $conditionFilters)); + $assetList->setOrderKey('LENGTH(`path`) ASC, id ASC', false); + $assetList->setOffset($offset); + $assetList->setLimit($limit); + + foreach ($assetList as $a) { + if (!$a->isAllowed('view') || $a instanceof Asset\Folder) { + continue; + } + $zip->addFile($a->getLocalFile(), preg_replace('@^' . preg_quote($asset->getRealPath(), '@') . '@i', '', $a->getRealFullPath())); + } + + $zip->close(); + } +} diff --git a/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipPayload.php b/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipPayload.php new file mode 100644 index 00000000..29e9fc97 --- /dev/null +++ b/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipPayload.php @@ -0,0 +1,43 @@ +query->getInt('id'), + selectedIds: $request->query->getString('selectedIds') ?: null, + offset: $request->query->getInt('offset'), + limit: $request->query->getInt('limit'), + jobId: $request->query->getString('jobId'), + ); + } +} diff --git a/src/Handler/Asset/Download/DownloadAsset/DownloadAssetHandler.php b/src/Handler/Asset/Download/DownloadAsset/DownloadAssetHandler.php new file mode 100644 index 00000000..36b61596 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadAsset/DownloadAssetHandler.php @@ -0,0 +1,39 @@ +id; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('Not allowed to view asset'); + } + + return new AssetResult($asset); + } +} diff --git a/src/Handler/Asset/Download/DownloadAsset/DownloadAssetPayload.php b/src/Handler/Asset/Download/DownloadAsset/DownloadAssetPayload.php new file mode 100644 index 00000000..4f18dcc9 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadAsset/DownloadAssetPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailHandler.php b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailHandler.php new file mode 100644 index 00000000..ba984227 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailHandler.php @@ -0,0 +1,97 @@ +id; + $thumbnailName = $payload->thumbnailName; + $config = $payload->config; + $configData = $payload->configData; + $image = Asset\Image::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$image->isAllowed('view')) { + throw new AccessDeniedHttpException('Not allowed to view thumbnail'); + } + + $thumbnail = null; + $thumbnailFile = null; + $deleteThumbnail = true; + + if ($configData) { + $thumbnailConfig = new Asset\Image\Thumbnail\Config(); + $thumbnailConfig->setName('opendxp-download-' . $image->getId() . '-' . md5($config ?? '')); + + if ($configData['resize_mode'] === 'scaleByWidth') { + $thumbnailConfig->addItem('scaleByWidth', ['width' => $configData['width']]); + } elseif ($configData['resize_mode'] === 'scaleByHeight') { + $thumbnailConfig->addItem('scaleByHeight', ['height' => $configData['height']]); + } else { + $thumbnailConfig->addItem('resize', ['width' => $configData['width'], 'height' => $configData['height']]); + } + + if (!empty($configData['quality']) && $configData['quality'] <= 100 && $configData['quality'] > 0) { + $thumbnailConfig->setQuality($configData['quality']); + } + if (!empty($configData['format'])) { + $thumbnailConfig->setFormat($configData['format']); + } + $thumbnailConfig->setRasterizeSVG(true); + + if ($thumbnailConfig->getFormat() === 'JPEG') { + $thumbnailConfig->setPreserveMetaData(true); + if (empty($configData['quality'])) { + $thumbnailConfig->setPreserveColor(true); + } + } + + $thumbnail = $image->getThumbnail($thumbnailConfig); + $thumbnailFile = $thumbnail->getLocalFile(); + + $exiftool = \OpenDxp\Tool\Console::getExecutable('exiftool'); + if ($thumbnailConfig->getFormat() === 'JPEG' && $exiftool && isset($configData['dpi']) && $configData['dpi']) { + $process = new Process([$exiftool, '-overwrite_original', '-xresolution=' . (int)$configData['dpi'], '-yresolution=' . (int)$configData['dpi'], '-resolutionunit=inches', $thumbnailFile]); + $process->run(); + } + } elseif ($thumbnailName) { + $thumbnail = $image->getThumbnail($thumbnailName); + $deleteThumbnail = false; + } + + if ($thumbnail) { + $thumbnailConfig = $thumbnail->getConfig(); + if ($thumbnailConfig->getFormat() === 'SOURCE' && $autoFormatConfigs = $thumbnailConfig->getAutoFormatThumbnailConfigs()) { + $autoFormatConfig = current($autoFormatConfigs); + $thumbnail = $image->getThumbnail($autoFormatConfig); + } + $thumbnailFile = $thumbnailFile ?: $thumbnail->getLocalFile(); + + return new DownloadImageThumbnailResult($image, $thumbnail, $thumbnailFile, $deleteThumbnail); + } + + throw new AssetNotFoundException($id); + } +} diff --git a/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailPayload.php b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailPayload.php new file mode 100644 index 00000000..372df8dc --- /dev/null +++ b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailPayload.php @@ -0,0 +1,57 @@ +query->getString('thumbnail') ?: null; + $config = $request->query->getString('config') ?: null; + $type = $request->query->getString('type') ?: null; + + $configData = null; + if ($config !== null) { + $configData = json_decode($config, true); + } elseif ($type !== null) { + $predefined = [ + 'web' => ['resize_mode' => 'scaleByWidth', 'width' => 3500, 'dpi' => 72, 'format' => 'JPEG', 'quality' => 85], + 'print' => ['resize_mode' => 'scaleByWidth', 'width' => 6000, 'dpi' => 300, 'format' => 'JPEG', 'quality' => 95], + 'office' => ['resize_mode' => 'scaleByWidth', 'width' => 1190, 'dpi' => 144, 'format' => 'JPEG', 'quality' => 90], + ]; + $configData = $predefined[$type] ?? null; + } + + return new static( + id: $request->query->getInt('id'), + thumbnailName: $thumbnail, + config: $config, + configData: $configData, + ); + } +} diff --git a/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailResult.php b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailResult.php new file mode 100644 index 00000000..5ff6213f --- /dev/null +++ b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailResult.php @@ -0,0 +1,30 @@ +id; + $jobId = $payload->jobId; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/download-zip-' . $jobId . '.zip'; + $suggestedFilename = $asset->getFilename() ?: 'assets'; + + return new DownloadZipResult($zipFile, $suggestedFilename); + } +} diff --git a/src/Handler/Asset/Download/DownloadZip/DownloadZipPayload.php b/src/Handler/Asset/Download/DownloadZip/DownloadZipPayload.php new file mode 100644 index 00000000..7a4fba70 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadZip/DownloadZipPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + jobId: $request->query->getString('jobId'), + ); + } +} diff --git a/src/Handler/Asset/Download/DownloadZip/DownloadZipResult.php b/src/Handler/Asset/Download/DownloadZip/DownloadZipResult.php new file mode 100644 index 00000000..4dc962ec --- /dev/null +++ b/src/Handler/Asset/Download/DownloadZip/DownloadZipResult.php @@ -0,0 +1,26 @@ +id; + $selectedIds = $payload->selectedIds; + $adminUser = $this->userContext->getAdminUser(); + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + return new GetDownloadZipJobsResult(jobId: uniqid('', false), jobs: []); + } + + $parentPath = $asset->getRealFullPath(); + if ($asset->getId() == 1) { + $parentPath = ''; + } + + $db = \OpenDxp\Db::get(); + $conditionFilters = []; + $selectedIdList = explode(',', $selectedIds); + $quotedSelectedIds = []; + foreach ($selectedIdList as $selectedId) { + if ($selectedId) { + $quotedSelectedIds[] = $db->quote($selectedId); + } + } + if ($quotedSelectedIds !== []) { + $conditionFilters[] = 'id IN (' . implode(',', $quotedSelectedIds) . ')'; + } + $conditionFilters[] = '`path` LIKE ' . $db->quote(Helper::escapeLike($parentPath) . '/%') . ' AND `type` != ' . $db->quote('folder'); + if (!$adminUser->isAdmin()) { + $userIds = $adminUser->getRoles(); + $userIds[] = $adminUser->getId(); + $conditionFilters[] = ' ( + (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`, filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + OR + (select list from users_workspaces_asset where userId in (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`, filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + )'; + } + + $assetList = new Asset\Listing(); + $assetList->setCondition(implode(' AND ', $conditionFilters)); + $assetList->setOrderKey('LENGTH(`path`)', false); + $assetList->setOrder('ASC'); + + $totalCount = $assetList->getTotalCount(); + $jobId = uniqid('', false); + $addFilesUrl = $this->router->generate('opendxp_admin_asset_downloadaszipaddfiles'); + $jobAmount = (int) ceil($totalCount / self::FILES_PER_JOB); + $jobs = []; + for ($i = 0; $i < $jobAmount; $i++) { + $jobs[] = [[ + 'url' => $addFilesUrl, + 'method' => 'GET', + 'params' => [ + 'id' => $asset->getId(), + 'selectedIds' => implode(',', $selectedIdList), + 'offset' => $i * self::FILES_PER_JOB, + 'limit' => self::FILES_PER_JOB, + 'jobId' => $jobId, + ], + ]]; + } + + return new GetDownloadZipJobsResult(jobId: $jobId, jobs: $jobs); + } +} diff --git a/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsPayload.php b/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsPayload.php new file mode 100644 index 00000000..00381e06 --- /dev/null +++ b/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + selectedIds: $request->query->getString('selectedIds'), + ); + } +} diff --git a/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsResult.php b/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsResult.php new file mode 100644 index 00000000..3741a6ba --- /dev/null +++ b/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsResult.php @@ -0,0 +1,26 @@ +id; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('Not allowed to preview asset'); + } + + return new AssetResult($asset); + } +} diff --git a/src/Handler/Asset/Editor/LoadAssetForEditor/LoadAssetForEditorPayload.php b/src/Handler/Asset/Editor/LoadAssetForEditor/LoadAssetForEditorPayload.php new file mode 100644 index 00000000..b20a252e --- /dev/null +++ b/src/Handler/Asset/Editor/LoadAssetForEditor/LoadAssetForEditorPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorHandler.php b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorHandler.php new file mode 100644 index 00000000..d02e508d --- /dev/null +++ b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorHandler.php @@ -0,0 +1,52 @@ +id; + $dataUri = $payload->dataUri; + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException('Not allowed to publish asset'); + } + + $data = substr($dataUri, strpos($dataUri, ',')); + $data = base64_decode($data); + $asset->setData($data); + $asset->setUserModification($userId); + $asset->save(); + + return new AssetResult($asset); + } +} diff --git a/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorPayload.php b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorPayload.php new file mode 100644 index 00000000..8cdbf448 --- /dev/null +++ b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + dataUri: $request->request->getString('dataUri'), + ); + } +} diff --git a/src/Handler/Asset/GetAssetChildren/GetAssetChildrenHandler.php b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenHandler.php new file mode 100644 index 00000000..75e0da50 --- /dev/null +++ b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenHandler.php @@ -0,0 +1,115 @@ +nodeId; + $customViewId = $payload->customViewId; + $filter = $payload->filter; + $limit = $payload->limit; + $offset = $payload->offset; + $asset = Asset::getById($nodeId); + if (!$asset instanceof Asset) { + throw new AssetNotFoundException($nodeId); + } + + $adminUser = $this->userContext->getAdminUser(); + $assets = []; + $cv = []; + $filteredTotalCount = 0; + + if ($filter !== null) { + if (!str_ends_with($filter, '*')) { + $filter .= '*'; + } + $filter = str_replace('*', '%', $filter); + $limit = 100; + $offset = 0; + } + + if ($asset->hasChildren()) { + if ($customViewId) { + $cv = $this->elementService->getCustomViewById($customViewId); + } + + $childrenList = new Asset\Listing(); + $childrenList->addConditionParam('parentId = ?', [$asset->getId()]); + $childrenList->filterAccessibleByUser($adminUser, $asset); + + if ($filter !== null) { + $childrenList->addConditionParam('CAST(assets.filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ?', [$filter]); + } + + $childrenList->setLimit($limit); + $childrenList->setOffset($offset); + $childrenList->setOrderKey("FIELD(assets.type, 'folder') DESC, CAST(assets.filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci ASC", false); + + Element\Service::addTreeFilterJoins($cv, $childrenList); + + $beforeListLoadEvent = new GenericEvent(null, [ + 'list' => $childrenList, + 'context' => [], + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); + /** @var Asset\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + $children = $childrenList->load(); + $filteredTotalCount = $childrenList->getTotalCount(); + + foreach ($children as $childAsset) { + $assetTreeNode = $this->elementService->getElementTreeNodeConfig($childAsset); + if ($assetTreeNode['permissions']['list'] == 1) { + $assets[] = $assetTreeNode; + } + } + } + + $event = new GenericEvent(null, ['assets' => $assets]); + $this->eventDispatcher->dispatch($event, AdminEvents::ASSET_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); + $assets = $event->getArgument('assets'); + + return new GetAssetChildrenResult( + $assets, + $filteredTotalCount, + $limit, + $offset, + $asset->getChildAmount($adminUser), + $filter, + ); + } +} diff --git a/src/Handler/Asset/GetAssetChildren/GetAssetChildrenPayload.php b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenPayload.php new file mode 100644 index 00000000..0eedbc4b --- /dev/null +++ b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenPayload.php @@ -0,0 +1,55 @@ +query->getString('filter') ?: null; + $limit = $request->query->getInt('limit', 0); + if ($filter !== null) { + $limit = 100; + } elseif (!$limit) { + $limit = 100000000; + } + + return new static( + nodeId: $request->query->getInt('node'), + customViewId: ($request->query->getString('view') ?: null), + filter: $filter, + limit: $limit, + offset: $request->query->getInt('start'), + hasLimit: $request->query->has('limit'), + inSearch: $request->query->getInt('inSearch'), + ); + } +} diff --git a/src/Handler/Asset/GetAssetChildren/GetAssetChildrenResult.php b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenResult.php new file mode 100644 index 00000000..11fce3e9 --- /dev/null +++ b/src/Handler/Asset/GetAssetChildren/GetAssetChildrenResult.php @@ -0,0 +1,30 @@ +id; + $requestSchemeAndHost = $payload->requestSchemeAndHost; + $asset = Asset::getById($id); + if (!$asset instanceof Asset) { + throw new AssetNotFoundException($id); + } + + $adminUser = $this->userContext->getAdminUser(); + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + if (!$asset instanceof Asset\Folder && ($asset->isAllowed('publish') || $asset->isAllowed('delete'))) { + $this->editLockService->checkAndAcquire($asset->getId(), 'asset', AdminEvents::ASSET_GET_IS_LOCKED, $asset); + } + + $asset = clone $asset; + $asset->setParent(null); + $asset->setStream(null); + + $data = $asset->getObjectVars(); + $data['locked'] = $asset->isLocked(); + + if ($asset instanceof Asset\Text) { + if ($asset->getFileSize() < 2000000) { + $data['data'] = \ForceUTF8\Encoding::toUTF8($asset->getData()); + } else { + $data['data'] = false; + } + } elseif ($asset instanceof Asset\Document) { + $data['pdfPreviewAvailable'] = (bool) $this->getDocumentPreviewPdf($asset); + } elseif ($asset instanceof Asset\Video) { + $videoInfo = []; + + if (\OpenDxp\Video::isAvailable()) { + $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); + $thumbnail = $asset->getThumbnail($config, ['mp4']); + if ($thumbnail && $thumbnail['status'] === 'finished') { + $videoInfo['previewUrl'] = $thumbnail['formats']['mp4']; + $videoInfo['width'] = $asset->getWidth(); + $videoInfo['height'] = $asset->getHeight(); + $metaData = $asset->getSphericalMetaData(); + if (isset($metaData['ProjectionType']) && strtolower($metaData['ProjectionType']) === 'equirectangular') { + $videoInfo['isVrVideo'] = true; + } + } + } + + $data['videoInfo'] = $videoInfo; + } elseif ($asset instanceof Asset\Image) { + $imageInfo = []; + + $previewUrl = $this->urlGenerator->generate('opendxp_admin_asset_getimagethumbnail', [ + 'id' => $asset->getId(), + 'treepreview' => true, + '_dc' => time(), + ]); + + if ($asset->isAnimated()) { + $previewUrl = $this->urlGenerator->generate('opendxp_admin_asset_getasset', [ + 'id' => $asset->getId(), + '_dc' => time(), + ]); + } + + $imageInfo['previewUrl'] = $previewUrl; + + if ($asset->getWidth() && $asset->getHeight()) { + $imageInfo['dimensions'] = [ + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + ]; + } + + $imageInfo['exiftoolAvailable'] = (bool) \OpenDxp\Tool\Console::getExecutable('exiftool'); + + if (!$asset->getEmbeddedMetaData(false)) { + $asset->getEmbeddedMetaData(true, false); + } + + $data['imageInfo'] = $imageInfo; + } + + $predefinedMetaData = Metadata\Predefined\Listing::getByTargetType('asset', [$asset->getType()]); + $predefinedMetaDataGroups = []; + foreach ($predefinedMetaData as $item) { + if ($item->getGroup()) { + $predefinedMetaDataGroups[$item->getGroup()] = true; + } + } + $data['predefinedMetaDataGroups'] = array_keys($predefinedMetaDataGroups); + $data['properties'] = Element\Service::minimizePropertiesForEditmode($asset->getProperties()); + $data['metadata'] = Asset\Service::expandMetadataForEditmode($asset->getMetadata()); + $data['versionDate'] = $asset->getModificationDate(); + $data['filesizeFormatted'] = $asset->getFileSize(true); + $data['filesize'] = $asset->getFileSize(); + $data['fileExtension'] = pathinfo($asset->getFilename(), PATHINFO_EXTENSION); + $data['idPath'] = Element\Service::getIdPath($asset); + $data['userPermissions'] = $asset->getUserPermissions($adminUser); + + $frontendPath = $asset->getFrontendFullPath(); + $data['url'] = preg_match('/^http(s)?:\\/\\/.+/', $frontendPath) + ? $frontendPath + : $requestSchemeAndHost . $frontendPath; + + $data['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $asset->getScheduledTasks() + ); + + $this->userNamesEnricher->enrich($asset, $data); + $this->adminStyleEnricher->forEditor($asset, $data); + + $data['php'] = [ + 'classes' => [$asset::class, ...array_values(class_parents($asset))], + 'interfaces' => array_values(class_implements($asset)), + ]; + + $event = new GenericEvent(null, [ + 'data' => $data, + 'asset' => $asset, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::ASSET_GET_PRE_SEND_DATA); + $data = $event->getArgument('data'); + + return new GetAssetDataResult($data); + } + + private function getDocumentPreviewPdf(Asset\Document $asset): mixed + { + $stream = null; + + if ($asset->getMimeType() == self::PDF_MIMETYPE) { + $stream = $asset->getStream(); + } + + if ( + !$stream && + $asset->getPageCount() && + \OpenDxp\Document::isAvailable() && + \OpenDxp\Document::isFileTypeSupported($asset->getFilename()) + ) { + try { + $stream = \OpenDxp\Document::getInstance()->getPdf($asset); + } catch (Exception) { + // nothing to do + } + } + + return $stream; + } + +} + diff --git a/src/Handler/Asset/GetAssetData/GetAssetDataPayload.php b/src/Handler/Asset/GetAssetData/GetAssetDataPayload.php new file mode 100644 index 00000000..79494937 --- /dev/null +++ b/src/Handler/Asset/GetAssetData/GetAssetDataPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + requestSchemeAndHost: $request->getSchemeAndHttpHost(), + ); + } +} diff --git a/src/Handler/Asset/GetAssetData/GetAssetDataResult.php b/src/Handler/Asset/GetAssetData/GetAssetDataResult.php new file mode 100644 index 00000000..7b44ea6d --- /dev/null +++ b/src/Handler/Asset/GetAssetData/GetAssetDataResult.php @@ -0,0 +1,25 @@ +params; + + $filterPrepareEvent = new GenericEvent(null, ['requestParams' => $params]); + $this->eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::ASSET_LIST_BEFORE_FILTER_PREPARE); + $params = $filterPrepareEvent->getArgument('requestParams'); + + return new GridProxyResult( + $this->assetGridService->gridProxy($params, $payload->language) + ); + } +} diff --git a/src/Handler/Asset/GridProxy/GridProxyPayload.php b/src/Handler/Asset/GridProxy/GridProxyPayload.php new file mode 100644 index 00000000..f1569dc4 --- /dev/null +++ b/src/Handler/Asset/GridProxy/GridProxyPayload.php @@ -0,0 +1,39 @@ +query->has('language') ? $request->query->getString('language') : null; + + return new static( + params: [...$request->request->all(), ...$request->query->all()], + language: $rawLanguage !== 'default' ? $rawLanguage : null, + ); + } +} diff --git a/src/Handler/Asset/GridProxy/GridProxyResult.php b/src/Handler/Asset/GridProxy/GridProxyResult.php new file mode 100644 index 00000000..b691b67b --- /dev/null +++ b/src/Handler/Asset/GridProxy/GridProxyResult.php @@ -0,0 +1,25 @@ +gridConfigId; + $adminUser = $this->userContext->getAdminUser(); + $gridConfig = GridConfig::getById($gridConfigId); + if (!$gridConfig) { + throw new NotFoundHttpException('Grid config not found: ' . $gridConfigId); + } + + if ($gridConfig->getOwnerId() !== $adminUser->getId()) { + throw new BadRequestHttpException("don't mess with someone elses grid config"); + } + + $gridConfig->delete(); + } +} diff --git a/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php new file mode 100644 index 00000000..6c388b6d --- /dev/null +++ b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php @@ -0,0 +1,37 @@ +request->getInt('gridConfigId'), + noSystemColumns: (bool) $request->query->get('no_system_columns'), + ); + } +} diff --git a/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportHandler.php b/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportHandler.php new file mode 100644 index 00000000..5ed95cd9 --- /dev/null +++ b/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportHandler.php @@ -0,0 +1,139 @@ +fileHandle; + $ids = $payload->ids; + $delimiter = $payload->delimiter; + $language = $payload->language; + $header = $payload->header; + $fields = $payload->fields; + $addTitles = $payload->addTitles; + $list = new Asset\Listing(); + + $quotedIds = []; + foreach ($ids as $id) { + $quotedIds[] = $list->quote($id); + } + + $list->setCondition('id IN (' . implode(',', $quotedIds) . ')'); + $list->setOrderKey(' FIELD(id, ' . implode(',', $quotedIds) . ')', false); + + $csv = $this->buildCsvData($language, $list, $fields, $header, $addTitles); + + $temp = tmpfile(); + + try { + $storage = Storage::get('temp'); + $csvFile = $this->gridExportService->getCsvFile($fileHandle); + + $fileStream = $storage->readStream($csvFile); + stream_copy_to_stream($fileStream, $temp, null, 0); + + $firstLine = !($addTitles && $header === 'no_header'); + + foreach ($csv as $line) { + if ($addTitles && $firstLine) { + $firstLine = false; + fwrite($temp, implode($delimiter, $line) . "\r\n"); + } else { + fwrite($temp, implode($delimiter, array_map($this->gridColumnConfigService->encode(...), $line)) . "\r\n"); + } + } + + $storage->writeStream($csvFile, $temp); + } catch (UnableToReadFile $exception) { + Logger::err($exception->getMessage()); + throw new BadRequestHttpException(sprintf('export file not found: %s', $fileHandle)); + } finally { + if (is_resource($temp)) { + fclose($temp); + } + } + } + + private function buildCsvData( + string $language, + Asset\Listing $list, + array $fields, + string $header, + bool $addTitles, + ): array { + $csv = []; + $unsupportedFields = ['preview~system', 'size~system']; + $fields = array_filter($fields, fn ($field) => !in_array($field['key'], $unsupportedFields)); + + if ($addTitles && $header !== 'no_header') { + $columns = $fields; + $titleIdx = $header === 'name' ? 'key' : 'label'; + foreach ($columns as $columnIdx => $columnKeys) { + $columns[$columnIdx] = '"' . $columnKeys[$titleIdx] . '"'; + } + $csv[] = $columns; + } + + foreach ($list->load() as $asset) { + if ($fields) { + $dataRows = []; + foreach ($fields as $field) { + $fieldDef = explode('~', $field['key']); + $getter = 'get' . ucfirst($fieldDef[0]); + + if (isset($fieldDef[1])) { + if ($fieldDef[1] === 'system' && method_exists($asset, $getter)) { + $data = $asset->$getter($language); + } else { + $fieldDef[1] = str_replace('none', '', $fieldDef[1]); + $data = $asset->getMetadata($fieldDef[0], $fieldDef[1], true); + } + } else { + $data = $asset->getMetadata($field['key'], $language, true); + } + + if ($data instanceof Element\ElementInterface) { + $data = $data->getRealFullPath(); + } + $dataRows[] = $data; + } + $dataRows = Element\Service::escapeCsvRecord($dataRows); + $csv[] = $dataRows; + } + } + + return $csv; + } +} diff --git a/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportPayload.php b/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportPayload.php new file mode 100644 index 00000000..d35a4b1c --- /dev/null +++ b/src/Handler/Asset/Helper/DoAssetExport/DoAssetExportPayload.php @@ -0,0 +1,51 @@ +request->getString('settings'), true) ?? []; + $fields = json_decode($request->request->all('fields')[0] ?? '[]', true) ?? []; + + return new static( + fileHandle: File::getValidFilename($request->request->getString('fileHandle')), + ids: $request->request->all('ids'), + delimiter: $settings['delimiter'] ?? ';', + language: str_replace('default', '', $request->request->getString('language')), + header: $settings['header'] ?? 'title', + fields: $fields, + addTitles: (bool) $request->request->get('initial'), + ); + } +} diff --git a/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchHandler.php b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchHandler.php new file mode 100644 index 00000000..bfc01e6d --- /dev/null +++ b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchHandler.php @@ -0,0 +1,40 @@ +data ?? []; + $adminUser = $this->userContext->getAdminUser(); + // Returns false when there is no asset to update (job already completed) — not an error. + // Throws on permission denied or save failure. + $this->gridBatchService->executeAssetBatch($data, $adminUser); + } +} diff --git a/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php new file mode 100644 index 00000000..e1313561 --- /dev/null +++ b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php @@ -0,0 +1,37 @@ +request->getString('data'); + + return new static( + data: $raw !== '' ? json_decode($raw, true) : null, + ); + } +} diff --git a/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsHandler.php b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsHandler.php new file mode 100644 index 00000000..ea9a8d4d --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsHandler.php @@ -0,0 +1,40 @@ +allParams; + $adminUser = $this->userContext->getAdminUser(); + return new GetAssetBatchJobsResult( + $this->gridBatchService->getAssetBatchJobIds($allParams, $adminUser), + ); + } +} diff --git a/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsPayload.php b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsPayload.php new file mode 100644 index 00000000..c0b908cd --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsPayload.php @@ -0,0 +1,39 @@ +request->getString('language') ?: null; + + return new static( + allParams: [...$request->request->all(), ...$request->query->all()], + language: $language, + ); + } +} diff --git a/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsResult.php b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsResult.php new file mode 100644 index 00000000..8cfa7594 --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsResult.php @@ -0,0 +1,25 @@ + $defaultMetadata, 'name' => $defaultMetadata, 'datatype' => 'data', 'fieldtype' => 'input']; + } + $result['defaultColumns']['nodeLabel'] = 'default_metadata'; + $result['defaultColumns']['nodeType'] = 'image'; + $result['defaultColumns']['children'] = $defaultColumns; + + //predefined metadata + $list = Metadata\Predefined\Listing::getByTargetType('asset'); + $metadataItems = []; + $tmp = []; + foreach ($list as $item) { + //only allow unique metadata columns with subtypes + $uniqueKey = $item->getName() . '_' . $item->getTargetSubtype(); + if (!in_array($uniqueKey, $tmp) && !in_array($item->getName(), $defaultMetadataNames)) { + $tmp[] = $uniqueKey; + $item->expand(); + $name = SecurityHelper::convertHtmlSpecialChars($item->getName()); + $metadataItems[] = [ + 'title' => $name, + 'name' => $name, + 'subtype' => $item->getTargetSubtype(), + 'datatype' => 'data', + 'fieldtype' => $item->getType(), + 'config' => $item->getConfig(), + ]; + } + } + + $result['metadataColumns']['children'] = $metadataItems; + $result['metadataColumns']['nodeLabel'] = 'predefined_metadata'; + $result['metadataColumns']['nodeType'] = 'metadata'; + + //system columns + $systemColumnNames = Asset\Service::GRID_SYSTEM_COLUMNS; + $systemColumns = []; + foreach ($systemColumnNames as $systemColumn) { + $systemColumns[] = ['title' => $systemColumn, 'name' => $systemColumn, 'datatype' => 'data', 'fieldtype' => 'system']; + } + $result['systemColumns']['nodeLabel'] = 'system_columns'; + $result['systemColumns']['nodeType'] = 'system'; + $result['systemColumns']['children'] = $systemColumns; + + return new GetAssetMetadataForColumnConfigResult($result); + } +} diff --git a/src/Handler/Asset/Helper/GetAssetMetadataForColumnConfig/GetAssetMetadataForColumnConfigResult.php b/src/Handler/Asset/Helper/GetAssetMetadataForColumnConfig/GetAssetMetadataForColumnConfigResult.php new file mode 100644 index 00000000..f842c3ff --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetMetadataForColumnConfig/GetAssetMetadataForColumnConfigResult.php @@ -0,0 +1,25 @@ +allParams; + $adminUser = $this->userContext->getAdminUser(); + $list = $this->gridHelperService->prepareAssetListingForGrid($allParams, $adminUser); + + if (empty($ids = $allParams['ids'] ?? '')) { + $ids = $list->loadIdList(); + } + + $jobs = array_chunk($ids, 20); + + $fileHandle = uniqid('asset-export-', false); + $storage = Storage::get('temp'); + $storage->write($this->gridExportService->getCsvFile($fileHandle), ''); + + return new GetExportJobsResult($jobs, $fileHandle); + } +} diff --git a/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsPayload.php b/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsPayload.php new file mode 100644 index 00000000..f7ad1c47 --- /dev/null +++ b/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsPayload.php @@ -0,0 +1,35 @@ +request->all(), ...$request->query->all()], + ); + } +} diff --git a/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsResult.php b/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsResult.php new file mode 100644 index 00000000..be241529 --- /dev/null +++ b/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsResult.php @@ -0,0 +1,26 @@ +classId; + $gridConfigId = $payload->gridConfigId; + $searchType = $payload->searchType; + $type = $payload->type; + $adminUser = $this->userContext->getAdminUser(); + $asset = Asset::getById(is_numeric($classId) ? (int) $classId : 0); + + if (!$asset || !$asset->isAllowed('list')) { + throw new AccessDeniedHttpException(); + } + + $favourite = new GridConfigFavourite(); + $favourite->setOwnerId($adminUser->getId()); + $favourite->setClassId($classId); + $favourite->setSearchType($searchType); + $favourite->setType($type); + + try { + if ($gridConfigId !== 0) { + $gridConfig = GridConfig::getById($gridConfigId); + $favourite->setGridConfigId($gridConfig->getId()); + } + + $favourite->setObjectId(0); + $favourite->save(); + } catch (Exception) { + $favourite->delete(); + } + + return new MarkGridConfigFavouriteResult(false); + } +} diff --git a/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouritePayload.php b/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouritePayload.php new file mode 100644 index 00000000..6f79f285 --- /dev/null +++ b/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouritePayload.php @@ -0,0 +1,41 @@ +request->getString('classId') ?: null, + gridConfigId: $request->request->getInt('gridConfigId'), + searchType: $request->request->getString('searchType') ?: null, + type: $request->request->getString('type') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouriteResult.php b/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouriteResult.php new file mode 100644 index 00000000..10a0b9ee --- /dev/null +++ b/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouriteResult.php @@ -0,0 +1,25 @@ +columns as $item) { + if (!empty($item->isOperator)) { + $itemKey = '#' . uniqid('', false); + + $item->key = $itemKey; + $newData[] = $item; + $helperColumns[$itemKey] = $item; + } else { + $newData[] = $item; + } + } + + return new PrepareHelperColumnConfigsResult( + newData: $newData, + helperColumns: $helperColumns, + ); + } +} diff --git a/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php new file mode 100644 index 00000000..4da54de6 --- /dev/null +++ b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php @@ -0,0 +1,34 @@ +request->getString('columns')) ?? [], + ); + } +} diff --git a/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php new file mode 100644 index 00000000..ed8cdcee --- /dev/null +++ b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php @@ -0,0 +1,25 @@ +assetId; + $classId = $payload->classId; + $context = $payload->context; + $searchType = $payload->searchType; + $type = $payload->type; + $gridConfigData = $payload->gridConfigData; + $metadata = $payload->metadata; + $adminUser = $this->userContext->getAdminUser(); + $asset = Asset::getById($assetId); + if (!$asset) { + throw new NotFoundHttpException(); + } + + if (!$asset->isAllowed('list')) { + throw new AccessDeniedHttpException(); + } + + $gridConfigData['opendxp_version'] = Version::getVersion(); + $gridConfigData['opendxp_revision'] = Version::getRevision(); + $gridConfigData['context'] = $context; + unset($gridConfigData['settings']['isShared']); + + $gridConfigId = $metadata['gridConfigId'] ?? null; + $gridConfig = null; + if ($gridConfigId) { + $gridConfig = GridConfig::getById($gridConfigId); + } + + if ($gridConfig && $gridConfig->getOwnerId() !== $adminUser->getId()) { + throw new BadRequestHttpException("don't mess around with somebody else's configuration"); + } + + $this->gridColumnConfigService->updateGridConfigShares($gridConfig, $metadata ?? [], $adminUser); + + if (!$gridConfig) { + $gridConfig = new GridConfig(); + $gridConfig->setName(date('c')); + $gridConfig->setClassId($classId); + $gridConfig->setSearchType($searchType); + $gridConfig->setType($type); + $gridConfig->setOwnerId($adminUser->getId()); + } + + if ($metadata) { + $gridConfig->setName($metadata['gridConfigName']); + $gridConfig->setDescription($metadata['gridConfigDescription']); + $gridConfig->setShareGlobally($metadata['shareGlobally'] && $adminUser->isAdmin()); + $gridConfig->setSetAsFavourite($metadata['setAsFavourite'] && $adminUser->isAdmin()); + } + + $gridConfig->setConfig(json_encode($gridConfigData)); + $gridConfig->save(); + + if (!empty($metadata['setAsFavourite']) && $adminUser->isAdmin()) { + $this->gridColumnConfigService->updateGridConfigFavourites($gridConfig, $metadata, $adminUser); + } + + $availableConfigs = $this->gridColumnConfigService->getMyOwnColumnConfigs($adminUser->getId(), $classId ?? '', $searchType); + $sharedConfigs = $this->gridColumnConfigService->getSharedColumnConfigs($adminUser, $classId ?? '', $searchType); + + $settings = $this->gridColumnConfigService->getShareSettings($gridConfig->getId()); + $settings['gridConfigId'] = (int) $gridConfig->getId(); + $settings['gridConfigName'] = $gridConfig->getName(); + $settings['gridConfigDescription'] = $gridConfig->getDescription(); + $settings['shareGlobally'] = $gridConfig->isShareGlobally(); + $settings['setAsFavourite'] = $gridConfig->isSetAsFavourite(); + $settings['isShared'] = $gridConfig->getOwnerId() !== $adminUser->getId(); + + return new SaveGridColumnConfigResult($settings, $availableConfigs, $sharedConfigs); + } +} diff --git a/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigPayload.php b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigPayload.php new file mode 100644 index 00000000..8687ad63 --- /dev/null +++ b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigPayload.php @@ -0,0 +1,50 @@ +request->getString('gridconfig'); + $settings = $request->request->getString('settings'); + + return new static( + assetId: $request->request->getInt('id'), + classId: $request->request->getString('class_id') ?: null, + context: $request->request->getString('context') ?: null, + searchType: $request->request->getString('searchType') ?: null, + type: $request->request->getString('type') ?: null, + gridConfigData: $gridconfig ? (json_decode($gridconfig, true) ?? []) : [], + metadata: $settings ? json_decode($settings, true) : null, + ); + } +} diff --git a/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigResult.php b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigResult.php new file mode 100644 index 00000000..f9cea76b --- /dev/null +++ b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigResult.php @@ -0,0 +1,27 @@ +id; + $page = $payload->page; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to view'); + } + + $text = null; + if ($asset instanceof Asset\Document) { + $text = $asset->getText($page); + } + + return new AssetTextResult($text); + } +} diff --git a/src/Handler/Asset/Media/GetAssetText/GetAssetTextPayload.php b/src/Handler/Asset/Media/GetAssetText/GetAssetTextPayload.php new file mode 100644 index 00000000..34c7adba --- /dev/null +++ b/src/Handler/Asset/Media/GetAssetText/GetAssetTextPayload.php @@ -0,0 +1,39 @@ +query->has('page') ? $request->query->getInt('page') : null; + + return new static( + id: $request->query->getInt('id'), + page: $page, + ); + } +} diff --git a/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewHandler.php b/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewHandler.php new file mode 100644 index 00000000..f2c9b6fc --- /dev/null +++ b/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewHandler.php @@ -0,0 +1,105 @@ +id; + $asset = Asset\Document::getById($id); + if (!$asset instanceof Asset\Document) { + throw new AssetNotFoundException($id); + } + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('Access to asset ' . $asset->getId() . ' denied'); + } + + $scanStatus = null; + $thumbnailPath = null; + $stream = null; + + if ($asset->getMimeType() === self::PDF_MIMETYPE) { + $scanStatus = $this->getScanStatus($asset); + $openPdfConfig = Config::getSystemConfiguration('assets')['document']['open_pdf_in_new_tab']; + + $openInNewTab = $openPdfConfig === 'all-pdfs' + || ($openPdfConfig === 'only-unsafe' && $scanStatus === PdfScanStatus::UNSAFE); + + if ($openInNewTab) { + $thumbnail = $asset->getImageThumbnail(Asset\Image\Thumbnail\Config::getPreviewConfig()); + $thumbnailPath = $thumbnail->getPath(); + + return new PreviewDocumentResult($asset, $scanStatus, $thumbnailPath, $asset->getRealFullPath(), null); + } + } + + if ($scanStatus === null || ($scanStatus !== PdfScanStatus::IN_PROGRESS && $scanStatus !== PdfScanStatus::UNSAFE)) { + $stream = $this->getPreviewPdf($asset); + } + + return new PreviewDocumentResult($asset, $scanStatus, null, $asset->getRealFullPath(), $stream); + } + + private function getScanStatus(Asset\Document $asset): ?PdfScanStatus + { + if (!Config::getSystemConfiguration('assets')['document']['scan_pdf']) { + return null; + } + + $scanStatus = $asset->getScanStatus(); + if (!$scanStatus instanceof PdfScanStatus) { + $asset->addToUpdateTaskQueue(); + + return PdfScanStatus::IN_PROGRESS; + } + + return $scanStatus; + } + + private function getPreviewPdf(Asset\Document $asset): mixed + { + $stream = null; + + if ($asset->getMimeType() === self::PDF_MIMETYPE) { + $stream = $asset->getStream(); + } + + if (!$stream && $asset->getPageCount() && \OpenDxp\Document::isAvailable() && \OpenDxp\Document::isFileTypeSupported($asset->getFilename())) { + try { + $document = \OpenDxp\Document::getInstance(); + $stream = $document->getPdf($asset); + } catch (Exception) { + // nothing to do + } + } + + return $stream; + } +} diff --git a/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewPayload.php b/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewPayload.php new file mode 100644 index 00000000..d8a04f75 --- /dev/null +++ b/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Asset/Media/GetDocumentPreview/PreviewDocumentResult.php b/src/Handler/Asset/Media/GetDocumentPreview/PreviewDocumentResult.php new file mode 100644 index 00000000..bc9e8235 --- /dev/null +++ b/src/Handler/Asset/Media/GetDocumentPreview/PreviewDocumentResult.php @@ -0,0 +1,34 @@ +id; + $configName = $payload->configName; + $asset = Asset\Video::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to preview'); + } + + $config = Asset\Video\Thumbnail\Config::getByName($configName); + if (!$config instanceof Asset\Video\Thumbnail\Config) { + $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); + } + + $thumbnail = $asset->getThumbnail($config, ['mp4']); + $isFinished = $thumbnail && $thumbnail['status'] === 'finished'; + + return new PreviewVideoResult($asset, $thumbnail, $config->getName(), $isFinished); + } +} diff --git a/src/Handler/Asset/Media/GetVideoPreview/GetVideoPreviewPayload.php b/src/Handler/Asset/Media/GetVideoPreview/GetVideoPreviewPayload.php new file mode 100644 index 00000000..f475b493 --- /dev/null +++ b/src/Handler/Asset/Media/GetVideoPreview/GetVideoPreviewPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + configName: $request->query->getString('config') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Media/GetVideoPreview/PreviewVideoResult.php b/src/Handler/Asset/Media/GetVideoPreview/PreviewVideoResult.php new file mode 100644 index 00000000..aef59b2c --- /dev/null +++ b/src/Handler/Asset/Media/GetVideoPreview/PreviewVideoResult.php @@ -0,0 +1,30 @@ +id; + $configName = $payload->configName; + $asset = Asset\Video::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to preview'); + } + + $config = Asset\Video\Thumbnail\Config::getByName($configName); + if (!$config instanceof Asset\Video\Thumbnail\Config) { + $config = Asset\Video\Thumbnail\Config::getPreviewConfig(); + } + + $thumbnail = $asset->getThumbnail($config, ['mp4']); + $storagePath = $asset->getRealPath() . '/' . preg_replace('@^' . preg_quote($asset->getPath(), '@') . '@', '', urldecode($thumbnail['formats']['mp4'])); + + $storage = Tool\Storage::get('thumbnail'); + if (!$storage->fileExists($storagePath)) { + throw new NotFoundHttpException('Video thumbnail not found'); + } + + return new ServeVideoPreviewResult( + $storage->readStream($storagePath), + $storage->fileSize($storagePath), + ); + } +} diff --git a/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewPayload.php b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewPayload.php new file mode 100644 index 00000000..cf3facbf --- /dev/null +++ b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + configName: $request->query->getString('config') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewResult.php b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewResult.php new file mode 100644 index 00000000..d5dcd349 --- /dev/null +++ b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewResult.php @@ -0,0 +1,26 @@ +id) ?? throw new NotFoundHttpException('Asset not found'); + + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException(); + } + + $this->payloadMapper->applyPayload($payload, $asset); + + return $this->coordinator->save($asset, $payload->task); + } +} diff --git a/src/Handler/Asset/SaveAsset/SaveAssetPayload.php b/src/Handler/Asset/SaveAsset/SaveAssetPayload.php new file mode 100644 index 00000000..ec607b25 --- /dev/null +++ b/src/Handler/Asset/SaveAsset/SaveAssetPayload.php @@ -0,0 +1,58 @@ +request->has('image'); + + return new static( + id: $request->request->getInt('id'), + task: $request->request->getString('task'), + metadata: $request->request->has('metadata') + ? (json_decode($request->request->getString('metadata'), true) ?? null) + : null, + propertiesData: $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? null) + : null, + schedulerData: $request->request->has('scheduler') + ? (json_decode($request->request->getString('scheduler'), true) ?? null) + : null, + rawData: $request->request->get('data'), + hasImage: $hasImage, + imageData: $hasImage + ? (json_decode($request->request->getString('image'), true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Asset/SaveAsset/SaveAssetResult.php b/src/Handler/Asset/SaveAsset/SaveAssetResult.php new file mode 100644 index 00000000..d0b69404 --- /dev/null +++ b/src/Handler/Asset/SaveAsset/SaveAssetResult.php @@ -0,0 +1,27 @@ +id; + $hasThumbnailPreview = $payload->hasThumbnailPreview; + $page = $payload->page; + $origin = $payload->origin; + $queryAll = $payload->queryAll; + $document = Asset\Document::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$document->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to view thumbnail'); + } + + $thumbnail = Asset\Image\Thumbnail\Config::getByAutoDetect($queryAll); + + $format = strtolower($thumbnail->getFormat()); + if ($format === 'source') { + $thumbnail->setFormat('jpeg'); + } + + if ($hasThumbnailPreview) { + $thumbnail = Asset\Image\Thumbnail\Config::getPreviewConfig(); + } + + $thumb = $document->getImageThumbnail($thumbnail, $page ?? 1); + + if ($origin === 'treeNode' && !$thumb->exists()) { + $this->messageBus->dispatch(new AssetPreviewImageMessage($document->getId())); + throw new NotFoundHttpException(sprintf('Tree preview thumbnail not available for asset %s', $document->getId())); + } + + return new GetDocumentThumbnailResult($thumb->getStream(), $thumb->getFileExtension()); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailPayload.php b/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailPayload.php new file mode 100644 index 00000000..5f07ba20 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailPayload.php @@ -0,0 +1,43 @@ +query->getInt('id'), + hasThumbnailPreview: $request->query->has('treepreview'), + page: $request->query->has('page') ? $request->query->getInt('page') : null, + origin: $request->query->getString('origin') ?: null, + queryAll: $request->query->all(), + ); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailResult.php b/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailResult.php new file mode 100644 index 00000000..7d0cdd96 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailResult.php @@ -0,0 +1,26 @@ +requestParams; + $adminUser = $this->userContext->getAdminUser(); + $filterPrepareEvent = new GenericEvent(null, ['requestParams' => $requestParams]); + $this->eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::ASSET_LIST_BEFORE_FILTER_PREPARE); + $requestParams = $filterPrepareEvent->getArgument('requestParams'); + + $folder = Asset::getById((int) $requestParams['id']); + + $start = (int) ($requestParams['start'] ?? 0); + $limit = (int) ($requestParams['limit'] ?? 10); + + $conditionFilters = []; + $list = new Asset\Listing(); + $conditionFilters[] = '`path` LIKE ' . ($folder->getRealFullPath() === '/' ? "'/%'" : $list->quote(Helper::escapeLike($folder->getRealFullPath()) . '/%')) . " AND `type` != 'folder'"; + + if (!$adminUser->isAdmin()) { + $conditionFilters[] = $this->gridHelperService->getPermittedPathsByUser('asset', $adminUser); + } + + $list->setCondition(implode(' AND ', $conditionFilters)); + $list->setLimit($limit); + $list->setOffset($start); + $list->setOrderKey('CAST(filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci ASC', false); + + $beforeListLoadEvent = new GenericEvent(null, ['list' => $list, 'context' => $requestParams]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); + /** @var Asset\Listing $list */ + $list = $beforeListLoadEvent->getArgument('list'); + + $list->load(); + + $assets = []; + foreach ($list as $asset) { + if (!$asset->isAllowed('list')) { + continue; + } + + $filenameDisplay = $asset->getFilename(); + if (strlen($filenameDisplay) > 32) { + $filenameDisplay = substr($filenameDisplay, 0, 25) . '...' . pathinfo($filenameDisplay, PATHINFO_EXTENSION); + } + + $assets[] = [ + 'id' => $asset->getId(), + 'type' => $asset->getType(), + 'filename' => $asset->getFilename(), + 'filenameDisplay' => htmlspecialchars($filenameDisplay ?? ''), + 'url' => $this->elementService->getThumbnailUrl($asset, ['origin' => 'folderPreview']), + 'idPath' => Element\Service::getIdPath($asset), + ]; + } + + $result = ['data' => $assets, 'total' => $list->getTotalCount()]; + + $afterListLoadEvent = new GenericEvent(null, ['list' => $result, 'context' => $requestParams]); + $this->eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::ASSET_LIST_AFTER_LIST_LOAD); + $result = $afterListLoadEvent->getArgument('list'); + + return new GetFolderContentPreviewResult($result['data'], $result['total']); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewPayload.php b/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewPayload.php new file mode 100644 index 00000000..88d5f6c6 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewPayload.php @@ -0,0 +1,35 @@ +query->all(), + ); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewResult.php b/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewResult.php new file mode 100644 index 00000000..03245da7 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewResult.php @@ -0,0 +1,26 @@ +id; + if ($id === null) { + throw new NotFoundHttpException('could not load asset folder'); + } + + $folder = Asset\Folder::getById($id); + if (!$folder instanceof Asset\Folder) { + throw new NotFoundHttpException('could not load asset folder'); + } + + if (!$folder->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to view thumbnail'); + } + + $stream = $folder->getPreviewImage(); + if (!$stream) { + throw new NotFoundHttpException(sprintf('Tree preview thumbnail not available for asset %s', $folder->getId())); + } + + return new FolderThumbnailResult($stream); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetFolderThumbnail/GetFolderThumbnailPayload.php b/src/Handler/Asset/Thumbnail/GetFolderThumbnail/GetFolderThumbnailPayload.php new file mode 100644 index 00000000..d8330d24 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetFolderThumbnail/GetFolderThumbnailPayload.php @@ -0,0 +1,35 @@ +query->has('id') ? $request->query->getInt('id') : null; + + return new static(id: $id); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailHandler.php b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailHandler.php new file mode 100644 index 00000000..b8532b18 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailHandler.php @@ -0,0 +1,96 @@ +id; + $hasFileinfo = $payload->hasFileinfo; + $thumbnailParam = $payload->thumbnailParam; + $configDecoded = $payload->configDecoded; + $queryAll = $payload->queryAll; + $hasThumbnailPreview = $payload->hasThumbnailPreview; + $origin = $payload->origin; + $hasCropPercent = $payload->hasCropPercent; + $cropWidth = $payload->cropWidth; + $cropHeight = $payload->cropHeight; + $cropTop = $payload->cropTop; + $cropLeft = $payload->cropLeft; + $image = Asset\Image::getById($id) ?? throw new AssetNotFoundException($id); + + if (!$image->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to view thumbnail'); + } + + $thumbnailConfig = null; + + if ($thumbnailParam) { + $thumbnailConfig = $image->getThumbnail($thumbnailParam)->getConfig(); + } + if (!$thumbnailConfig) { + if ($configDecoded) { + $thumbnailConfig = $image->getThumbnail($configDecoded)->getConfig(); + } else { + $thumbnailConfig = $image->getThumbnail($queryAll)->getConfig(); + } + } else { + $thumbnailConfig->setHighResolution(1); + } + + $format = strtolower($thumbnailConfig->getFormat()); + if ($format === 'source' || $format === 'print') { + $thumbnailConfig->setFormat('PNG'); + $thumbnailConfig->setRasterizeSVG(true); + } + + if ($hasThumbnailPreview) { + $thumbnailConfig = Asset\Image\Thumbnail\Config::getPreviewConfig(); + if (!$image->getThumbnail($thumbnailConfig)->exists()) { + $this->messageBus->dispatch(new AssetPreviewImageMessage($image->getId())); + + return new GetImageThumbnailResult($image, null, $origin === 'folderPreview', false); + } + } + + if ($hasCropPercent) { + $thumbnailConfig->addItemAt(0, 'cropPercent', [ + 'width' => $cropWidth, + 'height' => $cropHeight, + 'y' => $cropTop, + 'x' => $cropLeft, + ]); + + $thumbnailConfig->generateAutoName(); + } + + $thumbnailResult = $image->getThumbnail($thumbnailConfig); + + return new GetImageThumbnailResult($image, $thumbnailResult, false, $hasFileinfo); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php new file mode 100644 index 00000000..1dba5d1e --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php @@ -0,0 +1,60 @@ +query->getString('config') ?: null; + $cropPercent = $request->query->getString('cropPercent') ?: null; + + return new static( + id: $request->query->getInt('id'), + hasFileinfo: $request->query->has('fileinfo'), + thumbnailParam: $request->query->all('thumbnail') ?: null, + configDecoded: $config !== null ? json_decode($config, true) : null, + queryAll: $request->query->all(), + hasThumbnailPreview: $request->query->has('treepreview'), + origin: $request->query->getString('origin') ?: null, + hasCropPercent: $cropPercent !== null && filter_var($cropPercent, FILTER_VALIDATE_BOOLEAN), + cropWidth: $request->query->getString('cropWidth') ?: null, + cropHeight: $request->query->getString('cropHeight') ?: null, + cropTop: $request->query->getString('cropTop') ?: null, + cropLeft: $request->query->getString('cropLeft') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailResult.php b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailResult.php new file mode 100644 index 00000000..04a9f1ba --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailResult.php @@ -0,0 +1,30 @@ +id; + $path = $payload->path; + $hasThumbnailPreview = $payload->hasThumbnailPreview; + $hasSetTime = $payload->hasSetTime; + $hasSetImage = $payload->hasSetImage; + $hasImage = $payload->hasImage; + $imageId = $payload->imageId; + $time = $payload->time; + $origin = $payload->origin; + $queryAll = $payload->queryAll; + $video = null; + + if ($id !== null) { + $video = Asset\Video::getById($id); + } elseif ($path !== null) { + $video = Asset\Video::getByPath($path); + } + + if (!$video instanceof Asset\Video) { + throw new NotFoundHttpException('could not load video asset'); + } + + if (!$video->isAllowed('view')) { + throw new AccessDeniedHttpException('not allowed to view thumbnail'); + } + + $thumbnailConfig = $queryAll; + if ($hasThumbnailPreview) { + $thumbnailConfig = Asset\Image\Thumbnail\Config::getPreviewConfig(); + } + + $timeInt = is_numeric($time) ? (int) $time : null; + + if ($hasSetTime) { + $video->removeCustomSetting('image_thumbnail_asset'); + $video->setCustomSetting('image_thumbnail_time', $timeInt); + $video->save(); + } + + $image = null; + if ($hasImage) { + $image = Asset\Image::getById($imageId) ?? throw new AssetNotFoundException($imageId); + } + + if ($hasSetImage && $image) { + $video->removeCustomSetting('image_thumbnail_time'); + $video->setCustomSetting('image_thumbnail_asset', $image->getId()); + $video->save(); + } + + $thumb = $video->getImageThumbnail($thumbnailConfig, $timeInt, $image); + + if ($origin === 'treeNode' && !$thumb->exists()) { + $this->messageBus->dispatch(new AssetPreviewImageMessage($video->getId())); + throw new NotFoundHttpException(sprintf('Tree preview thumbnail not available for asset %s', $video->getId())); + } + + $stream = $thumb->getStream(); + if (!$stream) { + throw new NotFoundHttpException('Unable to get video thumbnail for video ' . $video->getId()); + } + + return new GetVideoThumbnailResult($stream, $thumb->getFileExtension()); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailPayload.php b/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailPayload.php new file mode 100644 index 00000000..7b201aaf --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailPayload.php @@ -0,0 +1,53 @@ +query->has('id') ? $request->query->getInt('id') : null, + path: $request->query->getString('path') ?: null, + hasThumbnailPreview: $request->query->has('treepreview'), + hasSetTime: $request->query->has('settime'), + hasSetImage: $request->query->has('setimage'), + hasImage: $request->query->has('image'), + imageId: $request->query->getInt('image'), + time: $request->query->getString('time') ?: null, + origin: $request->query->getString('origin') ?: null, + queryAll: $request->query->all(), + ); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailResult.php b/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailResult.php new file mode 100644 index 00000000..0ed6c6e0 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailResult.php @@ -0,0 +1,26 @@ +assetId; + $updateData = $payload->updateData; + $adminUser = $this->userContext->getAdminUser(); + $asset = Asset::getById($assetId); + $allowUpdate = true; + + if ($asset->isAllowed('settings')) { + $asset->setUserModification($adminUser->getId()); + + if (isset($updateData['parentId']) && $updateData['parentId']) { + $parentAsset = Asset::getById((int) $updateData['parentId']); + + if ($asset->getParentId() !== $parentAsset->getId()) { + if (!$parentAsset->isAllowed('create')) { + throw new RuntimeException('Prevented moving asset - no create permission on new parent.'); + } + + $intendedPath = $parentAsset->getRealPath(); + $pKey = $parentAsset->getKey(); + if (!empty($pKey)) { + $intendedPath .= $parentAsset->getKey() . '/'; + } + + if (Asset::getByPath($intendedPath . $asset->getKey()) != null) { + $allowUpdate = false; + } + + if ($asset->isLocked()) { + $allowUpdate = false; + } + } + } + + if ($allowUpdate) { + if (isset($updateData['filename']) && $updateData['filename'] != $asset->getFilename() && !$asset->isAllowed('rename')) { + unset($updateData['filename']); + Logger::debug('prevented renaming asset because of missing permissions.'); + } + + $asset->setValues($updateData); + $asset->save(); + + return new UpdateAssetResult($this->elementService->getElementTreeNodeConfig($asset)); + } + + $msg = 'prevented moving asset, asset with same path+key already exists at target location or the asset is locked. ID: ' . $asset->getId(); + Logger::debug($msg); + throw new BadRequestHttpException($msg); + } + + if ($asset->isAllowed('rename') && isset($updateData['filename'])) { + $asset->setFilename($updateData['filename']); + $asset->save(); + + return new UpdateAssetResult($this->elementService->getElementTreeNodeConfig($asset)); + } + + Logger::debug('prevented update asset because of missing permissions'); + throw new AccessDeniedHttpException('prevented update asset because of missing permissions'); + } +} diff --git a/src/Handler/Asset/UpdateAsset/UpdateAssetPayload.php b/src/Handler/Asset/UpdateAsset/UpdateAssetPayload.php new file mode 100644 index 00000000..acf8055a --- /dev/null +++ b/src/Handler/Asset/UpdateAsset/UpdateAssetPayload.php @@ -0,0 +1,37 @@ +request->getInt('id'), + updateData: [...$request->request->all(), ...$request->query->all()], + ); + } +} diff --git a/src/Handler/Asset/UpdateAsset/UpdateAssetResult.php b/src/Handler/Asset/UpdateAsset/UpdateAssetResult.php new file mode 100644 index 00000000..9ff7248f --- /dev/null +++ b/src/Handler/Asset/UpdateAsset/UpdateAssetResult.php @@ -0,0 +1,25 @@ +parentId; + $filename = $payload->filename; + $dir = $payload->dir; + $parentAsset = Asset::getById($parentId); + if (!$parentAsset) { + throw new NotFoundHttpException('Parent asset not found'); + } + + if ($dir) { + if (str_contains($dir, '..')) { + throw new BadRequestHttpException('not allowed'); + } + $dir = '/' . trim($dir, '/ '); + } + + return Asset\Service::pathExists($parentAsset->getRealFullPath() . $dir . '/' . $filename); + } +} \ No newline at end of file diff --git a/src/Handler/Asset/Upload/CheckAssetExists/CheckAssetExistsPayload.php b/src/Handler/Asset/Upload/CheckAssetExists/CheckAssetExistsPayload.php new file mode 100644 index 00000000..57886439 --- /dev/null +++ b/src/Handler/Asset/Upload/CheckAssetExists/CheckAssetExistsPayload.php @@ -0,0 +1,39 @@ +query->getInt('parentId'), + filename: $request->query->getString('filename'), + dir: $request->query->getString('dir'), + ); + } +} diff --git a/src/Handler/Asset/Upload/ImportZip/ImportZipHandler.php b/src/Handler/Asset/Upload/ImportZip/ImportZipHandler.php new file mode 100644 index 00000000..f2a4d6fa --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZip/ImportZipHandler.php @@ -0,0 +1,81 @@ +parentId; + $uploadedFilePath = $payload->uploadedFilePath; + $allowOverwrite = $payload->allowOverwrite; + $asset = Asset::getById($parentId) ?? throw new NotFoundHttpException('Parent asset not found'); + + if (!$asset->isAllowed('create')) { + throw new AccessDeniedHttpException('not allowed to create'); + } + + $jobId = uniqid('', false); + $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $jobId . '.zip'; + copy($uploadedFilePath, $zipFile); + + $zip = new ZipArchive(); + if ($zip->open($zipFile) !== true) { + throw new BadRequestHttpException($this->translator->trans('could_not_open_zip_file', [], 'admin')); + } + + $numFiles = $zip->numFiles; + $zip->close(); + + $importZipFilesUrl = $this->router->generate('opendxp_admin_asset_importzipfiles'); + $jobAmount = (int) ceil($numFiles / self::FILES_PER_JOB); + $jobs = []; + for ($i = 0; $i < $jobAmount; $i++) { + $jobs[] = [[ + 'url' => $importZipFilesUrl, + 'method' => 'POST', + 'params' => [ + 'parentId' => $asset->getId(), + 'offset' => $i * self::FILES_PER_JOB, + 'limit' => self::FILES_PER_JOB, + 'jobId' => $jobId, + 'last' => (($i + 1) >= $jobAmount) ? 'true' : '', + 'allowOverwrite' => $allowOverwrite ?: 'false', + ], + ]]; + } + + return new ImportZipResult(jobId: $jobId, jobs: $jobs); + } +} \ No newline at end of file diff --git a/src/Handler/Asset/Upload/ImportZip/ImportZipPayload.php b/src/Handler/Asset/Upload/ImportZip/ImportZipPayload.php new file mode 100644 index 00000000..6fe06ae1 --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZip/ImportZipPayload.php @@ -0,0 +1,43 @@ +files->get('Filedata'); + + return new static( + parentId: $request->query->getInt('parentId'), + uploadedFilePath: $file->getPathname(), + allowOverwrite: $request->query->getString('allowOverwrite') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Upload/ImportZip/ImportZipResult.php b/src/Handler/Asset/Upload/ImportZip/ImportZipResult.php new file mode 100644 index 00000000..bcba977c --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZip/ImportZipResult.php @@ -0,0 +1,26 @@ +parentId; + $jobId = $payload->jobId; + $offset = $payload->offset; + $limit = $payload->limit; + $allowOverwrite = $payload->allowOverwrite; + $isLast = $payload->isLast; + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + $importAsset = Asset::getById($parentId); + $zipFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $jobId . '.zip'; + $tmpDir = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/zip-import'; + + if (!is_dir($tmpDir)) { + $this->filesystem->mkdir($tmpDir); + } + + $zip = new ZipArchive(); + if ($zip->open($zipFile) !== true) { + return; + } + + for ($i = $offset; $i < ($offset + $limit); $i++) { + $path = $zip->getNameIndex($i); + if (str_starts_with($path, '__MACOSX/')) { + continue; + } + if (str_ends_with($path, '/Thumbs.db')) { + continue; + } + if (str_ends_with($path, '/.DS_Store')) { + continue; + } + + if ($path !== false && $zip->extractTo($tmpDir . '/', $path)) { + $tmpFile = $tmpDir . '/' . preg_replace('@^/@', '', $path); + $filename = Element\Service::getValidKey(basename($path), 'asset'); + $relativePath = ''; + if (dirname($path) !== '.') { + $relativePath = dirname($path); + } + $parentPath = $importAsset->getRealFullPath() . '/' . preg_replace('@^/@', '', $relativePath); + $parent = Asset\Service::createFolderByPath($parentPath); + + if (!$allowOverwrite) { + $filename = $this->assetUploadService->getSafeFilename($parent->getRealFullPath(), $filename); + } + + if ($parent->isAllowed('create')) { + if ($allowOverwrite && Asset\Service::pathExists($parent->getRealFullPath() . '/' . $filename)) { + $asset = Asset::getByPath($parent->getRealFullPath() . '/' . $filename); + $asset->setStream(fopen($tmpFile, 'rb', false, File::getContext())); + $asset->save(); + } else { + Asset::create($parent->getId(), [ + 'filename' => $filename, + 'sourcePath' => $tmpFile, + 'userOwner' => $userId, + 'userModification' => $userId, + ]); + } + + @unlink($tmpFile); + } else { + Logger::debug('prevented creating asset because of missing permissions'); + } + } + } + + $zip->close(); + + if ($isLast) { + unlink($zipFile); + } + } +} diff --git a/src/Handler/Asset/Upload/ImportZipFiles/ImportZipFilesPayload.php b/src/Handler/Asset/Upload/ImportZipFiles/ImportZipFilesPayload.php new file mode 100644 index 00000000..b14eac1d --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZipFiles/ImportZipFilesPayload.php @@ -0,0 +1,45 @@ +request->getInt('parentId'), + jobId: $request->request->getString('jobId'), + offset: $request->request->getInt('offset'), + limit: $request->request->getInt('limit'), + allowOverwrite: $request->request->getString('allowOverwrite') === 'true', + isLast: (bool) $request->request->get('last'), + ); + } +} diff --git a/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetHandler.php b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetHandler.php new file mode 100644 index 00000000..8cb701fb --- /dev/null +++ b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetHandler.php @@ -0,0 +1,81 @@ +id; + $filePath = $payload->filePath; + $originalFilename = $payload->originalFilename; + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + $asset = Asset::getById($id) ?? throw new AssetNotFoundException($id); + + $newFilename = Element\Service::getValidKey($originalFilename, 'asset'); + $mimetype = MimeTypes::getDefault()->guessMimeType($filePath); + $newType = Asset::getTypeFromMimeMapping($mimetype, $newFilename); + + if ($newType !== $asset->getType()) { + throw new BadRequestHttpException(sprintf( + $this->translator->trans('asset_type_change_not_allowed', [], 'admin'), + $newType, + $asset->getType(), + )); + } + + $stream = fopen($filePath, 'rb+'); + $asset->setStream($stream); + $asset->setCustomSetting('thumbnails', null); + + if (method_exists($asset, 'getEmbeddedMetaData')) { + $asset->getEmbeddedMetaData(true); + } + + $asset->setUserModification($userId); + + $newFileExt = pathinfo($newFilename, PATHINFO_EXTENSION); + $currentFileExt = pathinfo($asset->getFilename(), PATHINFO_EXTENSION); + if ($newFileExt !== $currentFileExt) { + $newFilename = preg_replace('/\.' . $currentFileExt . '$/i', '.' . $newFileExt, $asset->getFilename()); + $newFilename = Element\Service::getSafeCopyName($newFilename, $asset->getParent()); + $asset->setFilename($newFilename); + } + + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException('missing permission'); + } + + $asset->save(); + + return $asset; + } +} diff --git a/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetPayload.php b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetPayload.php new file mode 100644 index 00000000..beefd3b5 --- /dev/null +++ b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetPayload.php @@ -0,0 +1,43 @@ +files->get('Filedata'); + + return new static( + id: $request->query->getInt('id'), + filePath: $file->getPathname(), + originalFilename: $file->getClientOriginalName(), + ); + } +} diff --git a/src/Handler/Asset/Version/PublishVersion/PublishVersionHandler.php b/src/Handler/Asset/Version/PublishVersion/PublishVersionHandler.php new file mode 100644 index 00000000..07dbdcff --- /dev/null +++ b/src/Handler/Asset/Version/PublishVersion/PublishVersionHandler.php @@ -0,0 +1,56 @@ +versionId; + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + $version = Version::getById($versionId) + ?? throw new AssetVersionNotFoundException($versionId); + + $asset = $version->loadData(); + if (!$asset instanceof Asset) { + throw new AssetVersionNotFoundException($versionId); + } + + $currentAsset = Asset::getById($asset->getId()); + if (!$currentAsset?->isAllowed('publish')) { + throw new AccessDeniedHttpException(); + } + + $asset->setUserModification($userId); + $asset->save(); + + return new AssetResult($asset); + } +} diff --git a/src/Handler/Asset/Version/PublishVersion/PublishVersionPayload.php b/src/Handler/Asset/Version/PublishVersion/PublishVersionPayload.php new file mode 100644 index 00000000..3b59f259 --- /dev/null +++ b/src/Handler/Asset/Version/PublishVersion/PublishVersionPayload.php @@ -0,0 +1,35 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/Asset/Version/ShowVersion/ShowVersionHandler.php b/src/Handler/Asset/Version/ShowVersion/ShowVersionHandler.php new file mode 100644 index 00000000..bb936dce --- /dev/null +++ b/src/Handler/Asset/Version/ShowVersion/ShowVersionHandler.php @@ -0,0 +1,56 @@ +versionId; + $version = Version::getById($versionId) + ?? throw new AssetVersionNotFoundException($versionId); + + $asset = $version->loadData(); + if (!$asset instanceof Asset) { + throw new AssetVersionNotFoundException($versionId); + } + + if (!$asset->isAllowed('versions')) { + throw new AccessDeniedHttpException(); + } + + if ($asset instanceof Asset\Document && $asset->getMimeType() === self::PDF_MIMETYPE) { + return new ShowVersionResult( + asset: $asset, + version: $version, + isPdf: true, + pdfPath: $asset->getRealFullPath(), + ); + } + + return new ShowVersionResult(asset: $asset, version: $version); + } +} diff --git a/src/Handler/Asset/Version/ShowVersion/ShowVersionPayload.php b/src/Handler/Asset/Version/ShowVersion/ShowVersionPayload.php new file mode 100644 index 00000000..ac48539f --- /dev/null +++ b/src/Handler/Asset/Version/ShowVersion/ShowVersionPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + userTimezone: $request->query->getString('userTimezone') ?: null, + ); + } +} diff --git a/src/Handler/Asset/Version/ShowVersion/ShowVersionResult.php b/src/Handler/Asset/Version/ShowVersion/ShowVersionResult.php new file mode 100644 index 00000000..6e87cc63 --- /dev/null +++ b/src/Handler/Asset/Version/ShowVersion/ShowVersionResult.php @@ -0,0 +1,31 @@ +userContext->getAdminUser()?->getId() ?? 0; + $className = $payload->className; + $classId = $payload->classId; + $parentId = $payload->parentId; + $key = $payload->key; + $objectType = $payload->objectType; + $variantViaTree = $payload->variantViaTree; + $parent = DataObject::getById($parentId); + if ($parent === null) { + throw new NotFoundHttpException("Parent object not found: $parentId"); + } + + if (!$parent->isAllowed('create')) { + throw new AccessDeniedHttpException('prevented adding object because of missing permissions'); + } + + if (DataObject\Service::pathExists($parent->getRealFullPath() . '/' . $key)) { + throw new BadRequestHttpException('prevented creating object because object with same path+key already exists'); + } + + if ($variantViaTree) { + if (!$parent instanceof DataObject\Concrete) { + throw new BadRequestHttpException('Parent must be a concrete object for variant creation'); + } + $classId = $parent->getClass()->getId(); + } + + $fqcn = 'OpenDxp\\Model\\DataObject\\' . ucfirst($className); + /** @var DataObject\Concrete $object */ + $object = $this->modelFactory->build($fqcn); + $object->setOmitMandatoryCheck(true); + $object->setClassId($classId); + $object->setClassName($className); + $object->setParentId($parentId); + $object->setKey($key); + $object->setCreationDate(time()); + $object->setUserOwner($userId); + $object->setUserModification($userId); + $object->setPublished(false); + + if (in_array($objectType, [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_VARIANT])) { + $object->setType($objectType); + } + + $object->save(); + + return new AddObjectResult(id: $object->getId(), type: $object->getType()); + } +} diff --git a/src/Handler/DataObject/AddObject/AddObjectPayload.php b/src/Handler/DataObject/AddObject/AddObjectPayload.php new file mode 100644 index 00000000..6ae61e2f --- /dev/null +++ b/src/Handler/DataObject/AddObject/AddObjectPayload.php @@ -0,0 +1,45 @@ +request->getString('className'), + classId: $request->request->getString('classId'), + parentId: $request->request->getInt('parentId'), + key: $request->request->getString('key'), + objectType: $request->request->getString('objecttype'), + variantViaTree: (bool) $request->request->get('variantViaTree'), + ); + } +} diff --git a/src/Handler/DataObject/AddObject/AddObjectResult.php b/src/Handler/DataObject/AddObject/AddObjectResult.php new file mode 100644 index 00000000..d038bd9a --- /dev/null +++ b/src/Handler/DataObject/AddObject/AddObjectResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $parentId = $payload->parentId; + $key = $payload->key; + $parent = DataObject::getById($parentId); + if ($parent === null) { + throw new NotFoundHttpException("Parent object not found: $parentId"); + } + + if (!$parent->isAllowed('create')) { + throw new AccessDeniedHttpException('prevented creating folder because of missing permissions'); + } + + if (DataObject\Service::pathExists($parent->getRealFullPath() . '/' . $key)) { + throw new BadRequestHttpException('folder with same path+key already exists'); + } + + $folder = DataObject\Folder::create([ + 'parentId' => $parentId, + 'creationDate' => time(), + 'userOwner' => $userId, + 'userModification' => $userId, + 'key' => $key, + 'published' => true, + ]); + + $folder->save(); + } +} diff --git a/src/Handler/DataObject/AddObjectFolder/AddObjectFolderPayload.php b/src/Handler/DataObject/AddObjectFolder/AddObjectFolderPayload.php new file mode 100644 index 00000000..0a90f056 --- /dev/null +++ b/src/Handler/DataObject/AddObjectFolder/AddObjectFolderPayload.php @@ -0,0 +1,37 @@ +request->getInt('parentId'), + key: $request->request->getString('key'), + ); + } +} diff --git a/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByHandler.php b/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByHandler.php new file mode 100644 index 00000000..ed05817e --- /dev/null +++ b/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByHandler.php @@ -0,0 +1,155 @@ +userContext->getAdminUser(); + $id = $payload->id; + $sortBy = $payload->sortBy; + $sortOrder = in_array($payload->sortOrder, ['ASC', 'DESC']) ? $payload->sortOrder : 'ASC'; + + $object = DataObject::getById($id); + + if (!$object) { + throw new NotFoundHttpException(sprintf('DataObject with id %d not found', $id)); + } + + $currentSortBy = $object->getChildrenSortBy(); + + $object->setChildrenSortBy($sortBy); + $object->setChildrenSortOrder($sortOrder); + + if ($currentSortBy !== $sortBy) { + if (!$adminUser->isAdmin() && !$adminUser->isAllowed('objects_sort_method')) { + throw new AccessDeniedHttpException('Changing the sort method is only allowed for admin users'); + } + + if ($sortBy === 'index') { + $this->reindexBasedOnSortOrder($object, $sortOrder); + } + } + + $object->save(); + } + + private function reindexBasedOnSortOrder(DataObject\AbstractObject $parentObject, string $currentSortOrder): void + { + $fn = function () use ($parentObject, $currentSortOrder): void { + $list = new DataObject\Listing(); + + $db = Db::get(); + $db->executeStatement( + 'UPDATE ' . $list->getDao()->getTableName() . ' o, + ( + SELECT newIndex, id FROM ( + SELECT @n := @n +1 AS newIndex, id + FROM ' . $list->getDao()->getTableName() . ', + (SELECT @n := -1) variable + WHERE parentId = ? ORDER BY `key` ' . $currentSortOrder + . ') tmp + ) order_table + SET o.index = order_table.newIndex + WHERE o.id=order_table.id', + [ + $parentObject->getId(), + ] + ); + + $db = Db::get(); + $children = $db->fetchAllAssociative( + 'SELECT id, modificationDate, versionCount FROM objects WHERE parentId = ? ORDER BY `index` ASC', + [$parentObject->getId()] + ); + + foreach ($children as $child) { + $this->updateLatestVersionIndex($child['id'], $child['modificationDate']); + + DataObject::clearDependentCacheByObjectId($child['id']); + } + }; + + $this->executeInsideTransaction($fn); + } + + private function updateLatestVersionIndex(int $objectId, int $newIndex): void + { + $object = DataObject\Concrete::getById($objectId); + + if ( + $object && + $object->getType() !== DataObject::OBJECT_TYPE_FOLDER && + $latestVersion = $object->getLatestVersion() + ) { + // don't renew references (which means loading the target elements) + // Not needed as we just save a new version with the updated index + $object = $latestVersion->loadData(false); + if ($newIndex !== $object->getIndex()) { + $object->setIndex($newIndex); + } + $latestVersion->save(); + } + } + + private function executeInsideTransaction(callable $fn): void + { + $maxRetries = 5; + for ($retries = 0; $retries < $maxRetries; $retries++) { + try { + Db::get()->beginTransaction(); + + $fn(); + + Db::get()->commit(); + + break; + } catch (Exception $e) { + Db::get()->rollBack(); + + // we try to start the transaction $maxRetries times again (deadlocks, ...) + if ($retries < ($maxRetries - 1)) { + $run = $retries + 1; + $waitTime = random_int(1, 5) * 100000; // microseconds + Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')'); + + usleep($waitTime); // wait specified time until we restart the transaction + } else { + // if the transaction still fail after $maxRetries retries, we throw out the exception + Logger::error('Finally giving up restarting the same transaction again and again, last message: ' . $e->getMessage()); + + throw $e; + } + } + } + } +} diff --git a/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByPayload.php b/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByPayload.php new file mode 100644 index 00000000..a858e003 --- /dev/null +++ b/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByPayload.php @@ -0,0 +1,39 @@ +request->getInt('id'), + sortBy: $request->request->getString('sortBy'), + sortOrder: $request->request->getString('childrenSortOrder'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/AddClass/AddClassHandler.php b/src/Handler/DataObject/ClassDef/AddClass/AddClassHandler.php new file mode 100644 index 00000000..fbbcff40 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/AddClass/AddClassHandler.php @@ -0,0 +1,51 @@ +className); + $className = preg_replace('/^\d+/', '', $className); + + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + $existingClass = DataObject\ClassDefinition::getById($payload->classId); + if ($existingClass) { + throw new Exception('Class identifier already exists'); + } + + $class = DataObject\ClassDefinition::create([ + 'name' => $className, + 'userOwner' => $userId, + ]); + + $class->setId($payload->classId); + $class->save(true); + + return new AddClassResult(id: $class->getId()); + } +} diff --git a/src/Handler/DataObject/ClassDef/AddClass/AddClassPayload.php b/src/Handler/DataObject/ClassDef/AddClass/AddClassPayload.php new file mode 100644 index 00000000..a53175e1 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/AddClass/AddClassPayload.php @@ -0,0 +1,24 @@ +request->getString('className'), + classId: $request->request->getString('classIdentifier') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/AddClass/AddClassResult.php b/src/Handler/DataObject/ClassDef/AddClass/AddClassResult.php new file mode 100644 index 00000000..ee3247bc --- /dev/null +++ b/src/Handler/DataObject/ClassDef/AddClass/AddClassResult.php @@ -0,0 +1,25 @@ +data; + $permissionMap = ['class' => 'classes', 'objectbrick' => 'objectbricks', 'fieldcollection' => 'fieldcollections', 'customlayout' => 'classes']; + $type = $data['type']; + + if (isset($permissionMap[$type])) { + $adminUser = $this->userContext->getAdminUser(); + if (!$adminUser?->isAllowed($permissionMap[$type])) { + throw new AccessDeniedHttpException('Permission denied for bulk commit of type: ' . $type); + } + } + + $session = Session::getSessionBag($this->requestStack->getCurrentRequest()->getSession(), 'opendxp_objects'); + $filename = $session->get('class_bulk_import_file'); + + $json = @file_get_contents($filename); + $json = json_decode($json, true); + + $name = $data['name']; + $list = $json[$type]; + + foreach ($list as $item) { + unset($item['creationDate'], $item['modificationDate'], $item['userOwner'], $item['userModification']); + + if ($type === 'class' && $item['name'] == $name) { + $class = DataObject\ClassDefinition::getByName($name); + if (!$class) { + $class = new DataObject\ClassDefinition(); + $class->setName($name); + } + if (!DataObject\ClassDefinition\Service::importClassDefinitionFromJson($class, json_encode($item), true)) { + throw new RuntimeException('Failed to import class definition: ' . $name); + } + + return; + } + + if ($type === 'objectbrick' && $item['key'] == $name) { + if (!$brick = DataObject\Objectbrick\Definition::getByKey($name)) { + $brick = new DataObject\Objectbrick\Definition(); + $brick->setKey($name); + } + if (!DataObject\ClassDefinition\Service::importObjectBrickFromJson($brick, json_encode($item), true)) { + throw new RuntimeException('Failed to import objectbrick: ' . $name); + } + + return; + } + + if ($type === 'fieldcollection' && $item['key'] == $name) { + if (!$fieldCollection = DataObject\Fieldcollection\Definition::getByKey($name)) { + $fieldCollection = new DataObject\Fieldcollection\Definition(); + $fieldCollection->setKey($name); + } + if (!DataObject\ClassDefinition\Service::importFieldCollectionFromJson($fieldCollection, json_encode($item), true)) { + throw new RuntimeException('Failed to import field collection: ' . $name); + } + + return; + } + + if ($type === 'customlayout') { + $layoutData = json_decode(base64_decode($data['name']), true); + $className = $layoutData['className']; + $layoutName = $layoutData['name']; + + if ($item['name'] == $layoutName && $item['className'] == $className) { + $class = DataObject\ClassDefinition::getByName($className); + if (!$class) { + throw new BadRequestHttpException('Class does not exist'); + } + + $classId = $class->getId(); + + $layoutList = new DataObject\ClassDefinition\CustomLayout\Listing(); + $layoutList->setFilter(fn (DataObject\ClassDefinition\CustomLayout $layout) => $layout->getName() === $layoutName && $layout->getClassId() === $classId); + $layoutList = $layoutList->load(); + + $layoutDefinition = null; + if ($layoutList) { + $layoutDefinition = array_values($layoutList)[0]; + } + + if (!$layoutDefinition) { + $layoutDefinition = new DataObject\ClassDefinition\CustomLayout(); + $layoutDefinition->setName($layoutName); + $layoutDefinition->setClassId($classId); + } + + $layoutDefinition->setDescription($item['description']); + $layoutDef = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($item['layoutDefinitions'], true); + $layoutDefinition->setLayoutDefinitions($layoutDef); + $layoutDefinition->save(); + } + } + } + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php b/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php new file mode 100644 index 00000000..b26a97a1 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php @@ -0,0 +1,22 @@ +request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPreparePayload.php b/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPreparePayload.php new file mode 100644 index 00000000..482f32ee --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPreparePayload.php @@ -0,0 +1,22 @@ +request->getString('data'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkImport/BulkImportHandler.php b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportHandler.php new file mode 100644 index 00000000..c95fa647 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportHandler.php @@ -0,0 +1,69 @@ +json); + + $parsed = json_decode($payload->json, true); + $result = []; + + foreach ($parsed as $groupName => $group) { + foreach ($group as $groupItem) { + $displayName = null; + $icon = null; + + if ($groupName === 'class') { + $name = $groupItem['name']; + $icon = 'class'; + } elseif ($groupName === 'customlayout') { + $className = $groupItem['className']; + $layoutData = ['className' => $className, 'name' => $groupItem['name']]; + $name = base64_encode(json_encode($layoutData)); + $displayName = $className . ' / ' . $groupItem['name']; + $icon = 'custom_views'; + } else { + if ($groupName === 'objectbrick') { + $icon = 'objectbricks'; + } elseif ($groupName === 'fieldcollection') { + $icon = 'fieldcollection'; + } + $name = $groupItem['key']; + } + + if (!$displayName) { + $displayName = $name; + } + + $result[] = [ + 'icon' => $icon, + 'checked' => true, + 'type' => $groupName, + 'name' => $name, + 'displayName' => $displayName, + ]; + } + } + + return new BulkImportResult(items: $result, tmpFile: $tmpName); + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkImport/BulkImportPayload.php b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportPayload.php new file mode 100644 index 00000000..661deec7 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportPayload.php @@ -0,0 +1,26 @@ +files->get('Filedata'); + + return new static( + json: $file !== null ? (file_get_contents($file->getPathname()) ?: '') : '', + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkImport/BulkImportResult.php b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportResult.php new file mode 100644 index 00000000..c686d6af --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportResult.php @@ -0,0 +1,26 @@ +id); + if ($class) { + $class->delete(); + } + } +} diff --git a/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassPayload.php b/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassPayload.php new file mode 100644 index 00000000..2232ff0d --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassPayload.php @@ -0,0 +1,22 @@ +request->getString('id') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsHandler.php b/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsHandler.php new file mode 100644 index 00000000..34361f07 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsHandler.php @@ -0,0 +1,34 @@ +id); + if (!$selectOptions instanceof DataObject\SelectOptions\Config) { + throw new NotFoundHttpException('Not Found', code: 1677133720896); + } + + $selectOptions->delete(); + } +} diff --git a/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsPayload.php b/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsPayload.php new file mode 100644 index 00000000..ee391d3c --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsPayload.php @@ -0,0 +1,23 @@ +request->getString(Config::PROPERTY_ID) ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportHandler.php b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportHandler.php new file mode 100644 index 00000000..e219d160 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportHandler.php @@ -0,0 +1,73 @@ +requestStack->getCurrentRequest()->getSession(), 'opendxp_objects'); + $list = json_decode($session->get('class_bulk_export_settings'), true); + + $adminUser = $this->userContext->getAdminUser(); + $result = []; + + foreach ($list as $item) { + if ($item['type'] === 'fieldcollection' && $adminUser->isAllowed('fieldcollections')) { + if ($fieldCollection = DataObject\Fieldcollection\Definition::getByKey($item['name'])) { + $fieldCollectionJson = json_decode(DataObject\ClassDefinition\Service::generateFieldCollectionJson($fieldCollection)); + $fieldCollectionJson->key = $item['name']; + $result['fieldcollection'][] = $fieldCollectionJson; + } + } elseif ($item['type'] === 'class' && $adminUser->isAllowed('classes')) { + if ($class = DataObject\ClassDefinition::getByName($item['name'])) { + $data = json_decode(DataObject\ClassDefinition\Service::generateClassDefinitionJson($class)); + $data->name = $item['name']; + $result['class'][] = $data; + } + } elseif ($item['type'] === 'objectbrick' && $adminUser->isAllowed('objectbricks')) { + if ($objectBrick = DataObject\Objectbrick\Definition::getByKey($item['name'])) { + $objectBrickJson = json_decode(DataObject\ClassDefinition\Service::generateObjectBrickJson($objectBrick)); + $objectBrickJson->key = $item['name']; + $result['objectbrick'][] = $objectBrickJson; + } + } elseif ($item['type'] === 'customlayout' && $adminUser->isAllowed('classes')) { + if ($customLayout = DataObject\ClassDefinition\CustomLayout::getById($item['name'])) { + $classId = $customLayout->getClassId(); + $class = DataObject\ClassDefinition::getById($classId); + $customLayoutJson = json_decode(DataObject\ClassDefinition\Service::generateCustomLayoutJson($customLayout)); + $customLayoutJson->name = $customLayout->getName(); + $customLayoutJson->className = $class->getName(); + $result['customlayout'][] = $customLayoutJson; + } + } + } + + return new DoBulkExportResult(json: json_encode($result, JSON_PRETTY_PRINT)); + } +} diff --git a/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportResult.php b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportResult.php new file mode 100644 index 00000000..322da383 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportResult.php @@ -0,0 +1,25 @@ +id); + if (!$class instanceof DataObject\ClassDefinition) { + $errorMessage = ': Class with id [ ' . $payload->id . ' not found. ]'; + Logger::error($errorMessage); + + throw new NotFoundHttpException($errorMessage); + } + + $json = DataObject\ClassDefinition\Service::generateClassDefinitionJson($class); + + return new ExportClassResult( + json: $json, + className: $class->getName(), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/ExportClass/ExportClassPayload.php b/src/Handler/DataObject/ClassDef/ExportClass/ExportClassPayload.php new file mode 100644 index 00000000..6324e1e9 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/ExportClass/ExportClassPayload.php @@ -0,0 +1,22 @@ +query->getString('id') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/ExportClass/ExportClassResult.php b/src/Handler/DataObject/ClassDef/ExportClass/ExportClassResult.php new file mode 100644 index 00000000..49c2a414 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/ExportClass/ExportClassResult.php @@ -0,0 +1,26 @@ + $assetType]; + } + + return new GetAssetTypesResult(types: $typeItems); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetAssetTypes/GetAssetTypesResult.php b/src/Handler/DataObject/ClassDef/GetAssetTypes/GetAssetTypesResult.php new file mode 100644 index 00000000..7a0659a8 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetAssetTypes/GetAssetTypesResult.php @@ -0,0 +1,25 @@ +id); + if (!$class) { + throw new NotFoundHttpException('Class not found'); + } + + $class->setFieldDefinitions([]); + $isWriteable = $class->isWritable(); + $classData = $class->getObjectVars(); + $classData['isWriteable'] = $isWriteable; + + return new GetClassResult(classData: $classData); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClass/GetClassPayload.php b/src/Handler/DataObject/ClassDef/GetClass/GetClassPayload.php new file mode 100644 index 00000000..0b3f29df --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClass/GetClassPayload.php @@ -0,0 +1,22 @@ +query->getString('id') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClass/GetClassResult.php b/src/Handler/DataObject/ClassDef/GetClass/GetClassResult.php new file mode 100644 index 00000000..218dc816 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClass/GetClassResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $result = []; + + if ($adminUser->isAllowed('fieldcollections')) { + $fieldCollections = new DataObject\Fieldcollection\Definition\Listing(); + $fieldCollections = $fieldCollections->load(); + + foreach ($fieldCollections as $fieldCollection) { + $result[] = [ + 'icon' => 'fieldcollection', + 'checked' => true, + 'type' => 'fieldcollection', + 'name' => $fieldCollection->getKey(), + 'displayName' => $fieldCollection->getKey(), + ]; + } + } + + if ($adminUser->isAllowed('classes')) { + $classes = new DataObject\ClassDefinition\Listing(); + $classes->setOrder('ASC'); + $classes->setOrderKey('id'); + $classes = $classes->load(); + + foreach ($classes as $class) { + $result[] = [ + 'icon' => 'class', + 'checked' => true, + 'type' => 'class', + 'name' => $class->getName(), + 'displayName' => $class->getName(), + ]; + } + } + + if ($adminUser->isAllowed('objectbricks')) { + $objectBricks = new DataObject\Objectbrick\Definition\Listing(); + $objectBricks = $objectBricks->loadNames(); + + foreach ($objectBricks as $brickName) { + $result[] = [ + 'icon' => 'objectbricks', + 'checked' => true, + 'type' => 'objectbrick', + 'name' => $brickName, + 'displayName' => $brickName, + ]; + } + } + + if ($adminUser->isAllowed('classes')) { + $customLayouts = new DataObject\ClassDefinition\CustomLayout\Listing(); + $customLayouts = $customLayouts->load(); + foreach ($customLayouts as $customLayout) { + $class = DataObject\ClassDefinition::getById($customLayout->getClassId()); + $displayName = $class->getName() . ' / ' . $customLayout->getName(); + + $result[] = [ + 'icon' => 'custom_views', + 'checked' => true, + 'type' => 'customlayout', + 'name' => $customLayout->getId(), + 'displayName' => $displayName, + ]; + } + } + + return new GetClassBulkExportListResult(data: $result); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassBulkExportList/GetClassBulkExportListResult.php b/src/Handler/DataObject/ClassDef/GetClassBulkExportList/GetClassBulkExportListResult.php new file mode 100644 index 00000000..dc9467cf --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassBulkExportList/GetClassBulkExportListResult.php @@ -0,0 +1,25 @@ +id); + if (!$class) { + throw new NotFoundHttpException('Class not found'); + } + + $filteredDefinitions = DataObject\Service::getCustomLayoutDefinitionForGridColumnConfig($class, $payload->objectId); + + /** @var DataObject\ClassDefinition\Layout $layoutDefinitions */ + $layoutDefinitions = $filteredDefinitions['layoutDefinition'] ?? false; + $filteredFieldDefinition = $filteredDefinitions['fieldDefinition'] ?? false; + + $class->setFieldDefinitions([]); + + $result = []; + + DataObject\Service::enrichLayoutDefinition($layoutDefinitions); + + $result['objectColumns']['children'] = $layoutDefinitions->getChildren(); + $result['objectColumns']['nodeLabel'] = 'object_columns'; + $result['objectColumns']['nodeType'] = 'object'; + + $systemColumnNames = DataObject\Concrete::SYSTEM_COLUMN_NAMES; + $systemColumns = []; + foreach ($systemColumnNames as $systemColumn) { + $systemColumns[] = ['title' => $systemColumn, 'name' => $systemColumn, 'datatype' => 'data', 'fieldtype' => 'system']; + } + $result['systemColumns']['nodeLabel'] = 'system_columns'; + $result['systemColumns']['nodeType'] = 'system'; + $result['systemColumns']['children'] = $systemColumns; + + $list = new DataObject\Objectbrick\Definition\Listing(); + $list = $list->load(); + + foreach ($list as $brickDefinition) { + $classDefs = $brickDefinition->getClassDefinitions(); + if (!empty($classDefs)) { + foreach ($classDefs as $classDef) { + if ($classDef['classname'] == $class->getName()) { + $fieldName = $classDef['fieldname']; + if (isset($filteredFieldDefinition[$fieldName]) && !$filteredFieldDefinition[$fieldName]) { + continue; + } + + $key = $brickDefinition->getKey(); + + $brickLayoutDefinitions = $brickDefinition->getLayoutDefinitions(); + $context = [ + 'containerType' => 'objectbrick', + 'containerKey' => $key, + 'outerFieldname' => $fieldName, + ]; + DataObject\Service::enrichLayoutDefinition($brickLayoutDefinitions, null, $context); + + $result[$key]['nodeLabel'] = $key; + $result[$key]['brickField'] = $fieldName; + $result[$key]['nodeType'] = 'objectbricks'; + $result[$key]['children'] = $brickLayoutDefinitions->getChildren(); + + break; + } + } + } + } + + return new GetClassDefinitionForColumnConfigResult(config: $result); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigPayload.php b/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigPayload.php new file mode 100644 index 00000000..c00ac6b6 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigPayload.php @@ -0,0 +1,24 @@ +query->getString('id') ?: null, + objectId: $request->query->getInt('oid'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigResult.php b/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigResult.php new file mode 100644 index 00000000..d8424f08 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigResult.php @@ -0,0 +1,25 @@ +type; + $classId = $payload->classId; + + if ($type === '') { + return new GetClassIconsResult(icons: []); + } + + $iconDir = OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img'; + + if ($type === null) { + $icons = [ + ...FileSystemHelper::scanDirectory($iconDir . '/object-icons/'), + ...FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'), + ...FileSystemHelper::scanDirectory($iconDir . '/twemoji/'), + ]; + } else { + $icons = match ($type) { + 'color' => FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'), + 'white' => FileSystemHelper::scanDirectory($iconDir . '/flat-white-icons/'), + 'twemoji-1', 'twemoji-2', 'twemoji-3', + 'twemoji_variants-1', 'twemoji_variants-2', 'twemoji_variants-3' + => FileSystemHelper::scanDirectory($iconDir . '/twemoji/'), + default => [], + }; + } + + $style = $type === 'white' ? 'background-color:#000' : ''; + + foreach ($icons as &$icon) { + $icon = str_replace(OPENDXP_WEB_ROOT, '', $icon); + } + + $event = new GenericEvent(null, ['icons' => $icons, 'classId' => $classId]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECT_ICONS_PRE_SEND_DATA); + $icons = $event->getArgument('icons'); + + $startIndex = 0; + + if ($type !== null && str_starts_with($type, 'twemoji')) { + foreach ($icons as $index => $twemojiIcon) { + $iconBase = basename($twemojiIcon); + $explodeByHyphen = explode('-', $iconBase); + + if ( + (!str_starts_with($type, 'twemoji_variants') && isset($explodeByHyphen[1])) || + (str_starts_with($type, 'twemoji_variants') && !isset($explodeByHyphen[1])) + ) { + unset($icons[$index]); + } + } + + $icons = array_values($icons); + $limit = count($icons); + + if (str_ends_with($type, '-1')) { + $limit = (int) floor($limit / 3); + } + if (str_ends_with($type, '-2')) { + $startIndex = (int) floor($limit / 3); + $limit = (int) floor($limit / 3 * 2); + } + if (str_ends_with($type, '-3')) { + $startIndex = (int) floor($limit / 3 * 2); + } + } else { + $limit = count($icons); + } + + $result = []; + for ($i = $startIndex; $i < $limit; $i++) { + $icon = $icons[$i]; + $content = file_get_contents(OPENDXP_WEB_ROOT . $icon); + $result[] = [ + 'text' => sprintf( + '', + $style, + mime_content_type(OPENDXP_WEB_ROOT . $icon), + base64_encode($content) + ), + 'value' => $icon, + ]; + } + + return new GetClassIconsResult(icons: $result); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsPayload.php b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsPayload.php new file mode 100644 index 00000000..7873d07c --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsPayload.php @@ -0,0 +1,24 @@ +query->getString('type') ?: null, + classId: $request->query->getString('classId') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php new file mode 100644 index 00000000..331e5263 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php @@ -0,0 +1,25 @@ +createAllowed; + $withId = $payload->withId; + $useTitle = $payload->useTitle; + $grouped = $payload->grouped; + $adminUser = $this->userContext->getAdminUser(); + $classesList = new DataObject\ClassDefinition\Listing(); + $classesList->setOrderKey('name'); + $classesList->setOrder('asc'); + $classes = $classesList->load(); + + if ($createAllowed) { + $classes = array_filter($classes, fn ($class) => $adminUser->isAllowed($class->getId(), 'class')); + $classes = array_values($classes); + } + + $getClassConfig = static function ($class) use ($withId, $useTitle): array { + $text = $class->getName(); + if ($useTitle) { + $text = $class->getTitle() ?: $class->getName(); + } + if ($withId) { + $text .= ' (' . $class->getId() . ')'; + } + + $hasBrickField = false; + foreach ($class->getFieldDefinitions() as $fieldDefinition) { + if ($fieldDefinition instanceof DataObject\ClassDefinition\Data\Objectbricks) { + $hasBrickField = true; + break; + } + } + + return [ + 'id' => $class->getId(), + 'text' => $text, + 'leaf' => true, + 'icon' => $class->getIcon() ? htmlspecialchars($class->getIcon()) : self::DEFAULT_ICON, + 'cls' => 'opendxp_class_icon', + 'propertyVisibility' => $class->getPropertyVisibility(), + 'enableGridLocking' => $class->isEnableGridLocking(), + 'hasBrickField' => $hasBrickField, + ]; + }; + + $groups = []; + foreach ($classes as $class) { + $groupName = null; + + if ($class->getGroup()) { + $type = 'manual'; + $groupName = $class->getGroup(); + } else { + $type = 'auto'; + if (preg_match('@^([A-Za-z])([^A-Z]+)@', $class->getName(), $matches)) { + $groupName = $matches[0]; + } + if (!$groupName) { + $groupName = $class->getName(); + } + } + + $groupName = Translation::getByKeyLocalized($groupName, Translation::DOMAIN_ADMIN, true, true); + + if (!isset($groups[$groupName])) { + $groups[$groupName] = [ + 'classes' => [], + 'type' => $type, + ]; + } + $groups[$groupName]['classes'][] = $class; + } + + $treeNodes = []; + if ($groups !== []) { + $types = array_column($groups, 'type'); + array_multisort($types, SORT_ASC, array_keys($groups), SORT_ASC, $groups); + } + + if (!$grouped) { + foreach ($groups as $groupName => $groupData) { + foreach ($groupData['classes'] as $class) { + $node = $getClassConfig($class); + if (count($groupData['classes']) > 1 || $groupData['type'] === 'manual') { + $node['group'] = $groupName; + } + $treeNodes[] = $node; + } + } + } else { + foreach ($groups as $groupName => $groupData) { + if (count($groupData['classes']) === 1 && $groupData['type'] === 'auto') { + $node = $getClassConfig($groupData['classes'][0]); + } else { + $node = [ + 'id' => 'folder_' . $groupName, + 'text' => $groupName, + 'leaf' => false, + 'expandable' => true, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'children' => [], + ]; + + foreach ($groupData['classes'] as $class) { + $node['children'][] = $getClassConfig($class); + } + } + + $treeNodes[] = $node; + } + } + + return new GetClassTreeResult(nodes: $treeNodes); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreePayload.php b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreePayload.php new file mode 100644 index 00000000..d80db648 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreePayload.php @@ -0,0 +1,28 @@ +query->get('createAllowed'), + withId: (bool) $request->query->get('withId'), + useTitle: (bool) $request->query->get('useTitle'), + grouped: (bool) $request->query->get('grouped'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeResult.php b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeResult.php new file mode 100644 index 00000000..e7e6c53a --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeResult.php @@ -0,0 +1,25 @@ + $documentType]; + } + + return new GetDocumentTypesResult(types: $typeItems); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetDocumentTypes/GetDocumentTypesResult.php b/src/Handler/DataObject/ClassDef/GetDocumentTypes/GetDocumentTypesResult.php new file mode 100644 index 00000000..03125457 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetDocumentTypes/GetDocumentTypesResult.php @@ -0,0 +1,25 @@ +id); + if (!$selectOptions instanceof DataObject\SelectOptions\Config) { + throw new NotFoundHttpException('Not Found', code: 1677133720896); + } + + $data = $selectOptions->getObjectVars(); + $data['isWriteable'] = $selectOptions->isWriteable(); + $data['enumName'] = $selectOptions->getEnumName(true); + + return new GetSelectOptionsResult(data: $data); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsPayload.php b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsPayload.php new file mode 100644 index 00000000..dacf364f --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsPayload.php @@ -0,0 +1,23 @@ +query->getString(Config::PROPERTY_ID) ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsResult.php b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsResult.php new file mode 100644 index 00000000..99611403 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsResult.php @@ -0,0 +1,25 @@ +grouped; + $configurations = $groups = []; + + $selectOptionConfigs = new DataObject\SelectOptions\Config\Listing(); + foreach ($selectOptionConfigs as $selectOptionConfig) { + $id = $selectOptionConfig->getId(); + $configurationData = [ + 'id' => $id, + 'text' => $id, + 'leaf' => true, + 'iconCls' => 'opendxp_icon_select', + ]; + + if ($grouped === 0 || !$selectOptionConfig->hasGroup()) { + $configurations[] = $configurationData; + + continue; + } + + $group = $selectOptionConfig->getGroup(); + if (!isset($groups[$group])) { + $groups[$group] = [ + 'id' => 'group_' . $id, + 'text' => htmlspecialchars($group ?? ''), + 'expandable' => true, + 'leaf' => false, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'group' => $group, + 'children' => [], + ]; + } + $groups[$group]['children'][] = $configurationData; + } + + foreach ($groups as $group) { + $configurations[] = $group; + } + + $event = new GenericEvent(null, ['list' => $configurations]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_SELECTOPTIONS_LIST_PRE_SEND_DATA); + $configurations = $event->getArgument('list'); + + return new GetSelectOptionsTreeResult(configurations: $configurations); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreePayload.php b/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreePayload.php new file mode 100644 index 00000000..02e7c473 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreePayload.php @@ -0,0 +1,22 @@ +query->getInt('grouped'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreeResult.php b/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreeResult.php new file mode 100644 index 00000000..27cf6a62 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreeResult.php @@ -0,0 +1,25 @@ +id); + if (!$selectOptions instanceof DataObject\SelectOptions\Config) { + throw new NotFoundHttpException('Not Found', code: 1677133720896); + } + + $usages = []; + foreach ($selectOptions->getFieldsUsedIn() as $className => $fieldNames) { + foreach ($fieldNames as $fieldName) { + $usages[] = [ + 'class' => $className, + 'field' => $fieldName, + ]; + } + } + + return new GetSelectOptionsUsagesResult(usages: $usages); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesPayload.php b/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesPayload.php new file mode 100644 index 00000000..fd0eb1a4 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesPayload.php @@ -0,0 +1,23 @@ +query->getString(Config::PROPERTY_ID) ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesResult.php b/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesResult.php new file mode 100644 index 00000000..417559b1 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesResult.php @@ -0,0 +1,25 @@ +className; + $obj = DataObject::getByPath($payload->objPath) ?? new $fqClassName(); + + $textLayout = new DataObject\ClassDefinition\Layout\Text(); + $textLayout->setName('textLayoutPreview' . $payload->className); + + $context = [ + 'data' => $payload->renderingData, + ]; + + if ($payload->renderingClass) { + $textLayout->setRenderingClass($payload->renderingClass); + $textLayout->setRenderingData($payload->renderingData); + } + + if ($payload->html) { + $textLayout->setHtml($payload->html); + } + + $renderedHtml = $textLayout->enrichLayoutDefinition($obj, $context)->getHtml(); + + $content = + "\n" . + "\n" . + '\n" . + "\n\n" . + "\n" . + $renderedHtml . + "\n\n\n" . + "\n"; + + return new GetTextLayoutPreviewResult(content: $content); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewPayload.php b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewPayload.php new file mode 100644 index 00000000..420174e4 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewPayload.php @@ -0,0 +1,30 @@ +query->getString('previewObject'), + className: $request->query->getString('className') ?: null, + renderingData: $request->query->getString('renderingData') ?: null, + renderingClass: $request->query->getString('renderingClass') ?: null, + html: $request->query->getString('html') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewResult.php b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewResult.php new file mode 100644 index 00000000..27923881 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewResult.php @@ -0,0 +1,25 @@ +getSupportedTypes() as $type) { + $res[] = [ + 'key' => $type, + 'value' => $this->translator->trans($type, [], 'admin'), + ]; + } + + return new GetVideoAllowedTypesResult(types: $res); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetVideoAllowedTypes/GetVideoAllowedTypesResult.php b/src/Handler/DataObject/ClassDef/GetVideoAllowedTypes/GetVideoAllowedTypesResult.php new file mode 100644 index 00000000..872bb7a5 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetVideoAllowedTypes/GetVideoAllowedTypesResult.php @@ -0,0 +1,25 @@ +id); + if (!$class) { + throw new NotFoundHttpException('Class not found'); + } + + $success = DataObject\ClassDefinition\Service::importClassDefinitionFromJson($class, $payload->json, false, true); + if (!$success) { + throw new RuntimeException('Failed to import class definition'); + } + } +} diff --git a/src/Handler/DataObject/ClassDef/ImportClass/ImportClassPayload.php b/src/Handler/DataObject/ClassDef/ImportClass/ImportClassPayload.php new file mode 100644 index 00000000..2c94ff98 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/ImportClass/ImportClassPayload.php @@ -0,0 +1,28 @@ +files->get('Filedata'); + + return new static( + id: $request->query->getString('id') ?: null, + json: $file !== null ? (file_get_contents($file->getPathname()) ?: '') : '', + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionHandler.php b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionHandler.php new file mode 100644 index 00000000..82e0bcaa --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionHandler.php @@ -0,0 +1,104 @@ +userContext->getAdminUser()?->getId() ?? 0; + $class = DataObject\ClassDefinition::getById($payload->id); + if (!$class) { + throw new NotFoundHttpException('Class not found'); + } + + $values = $payload->values; + $configuration = $payload->configuration; + + if ($class->getModificationDate() != $values['modificationDate']) { + throw new BadRequestHttpException('The class was modified during editing, please reload the class and make your changes again'); + } + + if ($values['name'] != $class->getName()) { + $classByName = DataObject\ClassDefinition::getByName($values['name']); + if ($classByName && $classByName->getId() !== $class->getId()) { + throw new BadRequestHttpException('Class name already exists'); + } + + $values['name'] = $this->correctClassname($values['name']); + $class->rename($values['name']); + } + + if ($values['compositeIndices']) { + foreach ($values['compositeIndices'] as $index => $compositeIndex) { + if ($compositeIndex['index_key'] !== ($sanitizedKey = preg_replace('/[^a-za-z0-9_\-+]/', '', $compositeIndex['index_key']))) { + $values['compositeIndices'][$index]['index_key'] = $sanitizedKey; + } + } + } + + unset($values['creationDate'], $values['userOwner'], $values['layoutDefinitions'], $values['fieldDefinitions']); + + $configuration['datatype'] = 'layout'; + $configuration['fieldtype'] = 'panel'; + $configuration['name'] = 'opendxp_root'; + + $class->setValues($values); + + $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); + $class->setLayoutDefinitions($layout); + $class->setUserModification($userId); + $class->setModificationDate(time()); + + $propertyVisibility = []; + foreach ($values as $key => $value) { + if (false !== stripos($key, 'propertyVisibility')) { + if (preg_match("/\.grid\./i", $key)) { + $propertyVisibility['grid'][preg_replace("/propertyVisibility\.grid\./i", '', $key)] = (bool) $value; + } elseif (preg_match("/\.search\./i", $key)) { + $propertyVisibility['search'][preg_replace("/propertyVisibility\.search\./i", '', $key)] = (bool) $value; + } + } + } + if (!empty($propertyVisibility)) { + $class->setPropertyVisibility($propertyVisibility); + } + + $class->save(); + + $class->setFieldDefinitions([]); + + return new SaveClassDefinitionResult(class: $class); + } + + private function correctClassname(string $name): string + { + $name = preg_replace('/[^a-zA-Z0-9_]+/', '', $name); + + return preg_replace('/^\d+/', '', $name); + } +} diff --git a/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionPayload.php b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionPayload.php new file mode 100644 index 00000000..088daeb7 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionPayload.php @@ -0,0 +1,26 @@ +request->getString('id'), + configuration: json_decode($request->request->getString('configuration'), true) ?? [], + values: json_decode($request->request->getString('values'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionResult.php b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionResult.php new file mode 100644 index 00000000..88781769 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionResult.php @@ -0,0 +1,27 @@ +task === 'add' && (new DataObject\SelectOptions\Config\Listing())->hasConfig($payload->id)) { + throw new BadRequestHttpException('Select options with the same ID already exists (lower/upper cases may be different)'); + } + + $selectOptionsConfiguration = DataObject\SelectOptions\Config::createFromData([ + DataObject\SelectOptions\Config::PROPERTY_ID => $payload->id, + DataObject\SelectOptions\Config::PROPERTY_GROUP => $payload->group, + DataObject\SelectOptions\Config::PROPERTY_USE_TRAITS => $payload->useTraits, + DataObject\SelectOptions\Config::PROPERTY_IMPLEMENTS_INTERFACES => $payload->implementsInterfaces, + DataObject\SelectOptions\Config::PROPERTY_SELECT_OPTIONS => $payload->selectOptionsData, + ]); + + $event = new GenericEvent(null, ['selectOptionsConfiguration' => $selectOptionsConfiguration]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_SELECTOPTIONS_UPDATE_CONFIGURATION); + /** @var DataObject\SelectOptions\Config $selectOptionsConfiguration */ + $selectOptionsConfiguration = $event->getArgument('selectOptionsConfiguration'); + + $selectOptionsConfiguration->save(); + + return new SaveSelectOptionsResult(id: $selectOptionsConfiguration->getId()); + } +} diff --git a/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsPayload.php b/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsPayload.php new file mode 100644 index 00000000..18481eed --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsPayload.php @@ -0,0 +1,35 @@ +request->getString(Config::PROPERTY_SELECT_OPTIONS); + + return new static( + id: $request->request->getString(Config::PROPERTY_ID), + task: $request->request->getString('task'), + group: $request->request->getString(Config::PROPERTY_GROUP) ?: null, + useTraits: $request->request->getString(Config::PROPERTY_USE_TRAITS), + implementsInterfaces: $request->request->getString(Config::PROPERTY_IMPLEMENTS_INTERFACES), + selectOptionsData: $rawSelectOptions !== '' ? json_decode($rawSelectOptions, true) : null, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsResult.php b/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsResult.php new file mode 100644 index 00000000..6e127d3a --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsResult.php @@ -0,0 +1,25 @@ +fetchOne('SELECT MAX(CAST(id AS SIGNED)) FROM classes'); + $existingIds = $db->fetchFirstColumn('SELECT LOWER(id) FROM classes'); + + return new SuggestClassIdentifierResult( + suggestedIdentifier: $maxId ? $maxId + 1 : 1, + existingIds: $existingIds, + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/SuggestClassIdentifier/SuggestClassIdentifierResult.php b/src/Handler/DataObject/ClassDef/SuggestClassIdentifier/SuggestClassIdentifierResult.php new file mode 100644 index 00000000..323b4a95 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SuggestClassIdentifier/SuggestClassIdentifierResult.php @@ -0,0 +1,26 @@ +ids; + $oid = $payload->oid; + $fieldname = $payload->fieldname; + $data = []; + + if (!$ids) { + return new AddCollectionsResult(data: $data); + } + + $db = Db::get(); + $mappedData = []; + $groupsData = $db->fetchAllAssociative( + 'SELECT * FROM classificationstore_groups g, classificationstore_collectionrelations c + WHERE colId IN (?) AND g.id = c.groupId', + [array_values(array_filter($ids, is_numeric(...)))], + [ArrayParameterType::INTEGER] + ); + + foreach ($groupsData as $groupData) { + $mappedData[$groupData['id']] = $groupData; + } + + $groupIdList = []; + $groupId = null; + $allowedGroupIds = null; + + $object = $oid ? DataObject\Concrete::getById($oid) : null; + if ($object) { + $class = $object->getClass(); + /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ + $fd = $class->getFieldDefinition($fieldname); + $allowedGroupIds = $fd->getAllowedGroupIds(); + } + + foreach ($groupsData as $groupItem) { + $groupId = $groupItem['groupId']; + if (!$allowedGroupIds || in_array($groupId, $allowedGroupIds)) { + $groupIdList[] = $groupId; + } + } + + if ($groupIdList) { + $groupList = new Classificationstore\GroupConfig\Listing(); + $groupCondition = 'id in (' . implode(',', $groupIdList) . ')'; + $groupList->setCondition($groupCondition); + $groupList = $groupList->load(); + + $keyCondition = 'groupId in (' . implode(',', $groupIdList) . ')'; + $keyList = new Classificationstore\KeyGroupRelation\Listing(); + $keyList->setCondition($keyCondition); + $keyList->setOrderKey(['sorter', 'id']); + $keyList->setOrder(['ASC', 'ASC']); + $keyList = $keyList->load(); + + foreach ($groupList as $groupData) { + $data[$groupData->getId()] = [ + 'name' => $groupData->getName(), + 'id' => $groupData->getId(), + 'description' => $groupData->getDescription(), + 'keys' => [], + 'sorter' => (int) $mappedData[$groupData->getId()]['sorter'], + 'collectionId' => $mappedData[$groupId]['colId'], + ]; + } + + foreach ($keyList as $keyData) { + $groupId = $keyData->getGroupId(); + + $keys = $data[$groupId]['keys']; + $type = $keyData->getType(); + $definition = json_decode($keyData->getDefinition(), true); + $definition = Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); + + if (method_exists($definition, '__wakeup')) { + $definition->__wakeup(); + } + + $context['object'] = $object; + $context['class'] = $object ? $object->getClass() : null; + $context['ownerType'] = 'classificationstore'; + $context['ownerName'] = $fieldname; + $context['keyId'] = $keyData->getKeyId(); + $context['groupId'] = $groupId; + $context['keyDefinition'] = $definition; + + if ($definition instanceof LayoutDefinitionEnrichmentInterface) { + $definition = $definition->enrichLayoutDefinition($object, $context); + } + + $keys[] = [ + 'name' => $keyData->getName(), + 'id' => $keyData->getKeyId(), + 'description' => $keyData->getDescription(), + 'definition' => $definition, + ]; + $data[$groupId]['keys'] = $keys; + } + } + + return new AddCollectionsResult(data: $data); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsPayload.php b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsPayload.php new file mode 100644 index 00000000..e618903f --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsPayload.php @@ -0,0 +1,26 @@ +request->getString('collectionIds'), true) ?: [], + oid: $request->request->getInt('oid'), + fieldname: $request->request->getString('fieldname'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsResult.php b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsResult.php new file mode 100644 index 00000000..234247dc --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsResult.php @@ -0,0 +1,25 @@ +ids; + $oid = $payload->oid; + $fieldname = $payload->fieldname; + $object = $oid ? DataObject\Concrete::getById($oid) : null; + + $placeholders = implode(',', array_fill(0, count($ids), '?')); + + $keyList = new Classificationstore\KeyGroupRelation\Listing(); + $keyList->setCondition('groupId in (' . $placeholders . ')', $ids); + $keyList->setOrderKey(['sorter', 'id']); + $keyList->setOrder(['ASC', 'ASC']); + $keyList = $keyList->load(); + + $groupList = new Classificationstore\GroupConfig\Listing(); + $groupList->setCondition('id in (' . $placeholders . ')', $ids); + $groupList->setOrder('ASC'); + $groupList->setOrderKey('id'); + $groupList = $groupList->load(); + + $data = []; + + foreach ($groupList as $groupData) { + $data[$groupData->getId()] = [ + 'name' => $groupData->getName(), + 'id' => $groupData->getId(), + 'description' => $groupData->getDescription(), + 'keys' => [], + ]; + } + + foreach ($keyList as $keyData) { + $groupId = $keyData->getGroupId(); + + $keys = $data[$groupId]['keys']; + $type = $keyData->getType(); + $definition = json_decode($keyData->getDefinition(), true); + $definition = Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); + + if (method_exists($definition, '__wakeup')) { + $definition->__wakeup(); + } + + $context['object'] = $object; + $context['class'] = $object ? $object->getClass() : null; + $context['ownerType'] = 'classificationstore'; + $context['ownerName'] = $fieldname; + $context['keyId'] = $keyData->getKeyId(); + $context['groupId'] = $groupId; + $context['keyDefinition'] = $definition; + + if ($definition instanceof LayoutDefinitionEnrichmentInterface) { + $definition = $definition->enrichLayoutDefinition($object, $context); + } + + $keys[] = [ + 'name' => $keyData->getName(), + 'id' => $keyData->getKeyId(), + 'description' => $keyData->getDescription(), + 'definition' => $definition, + ]; + $data[$groupId]['keys'] = $keys; + } + + return new AddGroupsResult(data: $data); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsPayload.php b/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsPayload.php new file mode 100644 index 00000000..283af37c --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsPayload.php @@ -0,0 +1,26 @@ +request->getString('groupIds'), true) ?: [], + oid: $request->request->getInt('oid'), + fieldname: $request->request->getString('fieldname') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsResult.php b/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsResult.php new file mode 100644 index 00000000..d95661e2 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsResult.php @@ -0,0 +1,25 @@ + 'input', + 'name' => $payload->name, + 'title' => $payload->name, + 'datatype' => 'data', + ]; + + $config = new Classificationstore\KeyConfig(); + $config->setName($payload->name); + $config->setTitle($payload->name); + $config->setType('input'); + $config->setStoreId($payload->storeId); + $config->setEnabled(true); + $config->setDefinition(json_encode($definition)); + $config->save(); + + return new AddPropertyResult(name: $config->getName()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyPayload.php b/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyPayload.php new file mode 100644 index 00000000..64f96699 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyPayload.php @@ -0,0 +1,24 @@ +request->getString('name'), + storeId: $request->request->getInt('storeId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyResult.php b/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyResult.php new file mode 100644 index 00000000..e938bd65 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyResult.php @@ -0,0 +1,25 @@ +name, $payload->storeId); + + if (!$config) { + $config = new Classificationstore\CollectionConfig(); + $config->setName($payload->name); + $config->setStoreId($payload->storeId); + $config->save(); + } + + return new CreateCollectionResult(name: $config->getName()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionPayload.php b/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionPayload.php new file mode 100644 index 00000000..b6f73f01 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionPayload.php @@ -0,0 +1,25 @@ +request->getString('name')), + storeId: $request->request->getInt('storeId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionResult.php b/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionResult.php new file mode 100644 index 00000000..1eae6daf --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionResult.php @@ -0,0 +1,25 @@ +name, $payload->storeId); + + if (!$config) { + $config = new Classificationstore\GroupConfig(); + $config->setStoreId($payload->storeId); + $config->setName($payload->name); + $config->save(); + + return new CreateGroupResult(name: $config->getName(), alreadyExists: false); + } + + return new CreateGroupResult(name: $config->getName(), alreadyExists: true); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupPayload.php b/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupPayload.php new file mode 100644 index 00000000..1baff897 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupPayload.php @@ -0,0 +1,25 @@ +request->getString('name')), + storeId: $request->request->getInt('storeId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupResult.php b/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupResult.php new file mode 100644 index 00000000..a5ac70cc --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupResult.php @@ -0,0 +1,26 @@ +name); + + if (!$config) { + $config = new Classificationstore\StoreConfig(); + $config->setName($payload->name); + $config->save(); + } else { + throw new Exception('Store with the given name exists'); + } + + return new CreateStoreResult(storeId: $config->getId()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateStore/CreateStorePayload.php b/src/Handler/DataObject/Classificationstore/CreateStore/CreateStorePayload.php new file mode 100644 index 00000000..5e10c46c --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateStore/CreateStorePayload.php @@ -0,0 +1,23 @@ +request->getString('name')), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/CreateStore/CreateStoreResult.php b/src/Handler/DataObject/Classificationstore/CreateStore/CreateStoreResult.php new file mode 100644 index 00000000..224746e6 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateStore/CreateStoreResult.php @@ -0,0 +1,25 @@ +setCondition('colId = ?', $payload->id); + $list = $configRelations->load(); + foreach ($list as $item) { + $item->delete(); + } + + $config = Classificationstore\CollectionConfig::getById($payload->id); + $config->delete(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteCollection/DeleteCollectionPayload.php b/src/Handler/DataObject/Classificationstore/DeleteCollection/DeleteCollectionPayload.php new file mode 100644 index 00000000..ec0d3b52 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteCollection/DeleteCollectionPayload.php @@ -0,0 +1,22 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationHandler.php b/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationHandler.php new file mode 100644 index 00000000..79d88b5f --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationHandler.php @@ -0,0 +1,31 @@ +setColId($payload->colId); + $config->setGroupId($payload->groupId); + $config->delete(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationPayload.php b/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationPayload.php new file mode 100644 index 00000000..0bdcbc39 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteCollectionRelation/DeleteCollectionRelationPayload.php @@ -0,0 +1,24 @@ +request->getInt('colId'), + groupId: $request->request->getInt('groupId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupHandler.php b/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupHandler.php new file mode 100644 index 00000000..e48f6ccc --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupHandler.php @@ -0,0 +1,29 @@ +id); + $config->delete(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupPayload.php b/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupPayload.php new file mode 100644 index 00000000..75bb4336 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteGroup/DeleteGroupPayload.php @@ -0,0 +1,22 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyHandler.php b/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyHandler.php new file mode 100644 index 00000000..17150673 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyHandler.php @@ -0,0 +1,30 @@ +id); + $config->setEnabled(false); + $config->save(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyPayload.php b/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyPayload.php new file mode 100644 index 00000000..14a3feae --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteProperty/DeletePropertyPayload.php @@ -0,0 +1,22 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationHandler.php b/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationHandler.php new file mode 100644 index 00000000..e3e85a77 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationHandler.php @@ -0,0 +1,31 @@ +setKeyId($payload->keyId); + $config->setGroupId($payload->groupId); + $config->delete(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationPayload.php b/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationPayload.php new file mode 100644 index 00000000..ace7d6b9 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/DeleteRelation/DeleteRelationPayload.php @@ -0,0 +1,24 @@ +request->getInt('keyId'), + groupId: $request->request->getInt('groupId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/EditStore/EditStoreHandler.php b/src/Handler/DataObject/Classificationstore/EditStore/EditStoreHandler.php new file mode 100644 index 00000000..3cb08b92 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/EditStore/EditStoreHandler.php @@ -0,0 +1,51 @@ +id; + $name = $payload->name; + if (!$name) { + throw new Exception('Name must not be empty'); + } + + $config = Classificationstore\StoreConfig::getByName($name); + if ($config && $config->getId() != $id) { + throw new Exception('There is already a config with the same name'); + } + + $config = Classificationstore\StoreConfig::getById($id); + + if (!$config) { + throw new Exception('Configuration does not exist'); + } + + $config->setName($name); + $config->setDescription($payload->description); + $config->save(); + } +} diff --git a/src/Handler/DataObject/Classificationstore/EditStore/EditStorePayload.php b/src/Handler/DataObject/Classificationstore/EditStore/EditStorePayload.php new file mode 100644 index 00000000..436c097e --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/EditStore/EditStorePayload.php @@ -0,0 +1,28 @@ +request->getString('data'), true) ?? []; + + return new static( + id: $request->request->getInt('id'), + name: $data['name'] ?? '', + description: $data['description'] ?? null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsHandler.php b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsHandler.php new file mode 100644 index 00000000..52796c1f --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsHandler.php @@ -0,0 +1,103 @@ + 'name', 'groupDescription' => 'description']; + + $orderKey = 'sorter'; + $order = 'ASC'; + + if ($payload->dir !== null) { + $order = $payload->dir; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $sortingSettings['orderKey']; + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $list = new Classificationstore\CollectionGroupRelation\Listing(); + + if ($payload->limit > 0) { + $list->setLimit($payload->limit); + } + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($mapping[$orderKey] ?? $orderKey); + $condition = ''; + + if ($payload->filter !== null) { + $db = Db::get(); + $filters = json_decode($payload->filter); + + $count = 0; + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + if ($count > 0) { + $condition .= ' AND '; + } + $count++; + $fieldname = $mapping[$f->field]; + $condition .= $db->quoteIdentifier($fieldname) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + + if ($condition) { + $condition = '( ' . $condition . ' ) AND'; + } + $condition .= ' colId = ' . $list->quote($payload->colId); + + $list->setCondition($condition); + + $listItems = $list->load(); + + $data = []; + foreach ($listItems as $config) { + $item = [ + 'colId' => $config->getColId(), + 'groupId' => $config->getGroupId(), + 'groupName' => $config->getName(), + 'groupDescription' => $config->getDescription(), + 'id' => $config->getColId() . '-' . $config->getGroupId(), + 'sorter' => $config->getSorter(), + ]; + $data[] = $item; + } + + return new GetCollectionRelationsResult(data: $data, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsPayload.php b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsPayload.php new file mode 100644 index 00000000..25f8c619 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsPayload.php @@ -0,0 +1,36 @@ +query->getInt('limit'); + + return new static( + queryAll: $request->query->all(), + limit: $rawLimit ?: 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + filter: $request->query->getString('filter') ?: null, + colId: $request->query->getInt('colId'), + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsResult.php b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsResult.php new file mode 100644 index 00000000..562039fc --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsResult.php @@ -0,0 +1,26 @@ +dir !== null) { + $order = $payload->dir; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $sortingSettings['orderKey']; + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $storeIdFromDefinition = 0; + $allowedCollectionIds = []; + if ($payload->oid) { + $object = DataObject\Concrete::getById($payload->oid); + $class = $object->getClass(); + /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ + $fd = $class->getFieldDefinition($payload->fieldname); + $allowedGroupIds = $fd->getAllowedGroupIds(); + + if ($allowedGroupIds) { + $db = Db::get(); + $relationList = $db->fetchAllAssociative( + 'SELECT * FROM classificationstore_collectionrelations WHERE groupId IN (?)', + [$allowedGroupIds], + [ArrayParameterType::INTEGER] + ); + + foreach ($relationList as $item) { + $allowedCollectionIds[] = $item['colId']; + } + } + + $storeIdFromDefinition = $fd->getStoreId(); + } + + $list = new Classificationstore\CollectionConfig\Listing(); + + $list->setLimit($payload->limit); + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($orderKey); + + $conditionParts = []; + $db = Db::get(); + + if ($payload->searchfilter) { + $searchFilterConditions = []; + + $searchTerms = [$payload->searchfilter, ...$this->searchTermResolver->resolve($payload->searchfilter)]; + foreach ($searchTerms as $searchFilterTerm) { + $searchFilterConditions[] = 'name LIKE ' . $db->quote('%' . $searchFilterTerm . '%') . ' OR description LIKE ' . $db->quote('%' . $searchFilterTerm . '%'); + } + + $conditionParts[] = '(' . implode(' OR ', $searchFilterConditions) . ')'; + } + + $storeId = $payload->storeId ?: $storeIdFromDefinition; + + $conditionParts[] = ' (storeId = ' . $db->quote($storeId) . ')'; + + if ($payload->filter !== null) { + $filters = json_decode($payload->filter); + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + + if ($allowedCollectionIds) { + $conditionParts[] = ' id in (' . implode(',', $allowedCollectionIds) . ')'; + } + + $condition = implode(' AND ', $conditionParts); + + $list->setCondition($condition); + + $list->load(); + $configList = $list->getList(); + + $data = []; + foreach ($configList as $config) { + $name = $config->getName(); + if (!$name) { + $name = 'EMPTY'; + } + $item = [ + 'storeId' => $config->getStoreId(), + 'id' => $config->getId(), + 'name' => $name, + 'description' => $config->getDescription(), + ]; + if ($config->getCreationDate()) { + $item['creationDate'] = $config->getCreationDate(); + } + + if ($config->getModificationDate()) { + $item['modificationDate'] = $config->getModificationDate(); + } + + $data[] = $item; + } + + return new GetCollectionsResult(data: $data, total: $list->getTotalCount()); + } + +} diff --git a/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsPayload.php b/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsPayload.php new file mode 100644 index 00000000..143f8e7a --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsPayload.php @@ -0,0 +1,42 @@ +query->get('limit'); + + return new static( + queryAll: $request->query->all(), + limit: $rawLimit !== null ? (int) $rawLimit : 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + oid: ($v = $request->query->get('oid')) !== null && is_numeric($v) ? (int) $v : null, + fieldname: $request->query->getString('fieldname') ?: null, + searchfilter: $request->query->getString('searchfilter') ?: null, + storeId: ($v = $request->query->get('storeId')) !== null && is_numeric($v) ? (int) $v : null, + filter: $request->query->getString('filter') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsResult.php b/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsResult.php new file mode 100644 index 00000000..dac28d23 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsResult.php @@ -0,0 +1,26 @@ +dir !== null) { + $order = $payload->dir; + } + + if ($payload->sort !== null) { + $orderKey = $payload->sort; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $sortingSettings['orderKey']; + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $list = new Classificationstore\GroupConfig\Listing(); + + $list->setLimit($payload->limit); + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($orderKey); + + $conditionParts = []; + $db = Db::get(); + + if ($payload->searchfilter !== null) { + $searchFilterConditions = []; + + $searchTerms = [$payload->searchfilter, ...$this->searchTermResolver->resolve($payload->searchfilter)]; + foreach ($searchTerms as $searchFilterTerm) { + $searchFilterConditions[] = 'name LIKE ' . $db->quote('%' . $searchFilterTerm . '%') . ' OR description LIKE ' . $db->quote('%' . $searchFilterTerm . '%'); + } + + $conditionParts[] = '(' . implode(' OR ', $searchFilterConditions) . ')'; + } + + if ($payload->storeId) { + $conditionParts[] = '(storeId = ' . $db->quote($payload->storeId) . ')'; + } + + if ($payload->filter !== null) { + $filters = json_decode($payload->filter); + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + + if ($payload->oid !== null) { + $object = DataObject\Concrete::getById($payload->oid); + $class = $object->getClass(); + /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ + $fd = $class->getFieldDefinition($payload->fieldname); + $allowedGroupIds = $fd->getAllowedGroupIds(); + + if ($allowedGroupIds) { + $conditionParts[] = 'ID in (' . implode(',', $allowedGroupIds) . ')'; + } + } + + $condition = implode(' AND ', $conditionParts); + $list->setCondition($condition); + + $list->load(); + $configList = $list->getList(); + + $data = []; + foreach ($configList as $config) { + $name = $config->getName(); + if (!$name) { + $name = 'EMPTY'; + } + $item = [ + 'storeId' => $config->getStoreId(), + 'id' => $config->getId(), + 'name' => $name, + 'description' => $config->getDescription(), + ]; + if ($config->getCreationDate()) { + $item['creationDate'] = $config->getCreationDate(); + } + + if ($config->getModificationDate()) { + $item['modificationDate'] = $config->getModificationDate(); + } + + $data[] = $item; + } + + return new GetGroupsResult(data: $data, total: $list->getTotalCount()); + } + +} diff --git a/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsPayload.php b/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsPayload.php new file mode 100644 index 00000000..21aebf73 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsPayload.php @@ -0,0 +1,44 @@ +query->getInt('limit'); + + return new static( + queryAll: $request->query->all(), + limit: $rawLimit ?: 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + sort: $request->query->getString('sort') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + searchfilter: $request->query->getString('searchfilter') ?: null, + storeId: $request->query->getInt('storeId'), + filter: $request->query->getString('filter') ?: null, + oid: ($v = $request->query->get('oid')) !== null && is_numeric($v) ? (int) $v : null, + fieldname: $request->query->getString('fieldname') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsResult.php b/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsResult.php new file mode 100644 index 00000000..1c641f0a --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsResult.php @@ -0,0 +1,26 @@ +table; + if (!ArrayHelper::inArrayCaseInsensitive($tableSuffix, ['keys', 'groups'])) { + $tableSuffix = 'keys'; + } + + $table = 'classificationstore_' . $tableSuffix; + $db = Db::get(); + + $sortKey = $payload->sortKey; + $sortDir = $payload->sortDir; + + if (!$sortKey) { + $sortKey = 'name'; + $sortDir = 'ASC'; + } + + if (!ArrayHelper::inArrayCaseInsensitive($sortDir, ['DESC', 'ASC'])) { + $sortDir = 'DESC'; + } + + if (!ArrayHelper::inArrayCaseInsensitive($sortKey, ['name', 'title', 'description', 'id', 'type', 'creationDate', 'modificationDate', 'enabled', 'parentId', 'storeId'])) { + $sortKey = 'name'; + } + + $sorter = ' order by `' . $sortKey . '` ' . $sortDir; + + if ($table === 'keys') { + $query = ' + select *, (item.pos - 1)/ ' . $payload->pageSize . ' + 1 as page from ( + select * from ( + select @rownum := @rownum + 1 as pos, id, name, `type` + from `' . $table . '` + where enabled = 1 and storeId = ' . $payload->storeId . $sorter . ' + ) all_rows) item where id = ' . $payload->id . ';'; + } else { + $query = ' + select *, (item.pos - 1)/ ' . $payload->pageSize . ' + 1 as page from ( + select * from ( + select @rownum := @rownum + 1 as pos, id, name + from `' . $table . '` + where storeId = ' . $payload->storeId . $sorter . ' + ) all_rows) item where id = ' . $payload->id . ';'; + } + + $db->executeStatement('SET @rownum = 0'); + $result = $db->fetchAllAssociative($query); + + $page = (int) $result[0]['page']; + + return new GetPageResult(page: $page); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetPage/GetPagePayload.php b/src/Handler/DataObject/Classificationstore/GetPage/GetPagePayload.php new file mode 100644 index 00000000..9d662902 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetPage/GetPagePayload.php @@ -0,0 +1,32 @@ +query->getString('table') ?: null, + id: $request->query->getInt('id'), + storeId: $request->query->getInt('storeId'), + pageSize: $request->query->getInt('pageSize'), + sortKey: $request->query->getString('sortKey') ?: null, + sortDir: $request->query->getString('sortDir') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetPage/GetPageResult.php b/src/Handler/DataObject/Classificationstore/GetPage/GetPageResult.php new file mode 100644 index 00000000..0fe4dc24 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetPage/GetPageResult.php @@ -0,0 +1,25 @@ +frameName) { + $keyCriteria = ' FALSE '; + $frameConfig = Classificationstore\CollectionConfig::getByName($payload->frameName, $payload->storeId); + if ($frameConfig) { + // get all keys within that collection / frame + $frameId = $frameConfig->getId(); + $groupList = new Classificationstore\CollectionGroupRelation\Listing(); + $groupList->setCondition('colId = ' . $db->quote($frameId)); + $groupList = $groupList->load(); + $groupIdList = []; + foreach ($groupList as $groupEntry) { + $groupIdList[] = $groupEntry->getGroupId(); + } + + if ($groupIdList) { + $keyIdList = new Classificationstore\KeyGroupRelation\Listing(); + $keyIdList->setCondition('groupId in (' . implode(',', $groupIdList) . ')'); + $keyIdList = $keyIdList->load(); + if ($keyIdList) { + $keyIdValues = []; + foreach ($keyIdList as $keyEntry) { + $keyIdValues[] = $keyEntry->getKeyId(); + } + + $keyCriteria = ' id in (' . implode(',', $keyIdValues) . ')'; + } + } + } + + $conditionParts[] = $keyCriteria; + } + + $orderKey = 'name'; + $order = 'ASC'; + + if ($payload->dir !== null) { + $order = $payload->dir; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $sortingSettings['orderKey']; + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $list = new Classificationstore\KeyConfig\Listing(); + + if ($payload->limit > 0 && !$payload->groupIds && !$payload->keyIds) { + $list->setLimit($payload->limit); + } + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($orderKey); + + if ($payload->searchfilter) { + $conditionParts[] = '(name LIKE ' . $db->quote('%' . $payload->searchfilter . '%') . ' OR description LIKE ' . $db->quote('%' . $payload->searchfilter . '%') . ')'; + } + + if ($payload->storeId) { + $conditionParts[] = '(storeId = ' . $db->quote($payload->storeId) . ')'; + } + + if ($payload->filter !== null) { + $filters = json_decode($payload->filter); + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + $conditionParts[] = $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + $condition = implode(' AND ', $conditionParts); + $list->setCondition($condition); + + if ($payload->groupIds || $payload->keyIds) { + if ($payload->groupIds) { + $ids = json_decode($payload->groupIds, true); + $col = 'group'; + } else { + $ids = json_decode($payload->keyIds, true); + $col = 'id'; + } + + $condition = $db->quoteIdentifier($col) . ' IN ('; + $count = 0; + foreach ($ids as $theId) { + if ($count > 0) { + $condition .= ','; + } + $condition .= $theId; + $count++; + } + + $condition .= ')'; + $list->setCondition($condition); + } + + $list->load(); + $configList = $list->getList(); + + $data = []; + foreach ($configList as $config) { + $data[] = self::buildKeyConfigItem($config); + } + + return new GetPropertiesResult(data: $data, total: $list->getTotalCount()); + } + + public static function buildKeyConfigItem(Classificationstore\KeyConfig $config): array + { + $name = $config->getName(); + + $item = [ + 'storeId' => $config->getStoreId(), + 'id' => $config->getId(), + 'name' => $name, + 'description' => $config->getDescription(), + ]; + + if ($config->getCreationDate()) { + $item['creationDate'] = $config->getCreationDate(); + } + + if ($config->getModificationDate()) { + $item['modificationDate'] = $config->getModificationDate(); + } + + $item['type'] = $config->getType() ?: 'input'; + $definition = $config->getDefinition(); + $item['definition'] = $definition; + + if ($definition) { + $definition = json_decode($definition, true); + if ($definition) { + $item['title'] = $definition['title']; + } + } + + return $item; + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesPayload.php b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesPayload.php new file mode 100644 index 00000000..1fafc0c3 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesPayload.php @@ -0,0 +1,44 @@ +query->getInt('limit'); + + return new static( + queryAll: $request->query->all(), + storeId: $request->query->getInt('storeId'), + frameName: $request->query->getString('frameName') ?: null, + limit: $rawLimit ?: 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + groupIds: $request->query->getString('groupIds') ?: null, + keyIds: $request->query->getString('keyIds') ?: null, + searchfilter: $request->query->getString('searchfilter') ?: null, + filter: $request->query->getString('filter') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesResult.php b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesResult.php new file mode 100644 index 00000000..2b8f495e --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesResult.php @@ -0,0 +1,26 @@ + 'name', 'keyDescription' => 'description']; + + $orderKey = 'name'; + $order = 'ASC'; + + $relationIdList = $payload->relationIds ? json_decode($payload->relationIds, true) : null; + + if ($payload->dir !== null) { + $order = $payload->dir; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $mapping[$sortingSettings['orderKey']] ?? $sortingSettings['orderKey']; + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $limit = $payload->limit; + if ($limit === 0 && is_array($relationIdList)) { + $limit = count($relationIdList); + } + + $list = new Classificationstore\KeyGroupRelation\Listing(); + + if ($limit > 0) { + $list->setLimit($limit); + } + + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($orderKey); + $conditionParts = []; + + if ($payload->filter !== null) { + $db = Db::get(); + $filters = json_decode($payload->filter); + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + $fieldname = $mapping[$f->field]; + $conditionParts[] = $db->quoteIdentifier($fieldname) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + + if ($relationIdList === null) { + $conditionParts[] = ' groupId = ' . $list->quote($payload->groupId); + } + + if ($relationIdList) { + $relationParts = []; + + foreach ($relationIdList as $relationId) { + $keyId = $relationId['keyId']; + $entryGroupId = $relationId['groupId']; + $relationParts[] = '(keyId = ' . $list->quote($keyId) . ' AND groupId = ' . $list->quote($entryGroupId) . ')'; + } + + $conditionParts[] = '(' . implode(' OR ', $relationParts) . ')'; + } + + $condition = implode(' AND ', $conditionParts); + + $list->setCondition($condition); + + $listItems = $list->load(); + + $data = []; + foreach ($listItems as $config) { + $type = $config->getType(); + $definition = json_decode($config->getDefinition(), true); + $definition = Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); + DataObject\Service::enrichLayoutDefinition($definition); + + $item = [ + 'keyId' => $config->getKeyId(), + 'groupId' => $config->getGroupId(), + 'keyName' => $config->getName(), + 'keyDescription' => $config->getDescription(), + 'id' => $config->getGroupId() . '-' . $config->getKeyId(), + 'sorter' => $config->getSorter(), + 'layout' => $definition, + 'mandatory' => $config->isMandatory(), + ]; + + $data[] = $item; + } + + return new GetRelationsResult(data: $data, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsPayload.php b/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsPayload.php new file mode 100644 index 00000000..49c4253e --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsPayload.php @@ -0,0 +1,38 @@ +query->getInt('limit'); + + return new static( + queryAll: $request->query->all(), + relationIds: $request->query->getString('relationIds') ?: null, + limit: $rawLimit ?: 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + filter: $request->query->getString('filter') ?: null, + groupId: $request->query->getString('groupId') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsResult.php b/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsResult.php new file mode 100644 index 00000000..3565152d --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsResult.php @@ -0,0 +1,26 @@ +load(); + foreach ($list as $item) { + $resultItem = [ + 'id' => $item->getId(), + 'text' => htmlspecialchars($item->getName() ?? '', ENT_QUOTES), + 'expandable' => false, + 'leaf' => true, + 'expanded' => true, + 'description' => htmlspecialchars($item->getDescription() ?? '', ENT_QUOTES), + 'iconCls' => 'opendxp_icon_classificationstore', + ]; + + $resultItem['qtitle'] = 'ID: ' . $item->getId(); + $resultItem['qtip'] = $item->getDescription() ? htmlspecialchars($item->getDescription(), ENT_QUOTES) : ' '; + $result[] = $resultItem; + } + + return new GetStoreTreeResult(items: $result); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetStoreTree/GetStoreTreeResult.php b/src/Handler/DataObject/Classificationstore/GetStoreTree/GetStoreTreeResult.php new file mode 100644 index 00000000..e9eb1530 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetStoreTree/GetStoreTreeResult.php @@ -0,0 +1,25 @@ +load(); + + foreach ($storeConfigListing as $storeConfig) { + $storeConfigs[] = $storeConfig->getObjectVars(); + } + + return new ListStoresResult(storeConfigs: $storeConfigs); + } +} diff --git a/src/Handler/DataObject/Classificationstore/ListStores/ListStoresResult.php b/src/Handler/DataObject/Classificationstore/ListStores/ListStoresResult.php new file mode 100644 index 00000000..e39512e1 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/ListStores/ListStoresResult.php @@ -0,0 +1,25 @@ +data; + if (count($data) === count($data, 1)) { + $data = [$data]; + } + + foreach ($data as &$row) { + $colId = $row['colId']; + $groupId = $row['groupId']; + $sorter = $row['sorter']; + + $config = new Classificationstore\CollectionGroupRelation(); + $config->setGroupId($groupId); + $config->setColId($colId); + $config->setSorter((int) $sorter); + + $config->save(); + + $row['id'] = $config->getColId() . '-' . $config->getGroupId(); + } + + return new SaveCollectionRelationsResult(data: $data); + } +} diff --git a/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsPayload.php b/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsPayload.php new file mode 100644 index 00000000..48170967 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsPayload.php @@ -0,0 +1,24 @@ +request->has('data'), + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsResult.php b/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsResult.php new file mode 100644 index 00000000..4ede0332 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsResult.php @@ -0,0 +1,25 @@ +data; + $keyId = $data['keyId']; + $groupId = $data['groupId']; + $sorter = $data['sorter']; + $mandatory = $data['mandatory']; + + $config = new Classificationstore\KeyGroupRelation(); + $config->setGroupId((int) $groupId); + $config->setKeyId((int) $keyId); + $config->setSorter($sorter); + $config->setMandatory($mandatory); + + $config->save(); + $data['id'] = $config->getGroupId() . '-' . $config->getKeyId(); + + return new SaveRelationResult(data: $data); + } +} diff --git a/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationPayload.php b/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationPayload.php new file mode 100644 index 00000000..61298d23 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationPayload.php @@ -0,0 +1,24 @@ +request->has('data'), + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationResult.php b/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationResult.php new file mode 100644 index 00000000..cce70f5f --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationResult.php @@ -0,0 +1,25 @@ + Classificationstore\GroupConfig\Dao::TABLE_NAME_GROUPS . '.name', + 'keyName' => Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS . '.name', + 'keyDescription' => Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS . '.description', + ]; + + $orderKey = 'name'; + $order = 'ASC'; + + if ($payload->dir) { + $order = $payload->dir; + } + + $sortingSettings = QueryParams::extractSortingSettings($payload->queryAll); + if ($sortingSettings['orderKey'] && $sortingSettings['order']) { + $orderKey = $sortingSettings['orderKey']; + if ($orderKey === 'keyName') { + $orderKey = 'name'; + } + $order = $sortingSettings['order']; + } + + if ($payload->overrideSort) { + $orderKey = 'id'; + $order = 'DESC'; + } + + $list = new Classificationstore\KeyGroupRelation\Listing(); + + if ($payload->limit > 0) { + $list->setLimit($payload->limit); + } + $list->setOffset($payload->start); + $list->setOrder($order); + $list->setOrderKey($orderKey); + + $conditionParts = []; + + if ($payload->filter !== null) { + $filters = json_decode($payload->filter); + /** @var stdClass $f */ + foreach ($filters as $f) { + if (!isset($f->value)) { + continue; + } + + $fieldname = $mapping[$f->property]; + $conditionParts[] = $fieldname . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } + } + + $conditionParts[] = ' groupId IN (select id from classificationstore_groups where storeId = ' . $db->quote($payload->storeId) . ')'; + + if ($payload->searchfilter) { + $searchFilterConditions = []; + + $searchTerms = [$payload->searchfilter, ...$this->searchTermResolver->resolve($payload->searchfilter)]; + foreach ($searchTerms as $searchFilterTerm) { + $searchFilterConditions[] = Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS . '.name LIKE ' . $db->quote('%' . $searchFilterTerm . '%') + . ' OR ' . Classificationstore\GroupConfig\Dao::TABLE_NAME_GROUPS . '.name LIKE ' . $db->quote('%' . $searchFilterTerm . '%') + . ' OR ' . Classificationstore\KeyConfig\Dao::TABLE_NAME_KEYS . '.description LIKE ' . $db->quote('%' . $searchFilterTerm . '%'); + } + + $conditionParts[] = '(' . implode(' OR ', $searchFilterConditions) . ')'; + } + + $condition = implode(' AND ', $conditionParts); + $list->setCondition($condition); + $list->setResolveGroupName(true); + + $data = []; + foreach ($list->getList() as $config) { + $item = [ + 'keyId' => $config->getKeyId(), + 'groupId' => $config->getGroupId(), + 'keyName' => $config->getName(), + 'keyDescription' => $config->getDescription(), + 'id' => $config->getGroupId() . '-' . $config->getKeyId(), + 'sorter' => $config->getSorter(), + ]; + + $groupConfig = Classificationstore\GroupConfig::getById($config->getGroupId()); + if ($groupConfig) { + $item['groupName'] = $groupConfig->getName(); + } + + $data[] = $item; + } + + return new SearchRelationsResult(data: $data, total: $list->getTotalCount()); + } + +} diff --git a/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsPayload.php b/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsPayload.php new file mode 100644 index 00000000..d2d919d2 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsPayload.php @@ -0,0 +1,38 @@ +query->getInt('limit'); + + return new static( + queryAll: $request->query->all(), + storeId: ($v = $request->query->get('storeId')) !== null && is_numeric($v) ? (int) $v : null, + limit: $rawLimit ?: 15, + start: $request->query->getInt('start'), + dir: $request->query->getString('dir') ?: null, + overrideSort: (bool) $request->query->get('overrideSort'), + filter: $request->query->getString('filter') ?: null, + searchfilter: $request->query->getString('searchfilter') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsResult.php b/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsResult.php new file mode 100644 index 00000000..71a30ad6 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsResult.php @@ -0,0 +1,26 @@ +data; + $id = $data['id']; + $config = Classificationstore\CollectionConfig::getById($id); + + foreach ($data as $key => $value) { + if ($key !== 'id') { + $setter = 'set' . $key; + $config->$setter($value); + } + } + + $config->save(); + + $name = $config->getName(); + $item = [ + 'storeId' => $config->getStoreId(), + 'id' => $config->getId(), + 'name' => $name, + 'description' => $config->getDescription(), + ]; + + if ($config->getCreationDate()) { + $item['creationDate'] = $config->getCreationDate(); + } + + if ($config->getModificationDate()) { + $item['modificationDate'] = $config->getModificationDate(); + } + + return new UpdateCollectionResult(item: $item); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionPayload.php b/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionPayload.php new file mode 100644 index 00000000..01fb85af --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionPayload.php @@ -0,0 +1,24 @@ +request->has('data'), + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionResult.php b/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionResult.php new file mode 100644 index 00000000..b2926e4c --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionResult.php @@ -0,0 +1,25 @@ +data; + $id = $data['id']; + $config = Classificationstore\GroupConfig::getById($id); + + foreach ($data as $key => $value) { + if ($key !== 'id') { + $setter = 'set' . $key; + $config->$setter($value); + } + } + + $config->save(); + + $name = $config->getName(); + $item = [ + 'storeId' => $config->getStoreId(), + 'id' => $config->getId(), + 'name' => $name, + 'description' => $config->getDescription(), + ]; + + if ($config->getCreationDate()) { + $item['creationDate'] = $config->getCreationDate(); + } + + if ($config->getModificationDate()) { + $item['modificationDate'] = $config->getModificationDate(); + } + + return new UpdateGroupResult(item: $item); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupPayload.php b/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupPayload.php new file mode 100644 index 00000000..e661f96a --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupPayload.php @@ -0,0 +1,24 @@ +request->has('data'), + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupResult.php b/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupResult.php new file mode 100644 index 00000000..6a9cde08 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupResult.php @@ -0,0 +1,25 @@ +data; + $id = $data['id']; + $config = Classificationstore\KeyConfig::getById($id); + + foreach ($data as $key => $value) { + if ($key !== 'id') { + $setter = 'set' . $key; + if (method_exists($config, $setter)) { + $config->$setter($value); + } + } + } + + $config->save(); + $item = GetPropertiesHandler::buildKeyConfigItem($config); + + return new UpdatePropertyResult(item: $item); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyPayload.php b/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyPayload.php new file mode 100644 index 00000000..5b13be78 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyPayload.php @@ -0,0 +1,24 @@ +request->has('data'), + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyResult.php b/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyResult.php new file mode 100644 index 00000000..e20943b1 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $source = DataObject::getById($payload->sourceId); + + if ($payload->sourceParentId !== null && $payload->targetParentId !== null) { + $sourceParent = DataObject::getById($payload->sourceParentId) ?? throw new NotFoundHttpException('Source parent not found'); + $resolvedTargetParentId = $payload->sessionParentId ?? $payload->targetParentId; + $targetParent = DataObject::getById($resolvedTargetParentId) ?? throw new NotFoundHttpException('Target parent not found'); + $targetPath = preg_replace('@^' . preg_quote($sourceParent->getRealFullPath(), '@') . '@', $targetParent . '/', $source->getRealPath()); + $target = DataObject::getByPath($targetPath); + } else { + $target = DataObject::getById($payload->targetId); + } + + if (!$target instanceof DataObject) { + throw new NotFoundHttpException('Target not found'); + } + + $hasClassPermission = !($source instanceof DataObject\Concrete) || $adminUser->isAllowed($source->getClassId(), 'class'); + if (!$target->isAllowed('create') || !$hasClassPermission) { + throw new AccessDeniedHttpException(); + } + + $source = DataObject::getById($payload->sourceId); + if (!$source instanceof DataObject) { + throw new NotFoundHttpException("Source object not found: {$payload->sourceId}"); + } + + if ($source instanceof DataObject\Concrete && $latestVersion = $source->getLatestVersion()) { + $source = $latestVersion->loadData(); + $source->setPublished(false); + } + + $objectService = $this->serviceFactory->createDataObjectService(); + + if ($payload->type === 'child') { + $newObject = $objectService->copyAsChild($target, $source); + + return new CopyDataObjectResult($payload->sourceId, $newObject); + } + + if ($payload->type === 'replace') { + $concreteTarget = DataObject\Concrete::getById($target->getId()); + $concreteSource = DataObject\Concrete::getById($source->getId()); + $objectService->copyContents($concreteTarget, $concreteSource); + } + + return new CopyDataObjectResult($payload->sourceId); + } +} diff --git a/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectPayload.php b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectPayload.php new file mode 100644 index 00000000..576444dd --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectPayload.php @@ -0,0 +1,55 @@ +request->getString('transactionId'); + $sessionBag = Session::getSessionBag($request->getSession(), 'opendxp_copy')->get($transactionId) ?? []; + $hasTargetParentId = (bool) $request->request->getString('targetParentId'); + + return new static( + sourceId: $request->request->getInt('sourceId'), + targetId: $request->request->getInt('targetId'), + type: $request->request->getString('type'), + sourceParentId: $hasTargetParentId ? $request->request->getInt('sourceParentId') : null, + targetParentId: $hasTargetParentId ? $request->request->getInt('targetParentId') : null, + sessionParentId: !empty($sessionBag['parentId']) ? (int) $sessionBag['parentId'] : null, + transactionId: $transactionId, + saveParentId: (bool) $request->request->getString('saveParentId'), + sessionBag: $sessionBag, + ); + } +} diff --git a/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php new file mode 100644 index 00000000..98d77270 --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php @@ -0,0 +1,28 @@ +type === 'recursive' || $payload->type === 'recursive-update-references') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_dataobject_dataobject_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => 'child', + 'transactionId' => $transactionId, + 'saveParentId' => true, + ], + ]]; + + $object = DataObject::getById($payload->sourceId) ?? throw new DataObjectNotFoundException($payload->sourceId); + $childIds = []; + + if ($object->hasChildren(DataObject::$types)) { + $list = new DataObject\Listing(); + $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($object->getRealFullPath()) . '/%')); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('ASC'); + $list->setObjectTypes(DataObject::$types); + $childIds = $list->loadIdList(); + } + + foreach ($childIds as $id) { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_dataobject_dataobject_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $id, + 'targetParentId' => $payload->targetId, + 'sourceParentId' => $payload->sourceId, + 'type' => 'child', + 'transactionId' => $transactionId, + ], + ]]; + } + + if ($payload->type === 'recursive-update-references' && count($childIds) > 0) { + for ($i = 0; $i < (count($childIds) + 1); $i++) { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_dataobject_dataobject_copyrewriteids'), + 'method' => 'PUT', + 'params' => [ + 'transactionId' => $transactionId, + '_dc' => uniqid('', false), + ], + ]]; + } + } + } elseif ($payload->type === 'child' || $payload->type === 'replace') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_dataobject_dataobject_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => $payload->type, + 'transactionId' => $transactionId, + ], + ]]; + } + + return new CopyInfoResult($transactionId, $pasteJobs); + } +} diff --git a/src/Handler/DataObject/Copy/CopyInfo/CopyInfoPayload.php b/src/Handler/DataObject/Copy/CopyInfo/CopyInfoPayload.php new file mode 100644 index 00000000..e7c9c828 --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyInfo/CopyInfoPayload.php @@ -0,0 +1,39 @@ +query->getString('type') ?: null, + sourceId: $request->query->getInt('sourceId'), + targetId: $request->query->has('targetId') ? $request->query->getInt('targetId') : null, + ); + } +} diff --git a/src/Handler/DataObject/Copy/CopyInfo/CopyInfoResult.php b/src/Handler/DataObject/Copy/CopyInfo/CopyInfoResult.php new file mode 100644 index 00000000..89cad16f --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $object = DataObject::getById($payload->objectId) ?? throw new DataObjectNotFoundException($payload->objectId); + + $object = DataObject\Service::rewriteIds($object, ['object' => $payload->idMapping]); + $object->setUserModification($userId); + $object->save(); + } +} diff --git a/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php new file mode 100644 index 00000000..999ec067 --- /dev/null +++ b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php @@ -0,0 +1,50 @@ +request->getString('transactionId'); + $idStore = Session::getSessionBag($request->getSession(), 'opendxp_copy')->get($transactionId) ?? []; + + if (!array_key_exists('rewrite-stack', $idStore)) { + $idStore['rewrite-stack'] = array_values($idStore['idMapping'] ?? []); + } + + $objectId = (int) array_shift($idStore['rewrite-stack']); + + return new static( + objectId: $objectId, + idMapping: $idStore['idMapping'] ?? [], + transactionId: $transactionId, + updatedIdStore: $idStore, + ); + } +} diff --git a/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutHandler.php new file mode 100644 index 00000000..528eab1d --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutHandler.php @@ -0,0 +1,59 @@ +layoutIdentifier; + $layoutName = $payload->layoutName; + $classId = $payload->classId; + + $userId = $this->userContext->getAdminUser()?->getId() ?? 0; + if (DataObject\ClassDefinition\CustomLayout::getById($layoutId)) { + throw new BadRequestHttpException('Custom Layout identifier already exists'); + } + + $customLayout = DataObject\ClassDefinition\CustomLayout::create([ + 'name' => $layoutName, + 'userOwner' => $userId, + 'classId' => $classId, + ]); + + $customLayout->setId($layoutId); + + if (!$customLayout->isWriteable()) { + throw new ConfigWriteException(); + } + + $customLayout->save(); + + return $customLayout; + } +} diff --git a/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutPayload.php new file mode 100644 index 00000000..260549b9 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutPayload.php @@ -0,0 +1,38 @@ +request->getString('layoutIdentifier'), + layoutName: $request->request->getString('layoutName'), + classId: $request->request->getString('classId'), + ); + } +} diff --git a/src/Handler/DataObject/CustomLayout/DeleteCustomLayout/DeleteCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/DeleteCustomLayout/DeleteCustomLayoutHandler.php new file mode 100644 index 00000000..b9061ff1 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/DeleteCustomLayout/DeleteCustomLayoutHandler.php @@ -0,0 +1,39 @@ +id; + $customLayouts = new DataObject\ClassDefinition\CustomLayout\Listing(); + $customLayouts->setFilter(function (DataObject\ClassDefinition\CustomLayout $layout) use ($id) { + $currentLayoutId = $layout->getId(); + + return $currentLayoutId === $id || str_starts_with($currentLayoutId, $id . '.brick.'); + }); + + foreach ($customLayouts->getLayoutDefinitions() as $customLayout) { + $customLayout->delete(); + } + } +} diff --git a/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutHandler.php new file mode 100644 index 00000000..40a035bb --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutHandler.php @@ -0,0 +1,44 @@ +id) { + $customLayout = DataObject\ClassDefinition\CustomLayout::getById($payload->id); + if ($customLayout) { + return new ExportCustomLayoutResult( + $customLayout->getName(), + DataObject\ClassDefinition\Service::generateCustomLayoutJson($customLayout), + ); + } + } + + $errorMessage = ': Custom Layout with id [ ' . $payload->id . ' not found. ]'; + Logger::error($errorMessage); + + throw new NotFoundHttpException($errorMessage); + } +} diff --git a/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutPayload.php new file mode 100644 index 00000000..b1f94de9 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutPayload.php @@ -0,0 +1,30 @@ +query->getString('id') ?: null); + } +} diff --git a/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutResult.php b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutResult.php new file mode 100644 index 00000000..f12fbce7 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutResult.php @@ -0,0 +1,26 @@ +setFilter(fn (DataObject\ClassDefinition\CustomLayout $layout) => !str_contains($layout->getId(), '.brick.')); + $customLayouts->setOrder(fn (DataObject\ClassDefinition\CustomLayout $a, DataObject\ClassDefinition\CustomLayout $b) => strcmp($a->getName(), $b->getName())); + + foreach ($customLayouts->load() as $layout) { + $mapping[$layout->getClassId()][] = $layout; + } + + $classList = new DataObject\ClassDefinition\Listing(); + $classList->setOrder('ASC'); + $classList->setOrderKey('name'); + + $layouts = []; + foreach ($classList->load() as $class) { + if (!isset($mapping[$class->getId()])) { + continue; + } + $layouts[] = [ + 'type' => 'main', + 'id' => $class->getId() . '_' . 0, + 'name' => $class->getName(), + ]; + foreach ($mapping[$class->getId()] as $layout) { + $layouts[] = [ + 'type' => 'custom', + 'id' => $class->getId() . '_' . $layout->getId(), + 'name' => $class->getName() . ' - ' . $layout->getName(), + ]; + } + } + + return new AllLayoutsResult($layouts); + } +} diff --git a/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutHandler.php new file mode 100644 index 00000000..ec91f4a9 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutHandler.php @@ -0,0 +1,62 @@ +userContext->getAdminUser()?->getId() ?? 0; + $customLayout = DataObject\ClassDefinition\CustomLayout::getById($payload->id); + + if (!$customLayout) { + $brickLayoutSeparator = strpos($payload->id, '.brick.'); + if ($brickLayoutSeparator !== false) { + $parentLayout = DataObject\ClassDefinition\CustomLayout::getById(substr($payload->id, 0, $brickLayoutSeparator)); + if ($parentLayout instanceof DataObject\ClassDefinition\CustomLayout) { + $customLayout = DataObject\ClassDefinition\CustomLayout::create([ + 'name' => $parentLayout->getName() . ' ' . substr($payload->id, $brickLayoutSeparator + strlen('.brick.')), + 'userOwner' => $userId, + 'classId' => $parentLayout->getClassId(), + ]); + $customLayout->setId($payload->id); + if (!$customLayout->isWriteable()) { + throw new ConfigWriteException(); + } + $customLayout->save(); + } + } + + if (!$customLayout) { + throw new NotFoundHttpException(); + } + } + + return new GetCustomLayoutResult($customLayout->getObjectVars(), $customLayout->isWriteable()); + } +} diff --git a/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutPayload.php new file mode 100644 index 00000000..a1f00b31 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutPayload.php @@ -0,0 +1,30 @@ +query->getString('id')); + } +} diff --git a/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutResult.php b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutResult.php new file mode 100644 index 00000000..8aad38de --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutResult.php @@ -0,0 +1,26 @@ +classId); + $list = new DataObject\ClassDefinition\CustomLayout\Listing(); + $list->setFilter( + fn (DataObject\ClassDefinition\CustomLayout $layout) => in_array($layout->getClassId(), $classIds) && !str_contains($layout->getId(), '.brick.') + ); + + $definitions = []; + foreach ($list->load() as $item) { + $definitions[] = [ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'default' => $item->getDefault(), + ]; + } + + return new CustomLayoutDefinitionsResult($definitions); + } +} diff --git a/src/Handler/DataObject/CustomLayout/GetCustomLayoutDefinitions/GetCustomLayoutDefinitionsPayload.php b/src/Handler/DataObject/CustomLayout/GetCustomLayoutDefinitions/GetCustomLayoutDefinitionsPayload.php new file mode 100644 index 00000000..0d9f81da --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayoutDefinitions/GetCustomLayoutDefinitionsPayload.php @@ -0,0 +1,30 @@ +query->getString('classId')); + } +} diff --git a/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutHandler.php new file mode 100644 index 00000000..4d06f80e --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutHandler.php @@ -0,0 +1,47 @@ +id); + if (!$customLayout) { + return; + } + + $importData = $payload->importData; + $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($importData['layoutDefinitions'], true); + $customLayout->setLayoutDefinitions($layout); + if (isset($importData['name'])) { + $customLayout->setName($importData['name']); + } + $customLayout->setDescription($importData['description']); + + if (!$customLayout->isWriteable()) { + throw new ConfigWriteException(); + } + + $customLayout->save(); + } +} diff --git a/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php new file mode 100644 index 00000000..bbefe81f --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php @@ -0,0 +1,44 @@ +files->get('Filedata'); + $importData = json_decode(file_get_contents($file->getPathname()), true) ?? []; + + return new static( + id: $request->query->getString('id') ?: null, + importData: $importData, + nameAlreadyInUse: isset($importData['name']) && DataObject\ClassDefinition\CustomLayout::getByName($importData['name']) instanceof DataObject\ClassDefinition\CustomLayout, + ); + } +} diff --git a/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutHandler.php new file mode 100644 index 00000000..73e89a4d --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutHandler.php @@ -0,0 +1,60 @@ +id; + $configuration = $payload->configuration; + $values = $payload->values; + + $customLayout = DataObject\ClassDefinition\CustomLayout::getById($id) + ?? throw new NotFoundHttpException(); + + $modificationDate = (int) $values['modificationDate']; + if ($modificationDate < $customLayout->getModificationDate()) { + throw new BadRequestHttpException('custom_layout_changed'); + } + + $configuration['datatype'] = 'layout'; + $configuration['fieldtype'] = 'panel'; + $configuration['name'] = 'opendxp_root'; + + $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); + $customLayout->setLayoutDefinitions($layout); + $customLayout->setName($values['name']); + $customLayout->setDescription($values['description']); + $customLayout->setDefault($values['default']); + + if (!$customLayout->isWriteable()) { + throw new ConfigWriteException(); + } + + $customLayout->save(); + + return $customLayout; + } +} diff --git a/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutPayload.php new file mode 100644 index 00000000..afa092dd --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutPayload.php @@ -0,0 +1,38 @@ +request->getString('id'), + configuration: json_decode($request->request->getString('configuration'), true) ?? [], + values: json_decode($request->request->getString('values'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierHandler.php b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierHandler.php new file mode 100644 index 00000000..a70f6454 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierHandler.php @@ -0,0 +1,40 @@ +classId); + + $existingIds = []; + $existingNames = []; + + foreach ((new DataObject\ClassDefinition\CustomLayout\Listing())->load() as $item) { + $existingIds[] = $item->getId(); + if ($item->getClassId() == $payload->classId) { + $existingNames[] = $item->getName(); + } + } + + return new SuggestCustomLayoutIdentifierResult($identifier !== null ? (string) $identifier : '', $existingIds, $existingNames); + } +} diff --git a/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierPayload.php b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierPayload.php new file mode 100644 index 00000000..4be04c02 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierPayload.php @@ -0,0 +1,30 @@ +query->getString('classId')); + } +} diff --git a/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierResult.php b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierResult.php new file mode 100644 index 00000000..e25704a6 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierResult.php @@ -0,0 +1,27 @@ +allParams; + $filterPrepareEvent = new GenericEvent($this, ['requestParams' => $allParams]); + $this->eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::OBJECT_LIST_BEFORE_FILTER_PREPARE); + $allParams = $filterPrepareEvent->getArgument('requestParams'); + + $requestedLanguage = $allParams['language'] ?? null; + if (!$requestedLanguage) { + $requestedLanguage = $payload->locale; + } + + return new DataObjectGridProxyResult( + data: $this->gridService->gridProxy($allParams, DataObject::OBJECT_TYPE_OBJECT, $requestedLanguage), + requestedLanguage: $requestedLanguage, + ); + } +} diff --git a/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyPayload.php b/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyPayload.php new file mode 100644 index 00000000..d7700d42 --- /dev/null +++ b/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyPayload.php @@ -0,0 +1,45 @@ +request->all(), ...$request->query->all()]; + + if (!empty($allParams['context'])) { + $allParams['context'] = json_decode($allParams['context'], true); + } else { + $allParams['context'] = []; + } + + return new static( + allParams: $allParams, + locale: $request->getLocale(), + ); + } +} diff --git a/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyResult.php b/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyResult.php new file mode 100644 index 00000000..f02472ca --- /dev/null +++ b/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyResult.php @@ -0,0 +1,26 @@ +type; + $id = $payload->id; + $amount = $payload->amount; + if ($type === 'children') { + $parentObject = DataObject::getById($id); + + $list = new DataObject\Listing(); + $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($parentObject->getRealFullPath()) . '/%')); + $list->setLimit($amount); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('DESC'); + + $deletedItems = []; + foreach ($list as $object) { + $deletedItems[$object->getId()] = $object->getRealFullPath(); + if ($object->isAllowed('delete') && !$object->isLocked()) { + $object->delete(); + } + } + + return new DeleteDataObjectResult(deleted: $deletedItems); + } + + $object = DataObject::getById($id); + + if ($object) { + if (!$object->isAllowed('delete')) { + throw new AccessDeniedHttpException('Missing permission to delete object'); + } + + if ($object->isLocked()) { + throw new RuntimeException('prevented deleting object, because it is locked: ID: ' . $object->getId()); + } + + $object->delete(); + } + + // Return empty result even when the object doesn't exist — valid for batch delete incl. children + return new DeleteDataObjectResult(); + } +} diff --git a/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectPayload.php b/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectPayload.php new file mode 100644 index 00000000..72b4c372 --- /dev/null +++ b/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectPayload.php @@ -0,0 +1,39 @@ +request->getString('type'), + id: $request->request->getInt('id'), + amount: $request->request->getInt('amount'), + ); + } +} diff --git a/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectResult.php b/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectResult.php new file mode 100644 index 00000000..e83f5f3b --- /dev/null +++ b/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectResult.php @@ -0,0 +1,25 @@ +id); + $fc->delete(); + } +} diff --git a/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionHandler.php b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionHandler.php new file mode 100644 index 00000000..b712bd05 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionHandler.php @@ -0,0 +1,43 @@ +id); + + if (!$fieldCollection instanceof DataObject\Fieldcollection\Definition) { + $errorMessage = ': Field-Collection with id [ ' . $payload->id . ' not found. ]'; + Logger::error($errorMessage); + + throw new NotFoundHttpException($errorMessage); + } + + return new ExportFieldCollectionResult( + $fieldCollection->getKey(), + DataObject\ClassDefinition\Service::generateFieldCollectionJson($fieldCollection), + ); + } +} diff --git a/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionPayload.php b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionPayload.php new file mode 100644 index 00000000..57cbe0df --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionPayload.php @@ -0,0 +1,30 @@ +query->getString('id')); + } +} diff --git a/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionResult.php b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionResult.php new file mode 100644 index 00000000..e60dbead --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionResult.php @@ -0,0 +1,26 @@ +id); + + return new GetFieldCollectionResult($fc->getObjectVars(), $fc->isWritable()); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionPayload.php b/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionPayload.php new file mode 100644 index 00000000..23c6d305 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionPayload.php @@ -0,0 +1,30 @@ +query->getString('id')); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionResult.php b/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionResult.php new file mode 100644 index 00000000..b01be6e2 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionResult.php @@ -0,0 +1,26 @@ +allowedTypes; + $fieldName = $payload->fieldName; + $objectId = $payload->objectId; + $layoutId = $payload->layoutId; + $adminUser = $this->userContext->getAdminUser(); + $list = (new DataObject\Fieldcollection\Definition\Listing())->load(); + $currentLayoutId = $layoutId; + + if ($allowedTypes !== null) { + $filteredList = []; + $object = DataObject\Concrete::getById($objectId); + + foreach ($list as $type) { + if (!in_array($type->getKey(), $allowedTypes)) { + continue; + } + + $filteredList[] = $type; + + $layoutDefinitions = $type->getLayoutDefinitions(); + $context = [ + 'containerType' => 'fieldcollection', + 'containerKey' => $type->getKey(), + 'outerFieldname' => $fieldName, + ]; + + DataObject\Service::enrichLayoutDefinition($layoutDefinitions, $object, $context); + + if ($currentLayoutId == -1 && $adminUser->isAdmin()) { + DataObject\Service::createSuperLayout($layoutDefinitions); + } + } + + $list = $filteredList; + } + + $event = new GenericEvent(null, ['list' => $list, 'objectId' => $objectId]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_FIELDCOLLECTION_LIST_PRE_SEND_DATA); + + return new FieldCollectionListResult($event->getArgument('list')); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollectionList/GetFieldCollectionListPayload.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionList/GetFieldCollectionListPayload.php new file mode 100644 index 00000000..94ec1ab7 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionList/GetFieldCollectionListPayload.php @@ -0,0 +1,42 @@ +query->getString('allowedTypes'); + + return new static( + layoutId: $request->query->getString('layoutId') ?: null, + allowedTypes: $allowedTypes !== '' ? explode(',', $allowedTypes) : null, + fieldName: $request->query->getString('field_name') ?: null, + objectId: $request->query->getInt('object_id'), + ); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/FieldCollectionTreeResult.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/FieldCollectionTreeResult.php new file mode 100644 index 00000000..e14e7930 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/FieldCollectionTreeResult.php @@ -0,0 +1,26 @@ +forObjectEditor; + $allowedTypes = $payload->allowedTypes; + $objectId = $payload->objectId; + $layoutId = $payload->layoutId; + $adminUser = $this->userContext->getAdminUser(); + $list = (new DataObject\Fieldcollection\Definition\Listing())->load(); + + $layoutDefinitions = []; + $definitions = []; + $currentLayoutId = $layoutId; + $object = $objectId > 0 ? DataObject\Concrete::getById($objectId) : null; + + $groups = []; + foreach ($list as $item) { + if ($allowedTypes && !in_array($item->getKey(), $allowedTypes)) { + continue; + } + + $nodeData = [ + 'id' => $item->getKey(), + 'text' => $item->getKey(), + 'title' => $item->getTitle(), + 'key' => $item->getKey(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_fieldcollection', + ]; + + if ($forObjectEditor) { + $itemLayoutDefinitions = $item->getLayoutDefinitions(); + DataObject\Service::enrichLayoutDefinition($itemLayoutDefinitions, $object); + if ($currentLayoutId == -1 && $adminUser->isAdmin()) { + DataObject\Service::createSuperLayout($itemLayoutDefinitions); + } + $layoutDefinitions[$item->getKey()] = $itemLayoutDefinitions; + } + + if ($item->getGroup()) { + if (!isset($groups[$item->getGroup()])) { + $groups[$item->getGroup()] = [ + 'id' => 'group_' . $item->getKey(), + 'text' => htmlspecialchars($item->getGroup()), + 'expandable' => true, + 'leaf' => false, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'group' => $item->getGroup(), + 'children' => [], + ]; + } + $groups[$item->getGroup()]['children'][] = $nodeData; + } else { + $definitions[] = $nodeData; + } + } + + foreach ($groups as $group) { + $definitions[] = $group; + } + + $event = new GenericEvent(null, [ + 'list' => $definitions, + 'objectId' => $objectId, + 'layoutDefinitions' => $layoutDefinitions, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_FIELDCOLLECTION_LIST_PRE_SEND_DATA); + + return new FieldCollectionTreeResult( + $event->getArgument('list'), + $event->getArgument('layoutDefinitions'), + ); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/GetFieldCollectionTreePayload.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/GetFieldCollectionTreePayload.php new file mode 100644 index 00000000..945432c2 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/GetFieldCollectionTreePayload.php @@ -0,0 +1,42 @@ +query->getString('allowedTypes'); + + return new static( + forObjectEditor: (bool) $request->query->getString('forObjectEditor'), + allowedTypes: $allowedTypes !== '' ? explode(',', $allowedTypes) : null, + objectId: $request->query->getInt('object_id'), + layoutId: $request->query->getString('layoutId') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesHandler.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesHandler.php new file mode 100644 index 00000000..6c09fea9 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesHandler.php @@ -0,0 +1,43 @@ +load() as $class) { + foreach ($class->getFieldDefinitions() as $fieldDef) { + if ($fieldDef instanceof Fieldcollections && in_array($payload->key, $fieldDef->getAllowedTypes())) { + $result[] = [ + 'class' => $class->getName(), + 'field' => $fieldDef->getName(), + ]; + } + } + } + + return $result; + } +} diff --git a/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesPayload.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesPayload.php new file mode 100644 index 00000000..4d148dd8 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesPayload.php @@ -0,0 +1,30 @@ +query->getString('key')); + } +} diff --git a/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionHandler.php b/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionHandler.php new file mode 100644 index 00000000..20f6ed65 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionHandler.php @@ -0,0 +1,34 @@ +id); + + if (!DataObject\ClassDefinition\Service::importFieldCollectionFromJson($fieldCollection, $payload->json)) { + throw new RuntimeException('Failed to import field collection: ' . $payload->id); + } + } +} diff --git a/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionPayload.php b/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionPayload.php new file mode 100644 index 00000000..48557885 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ImportFieldCollection/ImportFieldCollectionPayload.php @@ -0,0 +1,40 @@ +files->get('Filedata'); + + return new static( + id: $request->query->getString('id'), + json: file_get_contents($file->getPathname()), + ); + } +} diff --git a/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionHandler.php b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionHandler.php new file mode 100644 index 00000000..4418043e --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionHandler.php @@ -0,0 +1,66 @@ +key; + $title = $payload->title; + $group = $payload->group; + $isAdd = $payload->isAdd; + $values = $payload->values; + $configuration = $payload->configuration; + + if ($isAdd) { + $list = new DataObject\Fieldcollection\Definition\Listing(); + foreach ($list->loadNames() as $fcName) { + if (strtolower($key) === strtolower($fcName)) { + throw new BadRequestHttpException('FieldCollection with the same name already exists (lower/upper cases may be different)'); + } + } + } + + $fcDef = new DataObject\Fieldcollection\Definition(); + $fcDef->setKey($key); + $fcDef->setTitle($title); + $fcDef->setGroup($group); + + if ($values !== null) { + $fcDef->setParentClass($values['parentClass']); + $fcDef->setImplementsInterfaces($values['implementsInterfaces']); + } + + if ($configuration !== null) { + $configuration['datatype'] = 'layout'; + $configuration['fieldtype'] = 'panel'; + + $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); + $fcDef->setLayoutDefinitions($layout); + } + + $fcDef->save(); + + return $fcDef; + } +} diff --git a/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionPayload.php b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionPayload.php new file mode 100644 index 00000000..d6add299 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionPayload.php @@ -0,0 +1,44 @@ +request->getString('key'), + title: $request->request->getString('title'), + group: $request->request->getString('group'), + isAdd: $request->request->getString('task') === 'add', + values: $request->request->has('values') ? (json_decode($request->request->getString('values'), true) ?? null) : null, + configuration: $request->request->has('configuration') ? (json_decode($request->request->getString('configuration'), true) ?? null) : null, + ); + } +} diff --git a/src/Handler/DataObject/GetDataObject/GetDataObjectHandler.php b/src/Handler/DataObject/GetDataObject/GetDataObjectHandler.php new file mode 100644 index 00000000..8deb3159 --- /dev/null +++ b/src/Handler/DataObject/GetDataObject/GetDataObjectHandler.php @@ -0,0 +1,387 @@ +id; + $layoutId = $payload->layoutId; + $objectFromDatabase = DataObject\Concrete::getById($id); + if (!$objectFromDatabase instanceof DataObject\Concrete) { + throw new NotFoundHttpException('element_not_found'); + } + + $objectFromDatabase = clone $objectFromDatabase; + $draftVersion = null; + $adminUser = $this->userContext->getAdminUser(); + $object = DataObjectVersionHelper::resolveLatestDraft($objectFromDatabase, $adminUser?->getId(), $draftVersion); + $objectFromVersion = $object !== $objectFromDatabase; + + if (!$object->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + if ($object->isAllowed('save') || $object->isAllowed('publish') || $object->isAllowed('unpublish') || $object->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($object->getId(), 'object', AdminEvents::OBJECT_GET_IS_LOCKED, $object); + } + + $objectData = []; + + /** ------------------------------------------------------------- + * Load some general data from published object (if existing) + * ------------------------------------------------------------- */ + $objectData['idPath'] = Element\Service::getIdPath($objectFromDatabase); + + $linkGeneratorReference = $objectFromDatabase->getClass()->getLinkGeneratorReference(); + $previewGenerator = $objectFromDatabase->getClass()->getPreviewGenerator(); + if (empty($previewGenerator) && !empty($linkGeneratorReference)) { + $previewGenerator = $this->defaultPreviewGenerator; + } + + $objectData['hasPreview'] = false; + if ($linkGeneratorReference || $previewGenerator) { + $objectData['hasPreview'] = true; + } + + $objectData['general'] = []; + + $allowedKeys = ['published', 'key', 'id', 'creationDate', 'classId', 'className', 'type', 'parentId', 'userOwner', 'userModification']; + foreach ($objectFromDatabase->getObjectVars() as $key => $value) { + if (in_array($key, $allowedKeys)) { + $objectData['general'][$key] = $value; + } + } + $objectData['general']['classTitle'] = $objectFromDatabase->getClass()->getTitle() ?: $objectFromDatabase->getClassName(); + $objectData['general']['fullpath'] = $objectFromDatabase->getRealFullPath(); + $objectData['general']['locked'] = $objectFromDatabase->isLocked(); + $objectData['general']['php'] = [ + 'classes' => [$objectFromDatabase::class, ...array_values(class_parents($objectFromDatabase))], + 'interfaces' => array_values(class_implements($objectFromDatabase)), + ]; + $objectData['general']['allowInheritance'] = $objectFromDatabase->getClass()->getAllowInherit(); + $objectData['general']['allowVariants'] = $objectFromDatabase->getClass()->getAllowVariants(); + $objectData['general']['showVariants'] = $objectFromDatabase->getClass()->getShowVariants(); + $objectData['general']['showAppLoggerTab'] = $objectFromDatabase->getClass()->getShowAppLoggerTab(); + $objectData['general']['showFieldLookup'] = $objectFromDatabase->getClass()->getShowFieldLookup(); + $objectData['general']['linkGeneratorReference'] = $linkGeneratorReference; + + if ($previewGenerator) { + $objectData['general']['previewConfig'] = $previewGenerator->getPreviewConfig($objectFromDatabase); + } + + $objectData['layout'] = $objectFromDatabase->getClass()->getLayoutDefinitions(); + $objectData['userPermissions'] = $objectFromDatabase->getUserPermissions($adminUser); + $objectVersions = Element\Service::getSafeVersionInfo($objectFromDatabase->getVersions()); + $objectData['versions'] = array_splice($objectVersions, -1, 1); + $objectData['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $objectFromDatabase->getScheduledTasks() + ); + + $objectData['childdata']['id'] = $objectFromDatabase->getId(); + $objectData['childdata']['data']['classes'] = $this->prepareChildClasses($objectFromDatabase->getDao()->getClasses()); + $objectData['childdata']['data']['general'] = $objectData['general']; + + /** ------------------------------------------------------------- + * Load remaining general data from latest version + * ------------------------------------------------------------- */ + $allowedKeys = ['modificationDate', 'userModification']; + foreach ($object->getObjectVars() as $key => $value) { + if (in_array($key, $allowedKeys)) { + $objectData['general'][$key] = $value; + } + } + + $loadContext = new DataObjectLoadContext(); + try { + $this->getDataForObject($object, $loadContext, $objectFromVersion); + } catch (Throwable) { + $loadContext = new DataObjectLoadContext(); + $this->getDataForObject($objectFromDatabase, $loadContext, false); + } + + $objectData['data'] = $loadContext->objectData; + $objectData['metaData'] = $loadContext->metaData; + $objectData['properties'] = Element\Service::minimizePropertiesForEditmode($object->getProperties()); + + // this used for the "this is not a published version" hint + // and for adding the published icon to version overview + $objectData['general']['versionDate'] = $objectFromDatabase->getModificationDate(); + $objectData['general']['versionCount'] = $objectFromDatabase->getVersionCount(); + + $currentLayoutId = $layoutId; + + $validLayouts = DataObject\Service::getValidLayouts($object); + + //Fallback if $currentLayoutId is not set or empty string + //Uses first valid layout instead of admin layout when empty + $ok = false; + foreach ($validLayouts as $layout) { + if ($currentLayoutId == $layout->getId()) { + $ok = true; + } + } + + if (!$ok) { + $currentLayoutId = null; + } + + //main layout has id 0 so we check for is_null() + if ($currentLayoutId === null && $validLayouts !== []) { + if (count($validLayouts) === 1) { + $firstLayout = reset($validLayouts); + $currentLayoutId = $firstLayout->getId(); + } else { + foreach ($validLayouts as $checkDefaultLayout) { + if ($checkDefaultLayout->getDefault()) { + $currentLayoutId = $checkDefaultLayout->getId(); + } + } + } + } + + if ($currentLayoutId === null && count($validLayouts) > 0) { + $currentLayoutId = reset($validLayouts)->getId(); + } + + if ($validLayouts !== []) { + $objectData['validLayouts'] = []; + + foreach ($validLayouts as $validLayout) { + $objectData['validLayouts'][] = ['id' => $validLayout->getId(), 'name' => $validLayout->getName()]; + } + + usort($objectData['validLayouts'], static function ($layoutData1, $layoutData2) { + if ($layoutData2['id'] === '-1') { + return 1; + } + + if ($layoutData1['id'] === '-1') { + return -1; + } + + if ($layoutData2['id'] === '0') { + return 1; + } + if ($layoutData1['id'] === '0') { + return -1; + } + + return strcasecmp($layoutData1['name'], $layoutData2['name']); + }); + + $user = Tool\Admin::getCurrentUser(); + + if ($currentLayoutId == -1 && $user->isAdmin()) { + $layout = DataObject\Service::getSuperLayoutDefinition($object); + $objectData['layout'] = $layout; + } elseif (!empty($currentLayoutId)) { + $objectData['layout'] = $validLayouts[$currentLayoutId]->getLayoutDefinitions(); + } + + $objectData['currentLayoutId'] = $currentLayoutId; + } + + DataObject\Service::enrichLayoutDefinition($objectData['layout'], $object); + + $event = new GenericEvent($this, ['data' => $objectData, 'object' => $object]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_GET_PRE_SEND_DATA); + $objectData = $event->getArgument('data'); + + $this->adminStyleEnricher->forEditor($object, $objectData['general']); + $this->userNamesEnricher->enrich($object, $objectData['general']); + $this->customLayoutEnricher->enrich($object, $objectData); + $this->draftEnricher->enrich($object, $objectData, $draftVersion); + + return new GetDataObjectResult(data: $objectData); + } + + private function getDataForObject(DataObject\Concrete $object, DataObjectLoadContext $loadContext, bool $objectFromVersion = false): void + { + foreach ($object->getClass()->getFieldDefinitions(['object' => $object]) as $key => $def) { + $this->getDataForField($object, $key, $def, $loadContext, $objectFromVersion); + } + } + + /** + * Gets recursively attribute data from parent and fills objectData and metaData + */ + private function getDataForField(DataObject\Concrete $object, string $key, DataObject\ClassDefinition\Data $fielddefinition, DataObjectLoadContext $loadContext, bool $objectFromVersion, int $level = 0): void + { + $parent = DataObject\Service::hasInheritableParentObject($object); + $getter = 'get' . ucfirst($key); + + // Editmode optimization for lazy loaded relations (note that this is just for AbstractRelations, not for all + // LazyLoadingSupportInterface types. It tries to optimize fetching the data needed for the editmode without + // loading the entire target element. + // ReverseObjectRelation should go in there anyway (regardless if it a version or not), + // so that the values can be loaded. + if ( + (!$objectFromVersion && $fielddefinition instanceof AbstractRelations) + || $fielddefinition instanceof ReverseObjectRelation + ) { + $refId = null; + + if ($fielddefinition instanceof ReverseObjectRelation) { + $refKey = $fielddefinition->getOwnerFieldName(); + $refClass = DataObject\ClassDefinition::getByName($fielddefinition->getOwnerClassName()); + if ($refClass) { + $refId = $refClass->getId(); + } + } else { + $refKey = $key; + } + + $relations = $object->getRelationData($refKey, !$fielddefinition instanceof ReverseObjectRelation, $refId); + + if ($fielddefinition->supportsInheritance() && $relations === [] && !empty($parent)) { + $this->getDataForField($parent, $key, $fielddefinition, $loadContext, $objectFromVersion, $level + 1); + } else { + $data = []; + + if ($fielddefinition instanceof DataObject\ClassDefinition\Data\ManyToOneRelation) { + if (isset($relations[0])) { + $data = $relations[0]; + $data['published'] = (bool)$data['published']; + } else { + $data = null; + } + } elseif ( + ($fielddefinition instanceof DataObject\ClassDefinition\Data\OptimizedAdminLoadingInterface && $fielddefinition->isOptimizedAdminLoading()) + || ($fielddefinition instanceof ManyToManyObjectRelation && !$fielddefinition->getVisibleFields() && !$fielddefinition instanceof DataObject\ClassDefinition\Data\AdvancedManyToManyObjectRelation) + ) { + foreach ($relations as $rkey => $rel) { + $index = $rkey + 1; + $rel['fullpath'] = $rel['path']; + $rel['classname'] = $rel['subtype']; + $rel['rowId'] = $rel['id'] . AbstractRelations::RELATION_ID_SEPARATOR . $index . AbstractRelations::RELATION_ID_SEPARATOR . $rel['type']; + $rel['published'] = (bool)$rel['published']; + $data[] = $rel; + } + } else { + $fieldData = $object->$getter(); + $data = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); + } + $loadContext->objectData[$key] = $data; + $loadContext->metaData[$key]['objectid'] = $object->getId(); + $loadContext->metaData[$key]['inherited'] = $level !== 0; + } + } else { + $fieldData = $object->$getter(); + $isInheritedValue = false; + + if ($fielddefinition instanceof DataObject\ClassDefinition\Data\CalculatedValue) { + $fieldData = new DataObject\Data\CalculatedValue($fielddefinition->getName()); + $fieldData->setContextualData('object', null, null, null, null, null, $fielddefinition); + $value = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); + } else { + $value = $fielddefinition->getDataForEditmode($fieldData, $object, ['objectFromVersion' => $objectFromVersion]); + } + + // following some exceptions for special data types (localizedfields, objectbricks) + if ($value && ($fieldData instanceof DataObject\Localizedfield || $fieldData instanceof DataObject\Classificationstore)) { + // make sure that the localized field participates in the inheritance detection process + $isInheritedValue = $value['inherited']; + } + if ($fielddefinition instanceof DataObject\ClassDefinition\Data\Objectbricks && is_array($value)) { + // make sure that the objectbricks participate in the inheritance detection process + foreach ($value as $singleBrickData) { + if (!empty($singleBrickData['inherited'])) { + $isInheritedValue = true; + } + } + } + + if ($fielddefinition->isEmpty($fieldData) && !empty($parent)) { + $this->getDataForField($parent, $key, $fielddefinition, $loadContext, $objectFromVersion, $level + 1); + // exception for classification store. if there are no items then it is empty by definition. + // consequence is that we have to preserve the metadata information + if ($fielddefinition instanceof DataObject\ClassDefinition\Data\Classificationstore && $level === 0) { + $loadContext->objectData[$key]['metaData'] = $value['metaData'] ?? []; + $loadContext->objectData[$key]['inherited'] = true; + } + } else { + $isInheritedValue = $isInheritedValue || ($level !== 0); + $loadContext->metaData[$key]['objectid'] = $object->getId(); + + $loadContext->objectData[$key] = $value; + $loadContext->metaData[$key]['inherited'] = $isInheritedValue; + + if ($isInheritedValue && !$fielddefinition->isEmpty($fieldData) && !$fielddefinition->supportsInheritance()) { + $loadContext->objectData[$key] = null; + $loadContext->metaData[$key]['inherited'] = false; + $loadContext->metaData[$key]['hasParentValue'] = true; + } + } + } + } + + /** + * @param DataObject\ClassDefinition[] $classes + */ + private function prepareChildClasses(array $classes): array + { + $reduced = []; + foreach ($classes as $class) { + $reduced[] = [ + 'id' => $class->getId(), + 'name' => $class->getName(), + 'inheritance' => $class->getAllowInherit(), + ]; + } + + return $reduced; + } + +} diff --git a/src/Handler/DataObject/GetDataObject/GetDataObjectPayload.php b/src/Handler/DataObject/GetDataObject/GetDataObjectPayload.php new file mode 100644 index 00000000..03ffc476 --- /dev/null +++ b/src/Handler/DataObject/GetDataObject/GetDataObjectPayload.php @@ -0,0 +1,36 @@ +query->getInt('id'), + layoutId: is_numeric($request->query->get('layoutId')) ? $request->query->getInt('layoutId') : null, + ); + } +} diff --git a/src/Handler/DataObject/GetDataObject/GetDataObjectResult.php b/src/Handler/DataObject/GetDataObject/GetDataObjectResult.php new file mode 100644 index 00000000..47706f36 --- /dev/null +++ b/src/Handler/DataObject/GetDataObject/GetDataObjectResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $objects = []; + + $cv = $view ? ($this->elementService->getCustomViewById($view) ?? []) : []; + + if (!is_null($filter)) { + // When filter is applied, limit was capped to 100 by caller + $limit = 100; + } + + $children = $childrenList->load(); + $filteredTotalCount = $childrenList->getTotalCount(); + + foreach ($children as $child) { + $objectTreeNode = $this->elementService->getElementTreeNodeConfig($child); + // this if is obsolete since as long as the change with #11714 about list on line 175-179 are working fine, we already filter the list=1 there + if ($objectTreeNode['permissions']['list'] == 1) { + $objects[] = $objectTreeNode; + } + } + + //pagination for custom view + $total = $cv + ? $filteredTotalCount + : $object->getChildAmount(null, $adminUser); + + return new GetDataObjectChildrenResult( + objects: $objects, + offset: $offset, + limit: $limit, + total: $total, + filteredTotalCount: $filteredTotalCount, + filter: $filter, + fromPaging: $fromPaging, + ); + } +} diff --git a/src/Handler/DataObject/GetDataObjectChildrenResult.php b/src/Handler/DataObject/GetDataObjectChildrenResult.php new file mode 100644 index 00000000..d83c81d3 --- /dev/null +++ b/src/Handler/DataObject/GetDataObjectChildrenResult.php @@ -0,0 +1,31 @@ +userContext->getAdminUser(); + $object = DataObject::getById($payload->id); + + if (!$object) { + throw new NotFoundHttpException(sprintf('DataObject with id %d not found', $payload->id)); + } + + if (!$object->isAllowed('view')) { + throw new AccessDeniedHttpException('Missing permission to view object'); + } + + $objectData = []; + + $objectData['general'] = []; + $objectData['idPath'] = Element\Service::getIdPath($object); + $objectData['type'] = $object->getType(); + $allowedKeys = ['published', 'key', 'id', 'type', 'path', 'modificationDate', 'creationDate', 'userOwner', 'userModification']; + foreach ($object->getObjectVars() as $key => $value) { + if (in_array($key, $allowedKeys)) { + $objectData['general'][$key] = $value; + } + } + $objectData['general']['fullpath'] = $object->getRealFullPath(); + $objectData['general']['locked'] = $object->isLocked(); + + $objectData['properties'] = Element\Service::minimizePropertiesForEditmode($object->getProperties()); + $objectData['userPermissions'] = $object->getUserPermissions($adminUser); + $objectData['classes'] = $this->prepareChildClasses($object->getDao()->getClasses()); + + $this->userNamesEnricher->enrich($object, $objectData['general']); + + $event = new GenericEvent($this, ['data' => $objectData, 'object' => $object]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_GET_PRE_SEND_DATA); + $objectData = $event->getArgument('data'); + + return new GetDataObjectFolderResult(data: $objectData); + } + + /** + * @param DataObject\ClassDefinition[] $classes + */ + private function prepareChildClasses(array $classes): array + { + $reduced = []; + foreach ($classes as $class) { + $reduced[] = [ + 'id' => $class->getId(), + 'name' => $class->getName(), + 'inheritance' => $class->getAllowInherit(), + ]; + } + + return $reduced; + } +} diff --git a/src/Handler/DataObject/GetDataObjectFolder/GetDataObjectFolderResult.php b/src/Handler/DataObject/GetDataObjectFolder/GetDataObjectFolderResult.php new file mode 100644 index 00000000..66c65198 --- /dev/null +++ b/src/Handler/DataObject/GetDataObjectFolder/GetDataObjectFolderResult.php @@ -0,0 +1,25 @@ +sessionService->getObject('object', $payload->id); + + if (!$object instanceof DataObject\Concrete) { + throw new NotFoundHttpException(sprintf('Expected an object of type "%s", got "%s"', DataObject\Concrete::class, get_debug_type($object))); + } + + $queryParams = $payload->queryParams; + $url = null; + if ($previewService = $object->getClass()->getPreviewGenerator()) { + $url = $previewService->generatePreviewUrl($object, ['preview' => true, ...$queryParams]); + } elseif ($object->getClass()->getLinkGenerator()) { + $parameters = [ + 'preview' => true, + ]; + + $url = $this->defaultPreviewGenerator->generatePreviewUrl($object, [...$parameters, ...$queryParams]); + } + + if (!$url) { + throw new NotFoundHttpException('Cannot render preview due to empty URL'); + } + + // replace all remaining % signs + $url = str_replace('%', '%25', $url); + + $urlParts = parse_url($url); + + $redirectParameters = array_filter([ + 'opendxp_object_preview' => $object->getId(), + 'site' => $queryParams[PreviewGeneratorInterface::PARAMETER_SITE] ?? null, + 'dc' => time(), + ]); + + return $urlParts['path'] . '?' . http_build_query($redirectParameters) . (isset($urlParts['query']) ? '&' . $urlParts['query'] : ''); + } +} diff --git a/src/Handler/DataObject/GetDataObjectPreviewUrl/GetDataObjectPreviewUrlPayload.php b/src/Handler/DataObject/GetDataObjectPreviewUrl/GetDataObjectPreviewUrlPayload.php new file mode 100644 index 00000000..d8bb915a --- /dev/null +++ b/src/Handler/DataObject/GetDataObjectPreviewUrl/GetDataObjectPreviewUrlPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + queryParams: $request->query->all(), + ); + } +} diff --git a/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoHandler.php b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoHandler.php new file mode 100644 index 00000000..a461ca6b --- /dev/null +++ b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoHandler.php @@ -0,0 +1,60 @@ +path; + $limit = $payload->limit; + $pathParts = explode('/', $path); + $id = (int) array_pop($pathParts); + + $data = []; + + $object = DataObject::getById($id); + + while ($parent = $object->getParent()) { + $list = new DataObject\Listing(); + $list->setCondition('parentId = ?', $parent->getId()); + $list->setUnpublished(true); + $total = $list->getTotalCount(); + + $info = [ + 'total' => $total, + ]; + + if ($total > $limit) { + $idList = $list->loadIdList(); + $position = array_search($object->getId(), $idList); + $info['position'] = $position + 1; + $info['page'] = ceil($info['position'] / $limit); + } + + $data[$parent->getId()] = $info; + + $object = $parent; + } + + return new GetIdPathPagingInfoResult(data: $data); + } +} diff --git a/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoPayload.php b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoPayload.php new file mode 100644 index 00000000..d79f45ec --- /dev/null +++ b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoPayload.php @@ -0,0 +1,37 @@ +query->getString('path') ?: null, + limit: $request->query->getInt('limit', 30), + ); + } +} diff --git a/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoResult.php b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoResult.php new file mode 100644 index 00000000..c2b3efdd --- /dev/null +++ b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoResult.php @@ -0,0 +1,25 @@ +objectId); + if (!$object instanceof DataObject\Concrete) { + throw new NotFoundHttpException('Object not found.'); + } + + if ($payload->changedData !== null) { + $this->mapper->applyChanges($object, $payload->changedData); + } + + /** @var DataObject\ClassDefinition\Data\Select|DataObject\ClassDefinition\Data\Multiselect $fieldDefinition */ + $fieldDefinition = DataObject\Classificationstore\Service::getFieldDefinitionFromJson( + $payload->fieldDefinitionConfig, + $payload->fieldDefinitionConfig['fieldtype'] + ); + + $optionsProvider = OptionsProviderResolver::resolveProvider( + $fieldDefinition->getOptionsProviderClass(), + $fieldDefinition instanceof DataObject\ClassDefinition\Data\Multiselect + ? OptionsProviderResolver::MODE_MULTISELECT + : OptionsProviderResolver::MODE_SELECT + ); + + return $optionsProvider->getOptions( + [ + 'object' => $object, + 'fieldname' => $fieldDefinition->getName(), + 'class' => $object->getClass(), + 'context' => $payload->context, + ], + $fieldDefinition + ); + } +} diff --git a/src/Handler/DataObject/GetSelectOptions/GetSelectOptionsPayload.php b/src/Handler/DataObject/GetSelectOptions/GetSelectOptionsPayload.php new file mode 100644 index 00000000..a59fad48 --- /dev/null +++ b/src/Handler/DataObject/GetSelectOptions/GetSelectOptionsPayload.php @@ -0,0 +1,40 @@ +request->getInt('objectId'), + changedData: $request->request->has('changedData') ? json_decode($request->request->getString('changedData'), true) : null, + fieldDefinitionConfig: json_decode($request->request->getString('fieldDefinition'), true) ?? [], + context: json_decode($request->request->getString('context'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllHandler.php b/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllHandler.php new file mode 100644 index 00000000..094778a3 --- /dev/null +++ b/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllHandler.php @@ -0,0 +1,49 @@ +userContext->getAdminUser(); + $object = DataObject::getById($payload->objectId); + if (!$object) { + throw new NotFoundHttpException(); + } + + if (!$object->isAllowed('list')) { + throw new AccessDeniedHttpException(); + } + + Db::get()->executeStatement( + 'DELETE FROM gridconfig_favourites WHERE ownerId = ? AND classId = ? AND searchType = ? AND objectId != ? AND objectId != 0', + [$adminUser->getId(), $payload->classId, $payload->searchType, $payload->objectId] + ); + } +} diff --git a/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllPayload.php b/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllPayload.php new file mode 100644 index 00000000..05ef527e --- /dev/null +++ b/src/Handler/DataObject/Helper/ApplyGridConfigToAll/ApplyGridConfigToAllPayload.php @@ -0,0 +1,39 @@ +request->getInt('objectId'), + classId: $request->request->getString('classId'), + searchType: $request->request->getString('searchType'), + ); + } +} diff --git a/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigHandler.php b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigHandler.php new file mode 100644 index 00000000..6f0aa698 --- /dev/null +++ b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigHandler.php @@ -0,0 +1,81 @@ +gridConfigId !== null) { + $adminUser = $this->userContext->getAdminUser(); + $gridConfig = GridConfig::getById($payload->gridConfigId); + if (!$gridConfig) { + throw new NotFoundHttpException('Grid config not found: ' . $payload->gridConfigId); + } + if ($gridConfig->getOwnerId() !== $adminUser->getId() && !$adminUser->isAdmin()) { + throw new BadRequestHttpException("don't mess with someone elses grid config"); + } + $gridConfig->delete(); + } + + $params = [ + 'id' => $payload->id, + 'objectId' => $payload->objectId, + 'name' => $payload->name, + 'type' => $payload->type, + 'types' => $payload->types, + 'gridtype' => $payload->gridtype, + 'gridConfigId' => $payload->gridConfigId, + 'searchType' => $payload->searchType, + 'noSystemColumns' => $payload->noSystemColumns, + 'noBrickColumns' => $payload->noBrickColumns, + ]; + + $resolverResult = $this->gridConfigResolver->resolve($payload->locale, $params, null, true); + $data = [...$resolverResult->jsonSerialize(), 'deleteSuccess' => true]; + + $event = new GenericEvent($this, [ + 'data' => $data, + 'request' => $this->requestStack->getCurrentRequest(), + 'config' => $this->config, + 'context' => 'delete', + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_GRID_GET_COLUMN_CONFIG_PRE_SEND_DATA); + + return $event->getArgument('data'); + } +} diff --git a/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php new file mode 100644 index 00000000..7a202d5e --- /dev/null +++ b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php @@ -0,0 +1,55 @@ +request->getString('id') ?: null, + objectId: is_numeric($request->request->get('objectId')) ? $request->request->getInt('objectId') : null, + name: $request->request->getString('name') ?: null, + type: $request->request->getString('type') ?: null, + types: $request->request->getString('types') ?: null, + gridtype: $request->request->getString('gridtype') ?: null, + gridConfigId: is_numeric($request->request->get('gridConfigId')) ? $request->request->getInt('gridConfigId') : null, + searchType: $request->request->getString('searchType') ?: null, + noSystemColumns: $request->query->getBoolean('no_system_columns'), + noBrickColumns: $request->query->getBoolean('no_brick_columns'), + locale: $request->getLocale(), + ); + } +} diff --git a/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportHandler.php b/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportHandler.php new file mode 100644 index 00000000..6dddf987 --- /dev/null +++ b/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportHandler.php @@ -0,0 +1,130 @@ +userTimezone); + DataObject\Concrete::setGetInheritedValues($payload->enableInheritance); + + $class = DataObject\ClassDefinition::getById($payload->classId); + if (!$class) { + throw new InvalidArgumentException('No class definition found'); + } + + $listClass = '\\OpenDxp\\Model\\DataObject\\' . ucfirst($class->getName()) . '\\Listing'; + + /** @var Listing $list */ + $list = new $listClass(); + + $quotedIds = []; + foreach ($payload->ids as $id) { + $quotedIds[] = $list->quote($id); + } + + $list->setObjectTypes(DataObject::$types); + $list->setCondition('id IN (' . implode(',', $quotedIds) . ')'); + $list->setOrderKey(' FIELD(id, ' . implode(',', $quotedIds) . ')', false); + + $beforeListExportEvent = new GenericEvent(null, [ + 'list' => $list, + 'context' => $payload->allParams, + ]); + $this->eventDispatcher->dispatch($beforeListExportEvent, AdminEvents::OBJECT_LIST_BEFORE_EXPORT); + $list = $beforeListExportEvent->getArgument('list'); + + $csv = DataObject\Service::getCsvData( + $payload->requestedLanguage, + $this->localeService, + $list, + $payload->fields, + $payload->header, + $payload->addTitles, + $payload->context + ); + + $temp = tmpfile(); + + try { + $storage = Storage::get('temp'); + $csvFile = $this->gridExportService->getCsvFile($payload->fileHandle); + + $fileStream = $storage->readStream($csvFile); + stream_copy_to_stream($fileStream, $temp, null, 0); + + $firstLine = true; + + if ($payload->addTitles && $payload->header === 'no_header') { + array_shift($csv); + $firstLine = false; + } + + $lineCount = count($csv); + + if (!$payload->addTitles && $lineCount > 0) { + fwrite($temp, "\r\n"); + } + + for ($i = 0; $i < $lineCount; $i++) { + $line = $csv[$i]; + if ($payload->addTitles && $firstLine) { + $firstLine = false; + fwrite($temp, implode($payload->delimiter, $line)); + } else { + fwrite($temp, implode($payload->delimiter, array_map($this->gridColumnConfigService->encode(...), $line))); + } + if ($i < $lineCount - 1) { + fwrite($temp, "\r\n"); + } + } + + $storage->writeStream($csvFile, $temp); + } catch (UnableToReadFile $exception) { + Logger::err($exception->getMessage()); + throw new BadRequestHttpException(sprintf('export file not found: %s', $payload->fileHandle)); + } finally { + if (is_resource($temp)) { + fclose($temp); + } + } + } +} diff --git a/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportPayload.php b/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportPayload.php new file mode 100644 index 00000000..e7549ae9 --- /dev/null +++ b/src/Handler/DataObject/Helper/DoDataObjectExport/DoDataObjectExportPayload.php @@ -0,0 +1,66 @@ +request->getString('settings'), true) ?? []; + $fieldsRaw = $request->request->all('fields'); + $contextFromRequest = $request->request->getString('context') ?: null; + $context = ['source' => 'opendxp-export']; + if ($contextFromRequest) { + $context = [...$context, ...json_decode($contextFromRequest, true)]; + } + + return new static( + fileHandle: File::getValidFilename($request->request->getString('fileHandle')), + ids: $request->request->all('ids'), + classId: $request->request->getString('classId'), + delimiter: $settings['delimiter'] ?? ';', + header: $settings['header'] ?? 'title', + userTimezone: $request->request->getString('userTimezone') ?: null, + allParams: [...$request->request->all(), ...$request->query->all()], + requestedLanguage: $request->request->getString('language') ?: $request->getLocale(), + fields: !empty($fieldsRaw) ? (json_decode($fieldsRaw[0], true) ?? []) : [], + addTitles: (bool) $request->request->get('initial'), + enableInheritance: (bool) ($settings['enableInheritance'] ?? false), + context: $context, + ); + } +} diff --git a/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchHandler.php b/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchHandler.php new file mode 100644 index 00000000..ad6da056 --- /dev/null +++ b/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchHandler.php @@ -0,0 +1,36 @@ +userContext->getAdminUser(); + + return $this->gridBatchService->executeObjectBatch($payload->params, $payload->locale, $adminUser); + } +} diff --git a/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchPayload.php b/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchPayload.php new file mode 100644 index 00000000..d1bf156c --- /dev/null +++ b/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchPayload.php @@ -0,0 +1,41 @@ +request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? []) + : [], + locale: $request->getLocale(), + hasData: $request->request->has('data'), + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsHandler.php b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsHandler.php new file mode 100644 index 00000000..3a271795 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsHandler.php @@ -0,0 +1,115 @@ +classes; + if ($classes === null) { + return new GetAvailableVisibleFieldsResult([]); + } + + $classNameList = explode(',', $classes); + $classList = []; + foreach ($classNameList as $className) { + $class = DataObject\ClassDefinition::getByName($className); + if ($class) { + $classList[] = $class; + } + } + + if (!$classList) { + return new GetAvailableVisibleFieldsResult([]); + } + + $availableFields = []; + foreach (DataObjectGridColumnConfigResolver::SYSTEM_COLUMNS as $field) { + $availableFields[] = [ + 'key' => $field, + 'value' => $field, + ]; + } + + $commonFields = []; + $firstOne = true; + + foreach ($classNameList as $className) { + $class = DataObject\ClassDefinition::getByName($className); + if (!$class) { + continue; + } + + $fds = $class->getFieldDefinitions(); + $additionalFieldNames = array_keys($fds); + + $localizedFields = $class->getFieldDefinition('localizedfields'); + if ($localizedFields instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $lfNames = array_keys($localizedFields->getFieldDefinitions()); + $additionalFieldNames = [...$additionalFieldNames, ...$lfNames]; + } + + foreach ($commonFields as $commonFieldKey => $commonFieldDefinition) { + if (!in_array($commonFieldKey, $additionalFieldNames)) { + unset($commonFields[$commonFieldKey]); + } + } + + $this->processAvailableFieldDefinitions($fds, $firstOne, $commonFields); + $firstOne = false; + } + + foreach (array_keys($commonFields) as $field) { + $availableFields[] = [ + 'key' => $field, + 'value' => $field, + ]; + } + + return new GetAvailableVisibleFieldsResult($availableFields); + } + + /** + * @param DataObject\ClassDefinition\Data[] $fds + * @param DataObject\ClassDefinition\Data[] $commonFields + */ + private function processAvailableFieldDefinitions(array $fds, bool &$firstOne, array &$commonFields): void + { + foreach ($fds as $fd) { + if ($fd instanceof DataObject\ClassDefinition\Data\Fieldcollections) { + continue; + } + if ($fd instanceof DataObject\ClassDefinition\Data\Objectbricks) { + continue; + } + if ($fd instanceof DataObject\ClassDefinition\Data\Block) { + continue; + } + if ($fd instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $lfDefs = $fd->getFieldDefinitions(); + $this->processAvailableFieldDefinitions($lfDefs, $firstOne, $commonFields); + } elseif ($firstOne || (isset($commonFields[$fd->getName()]) && $commonFields[$fd->getName()]->getFieldtype() == $fd->getFieldtype())) { + $commonFields[$fd->getName()] = $fd; + } + } + } +} diff --git a/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsPayload.php b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsPayload.php new file mode 100644 index 00000000..9a3c8d96 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsPayload.php @@ -0,0 +1,35 @@ +query->getString('classes') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsResult.php b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsResult.php new file mode 100644 index 00000000..0a0e2c85 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + + return new GetBatchJobsResult( + jobs: $this->gridBatchService->getObjectBatchJobIds($payload->allParams, $payload->locale, $adminUser), + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsPayload.php b/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsPayload.php new file mode 100644 index 00000000..a28314a6 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsPayload.php @@ -0,0 +1,37 @@ +request->all(), ...$request->query->all()], + locale: $request->request->getString('language') ?: $request->getLocale(), + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsResult.php b/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsResult.php new file mode 100644 index 00000000..7935fd01 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $userId = $adminUser?->getId() ?? 0; + + $list = $this->gridColumnConfigService->getMyOwnColumnConfigs($userId, $payload->classId); + $list = [...$list, ...$this->gridColumnConfigService->getSharedColumnConfigs($adminUser, $payload->classId)]; + + $result = [['id' => -1, 'name' => '--default--']]; + + foreach ($list as $config) { + $result[] = [ + 'id' => $config['id'], + 'name' => $config['name'], + ]; + } + + return $result; + } +} diff --git a/src/Handler/DataObject/Helper/GetExportConfigs/GetExportConfigsPayload.php b/src/Handler/DataObject/Helper/GetExportConfigs/GetExportConfigsPayload.php new file mode 100644 index 00000000..c8993cba --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportConfigs/GetExportConfigsPayload.php @@ -0,0 +1,35 @@ +query->getString('classId') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsHandler.php b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsHandler.php new file mode 100644 index 00000000..5fb3a235 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsHandler.php @@ -0,0 +1,64 @@ +allParams; + $fieldnames = []; + $fields = json_decode($allParams['fields'][0], true); + foreach ($fields as $field) { + $fieldnames[] = $field['key']; + } + $allParams['fields'] = $fieldnames; + + $list = $this->gridHelperService->prepareListingForGrid($allParams, $payload->requestedLanguage, $this->userContext->getAdminUser()); + + $beforeListPrepareEvent = new GenericEvent($this, [ + 'list' => $list, + 'context' => $allParams, + ]); + $this->eventDispatcher->dispatch($beforeListPrepareEvent, AdminEvents::OBJECT_LIST_BEFORE_EXPORT_PREPARE); + $list = $beforeListPrepareEvent->getArgument('list'); + + $ids = $list->loadIdList(); + $jobs = array_chunk($ids, 20); + + $fileHandle = uniqid('export-'); + Storage::get('temp')->write($this->gridExportService->getCsvFile($fileHandle), ''); + + return new GetExportJobsResult(jobs: $jobs, fileHandle: $fileHandle); + } +} diff --git a/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsPayload.php b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsPayload.php new file mode 100644 index 00000000..23bb21a7 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsPayload.php @@ -0,0 +1,37 @@ +request->all(), ...$request->query->all()], + requestedLanguage: $request->request->getString('language') ?: $request->getLocale(), + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsResult.php b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsResult.php new file mode 100644 index 00000000..e4ec3bd6 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsResult.php @@ -0,0 +1,26 @@ + $payload->id, + 'objectId' => $payload->objectId, + 'name' => $payload->name, + 'type' => $payload->type, + 'types' => $payload->types, + 'gridtype' => $payload->gridtype, + 'gridConfigId' => $payload->gridConfigId, + 'searchType' => $payload->searchType, + 'noSystemColumns' => $payload->noSystemColumns, + 'noBrickColumns' => $payload->noBrickColumns, + ]; + + $result = $this->gridConfigResolver->resolve($payload->locale, $params, $payload->helperColumnsBag); + + $event = new GenericEvent($this, [ + 'data' => $result->jsonSerialize(), + 'request' => $this->requestStack->getCurrentRequest(), + 'config' => $this->config, + 'context' => 'get', + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_GRID_GET_COLUMN_CONFIG_PRE_SEND_DATA); + + return $event->getArgument('data'); + } +} diff --git a/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php b/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php new file mode 100644 index 00000000..7831c5aa --- /dev/null +++ b/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php @@ -0,0 +1,59 @@ +query->getString('id') ?: null, + objectId: is_numeric($request->query->get('objectId')) ? $request->query->getInt('objectId') : null, + name: $request->query->getString('name') ?: null, + type: $request->query->getString('type') ?: null, + types: $request->query->getString('types') ?: null, + gridtype: $request->query->getString('gridtype') ?: null, + gridConfigId: is_numeric($request->query->get('gridConfigId')) ? $request->query->getInt('gridConfigId') : null, + searchType: $request->query->getString('searchType') ?: null, + noSystemColumns: $request->query->getBoolean('no_system_columns'), + noBrickColumns: $request->query->getBoolean('no_brick_columns'), + locale: $request->getLocale(), + helperColumnsBag: Session::getSessionBag($request->getSession(), 'opendxp_gridconfig'), + ); + } +} diff --git a/src/Handler/DataObject/Helper/ImportUpload/ImportUploadHandler.php b/src/Handler/DataObject/Helper/ImportUpload/ImportUploadHandler.php new file mode 100644 index 00000000..cbba6cf6 --- /dev/null +++ b/src/Handler/DataObject/Helper/ImportUpload/ImportUploadHandler.php @@ -0,0 +1,38 @@ +fileContents); + $importId = str_replace('..', '', $payload->importId); + + $importFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/import_' . $importId; + $this->filesystem->dumpFile($importFile, $data); + + $importFileOriginal = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/import_' . $importId . '_original'; + $this->filesystem->dumpFile($importFileOriginal, $data); + } +} diff --git a/src/Handler/DataObject/Helper/ImportUpload/ImportUploadPayload.php b/src/Handler/DataObject/Helper/ImportUpload/ImportUploadPayload.php new file mode 100644 index 00000000..135776d8 --- /dev/null +++ b/src/Handler/DataObject/Helper/ImportUpload/ImportUploadPayload.php @@ -0,0 +1,41 @@ +files->get('Filedata'); + + return new static( + fileContents: $file !== null ? (file_get_contents($file->getPathname()) ?: '') : '', + importId: $request->request->getString('importId'), + ); + } +} diff --git a/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataHandler.php b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataHandler.php new file mode 100644 index 00000000..5043d936 --- /dev/null +++ b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataHandler.php @@ -0,0 +1,34 @@ +id); + if (!$object instanceof DataObject) { + throw new DataObjectNotFoundException($payload->id); + } + + return GridData\DataObject::getData($object, $payload->fields); + } +} diff --git a/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataPayload.php b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataPayload.php new file mode 100644 index 00000000..58396e7e --- /dev/null +++ b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataPayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + fields: $request->query->all('fields'), + ); + } +} diff --git a/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteHandler.php b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteHandler.php new file mode 100644 index 00000000..92e42c38 --- /dev/null +++ b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteHandler.php @@ -0,0 +1,86 @@ +userContext->getAdminUser(); + $object = DataObject::getById($payload->objectId); + if (!$object) { + throw new NotFoundHttpException(); + } + + if (!$object->isAllowed('list')) { + throw new AccessDeniedHttpException(); + } + + $class = DataObject\ClassDefinition::getById($payload->classId); + if (!$class) { + throw new BadRequestHttpException('class ' . $payload->classId . ' does not exist anymore'); + } + + $favourite = new GridConfigFavourite(); + $favourite->setOwnerId($adminUser->getId()); + $favourite->setClassId($payload->classId); + $favourite->setSearchType($payload->searchType); + $favourite->setType($payload->type); + + $specializedConfigs = false; + + try { + if ($payload->gridConfigId !== 0) { + $gridConfig = GridConfig::getById($payload->gridConfigId); + $favourite->setGridConfigId($gridConfig->getId()); + } + + $favourite->setObjectId($payload->objectId); + $favourite->save(); + + if ($payload->global) { + $favourite->setObjectId(0); + $favourite->save(); + } + + $count = Db::get()->fetchOne( + 'SELECT * FROM gridconfig_favourites WHERE ownerId = ? AND classId = ? AND searchType = ? AND objectId != ? AND objectId != 0 AND `type` != ?', + [$adminUser->getId(), $payload->classId, $payload->searchType, $payload->objectId, $payload->type] + ); + $specializedConfigs = $count > 0; + } catch (Exception) { + $favourite->delete(); + } + + return new MarkDataObjectGridConfigFavouriteResult($specializedConfigs); + } +} diff --git a/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouritePayload.php b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouritePayload.php new file mode 100644 index 00000000..511ac7c7 --- /dev/null +++ b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouritePayload.php @@ -0,0 +1,45 @@ +request->getInt('objectId'), + classId: $request->request->getString('classId') ?: null, + gridConfigId: $request->request->getInt('gridConfigId'), + searchType: $request->request->getString('searchType') ?: null, + global: (bool) $request->request->get('global'), + type: $request->request->getString('type') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteResult.php b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteResult.php new file mode 100644 index 00000000..27644033 --- /dev/null +++ b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteResult.php @@ -0,0 +1,25 @@ +} + */ + public function __invoke(PrepareHelperColumnConfigsPayload $payload): array + { + $helperColumns = []; + $newData = []; + + foreach ($payload->columns as $item) { + if (!empty($item->isOperator)) { + $itemKey = '#' . uniqid('', false); + $item->key = $itemKey; + $helperColumns[$itemKey] = $item; + } + $newData[] = $item; + } + + return [ + 'newData' => $newData, + 'helperColumns' => $helperColumns, + ]; + } +} diff --git a/src/Handler/DataObject/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php b/src/Handler/DataObject/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php new file mode 100644 index 00000000..0c431e04 --- /dev/null +++ b/src/Handler/DataObject/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php @@ -0,0 +1,35 @@ +request->getString('columns')) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigHandler.php b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigHandler.php new file mode 100644 index 00000000..4ab5f87c --- /dev/null +++ b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigHandler.php @@ -0,0 +1,106 @@ +userContext->getAdminUser(); + $object = DataObject::getById($payload->objectId); + if (!$object) { + throw new NotFoundHttpException(); + } + + if (!$object->isAllowed('list')) { + throw new AccessDeniedHttpException(); + } + + $gridConfigData = $payload->gridConfigData; + $metadata = $payload->metadata; + + $gridConfigData['opendxp_version'] = Version::getVersion(); + $gridConfigData['opendxp_revision'] = Version::getRevision(); + $gridConfigData['context'] = $payload->context; + unset($gridConfigData['settings']['isShared']); + + $gridConfigId = $metadata['gridConfigId'] ?? null; + $gridConfig = null; + if ($gridConfigId) { + $gridConfig = GridConfig::getById($gridConfigId); + } + + if ($gridConfig && $gridConfig->getOwnerId() !== $adminUser->getId() && !$adminUser->isAdmin()) { + throw new BadRequestHttpException("don't mess around with somebody elses configuration"); + } + + $this->gridColumnConfigService->updateGridConfigShares($gridConfig, $metadata ?? [], $adminUser, adminCanEditAll: true); + + if (!empty($metadata['setAsFavourite']) && $adminUser->isAdmin()) { + $this->gridColumnConfigService->updateGridConfigFavourites($gridConfig, $metadata, $adminUser, $payload->objectId); + } + + if (!$gridConfig) { + $gridConfig = new GridConfig(); + $gridConfig->setName(date('c')); + $gridConfig->setClassId($payload->classId); + $gridConfig->setSearchType($payload->searchType); + $gridConfig->setOwnerId($adminUser->getId()); + } + + if ($metadata) { + $gridConfig->setName(SecurityHelper::convertHtmlSpecialChars($metadata['gridConfigName'])); + $gridConfig->setDescription(SecurityHelper::convertHtmlSpecialChars($metadata['gridConfigDescription'])); + $gridConfig->setShareGlobally($metadata['shareGlobally'] && $adminUser->isAdmin()); + $gridConfig->setSetAsFavourite($metadata['setAsFavourite'] && $adminUser->isAdmin()); + $gridConfig->setSaveFilters($metadata['saveFilters'] ?? false); + } + + $gridConfig->setConfig(json_encode($gridConfigData)); + $gridConfig->save(); + + $availableConfigs = $this->gridColumnConfigService->getMyOwnColumnConfigs($adminUser->getId(), $payload->classId ?? '', $payload->searchType); + $sharedConfigs = $this->gridColumnConfigService->getSharedColumnConfigs($adminUser, $payload->classId ?? '', $payload->searchType); + + $settings = $this->gridColumnConfigService->getShareSettings($gridConfig->getId()); + $settings['gridConfigId'] = (int) $gridConfig->getId(); + $settings['gridConfigName'] = SecurityHelper::convertHtmlSpecialChars($gridConfig->getName()); + $settings['gridConfigDescription'] = SecurityHelper::convertHtmlSpecialChars($gridConfig->getDescription()); + $settings['shareGlobally'] = $gridConfig->isShareGlobally(); + $settings['setAsFavourite'] = $gridConfig->isSetAsFavourite(); + $settings['saveFilters'] = $gridConfig->isSaveFilters(); + $settings['isShared'] = $gridConfig->getOwnerId() !== $adminUser->getId() && !$adminUser->isAdmin(); + + return new SaveDataObjectGridColumnConfigResult($settings, $availableConfigs, $sharedConfigs); + } +} diff --git a/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigPayload.php b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigPayload.php new file mode 100644 index 00000000..87bd709f --- /dev/null +++ b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigPayload.php @@ -0,0 +1,47 @@ +request->getString('settings'); + + return new static( + objectId: $request->request->getInt('id'), + classId: $request->request->getString('class_id') ?: null, + context: $request->request->getString('context') ?: null, + searchType: $request->request->getString('searchType') ?: null, + gridConfigData: json_decode($request->request->getString('gridconfig'), true) ?? [], + metadata: $meta ? json_decode($meta, true) : null, + ); + } +} diff --git a/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigResult.php b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigResult.php new file mode 100644 index 00000000..3297791e --- /dev/null +++ b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigResult.php @@ -0,0 +1,27 @@ +id); + $brick->delete(); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickHandler.php b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickHandler.php new file mode 100644 index 00000000..f0ade614 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickHandler.php @@ -0,0 +1,43 @@ +id); + + if (!$objectBrick instanceof DataObject\Objectbrick\Definition) { + $errorMessage = ': Object-Brick with id [ ' . $payload->id . ' not found. ]'; + Logger::error($errorMessage); + + throw new NotFoundHttpException($errorMessage); + } + + return new ExportObjectBrickResult( + $objectBrick->getKey(), + DataObject\ClassDefinition\Service::generateObjectBrickJson($objectBrick), + ); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickPayload.php b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickPayload.php new file mode 100644 index 00000000..a0d97065 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickPayload.php @@ -0,0 +1,30 @@ +query->getString('id')); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickResult.php b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickResult.php new file mode 100644 index 00000000..fbb29ff5 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickResult.php @@ -0,0 +1,26 @@ +classId); + $usages = []; + + foreach ((new DataObject\Objectbrick\Definition\Listing())->load() as $brickDefinition) { + foreach ($brickDefinition->getClassDefinitions() as $class) { + if ($myClass->getName() == $class['classname']) { + $usages[] = [ + 'objectbrick' => $brickDefinition->getKey(), + 'field' => $class['fieldname'], + ]; + } + } + } + + return new BrickUsagesResult($usages); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetBrickUsages/GetBrickUsagesPayload.php b/src/Handler/DataObject/ObjectBrick/GetBrickUsages/GetBrickUsagesPayload.php new file mode 100644 index 00000000..9d84db6c --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetBrickUsages/GetBrickUsagesPayload.php @@ -0,0 +1,30 @@ +query->getString('classId')); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickHandler.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickHandler.php new file mode 100644 index 00000000..88f38f1a --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickHandler.php @@ -0,0 +1,31 @@ +id); + + return new GetObjectBrickResult($brick->getObjectVars(), $brick->isWritable()); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickPayload.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickPayload.php new file mode 100644 index 00000000..630d68c2 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickPayload.php @@ -0,0 +1,30 @@ +query->getString('id')); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickResult.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickResult.php new file mode 100644 index 00000000..f7885f42 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickResult.php @@ -0,0 +1,26 @@ +classId; + $fieldName = $payload->fieldName; + $layoutId = $payload->layoutId; + $objectId = $payload->objectId; + $adminUser = $this->userContext->getAdminUser(); + $list = (new DataObject\Objectbrick\Definition\Listing())->load(); + + if ($classId !== null && $fieldName !== null) { + $filteredList = []; + $className = DataObject\ClassDefinition::getById($classId)->getName(); + + foreach ($list as $type) { + $clsDefs = $type->getClassDefinitions(); + if (!empty($clsDefs)) { + foreach ($clsDefs as $cd) { + if ($cd['classname'] == $className && $cd['fieldname'] == $fieldName) { + $filteredList[] = $type; + break; + } + } + } + + $layout = $type->getLayoutDefinitions(); + if ($layoutId == -1 && $adminUser->isAdmin()) { + DataObject\Service::createSuperLayout($layout); + } + + $context = [ + 'containerType' => 'objectbrick', + 'containerKey' => $type->getKey(), + 'outerFieldname' => $fieldName, + ]; + + $object = DataObject\Concrete::getById($objectId); + DataObject\Service::enrichLayoutDefinition($layout, $object, $context); + $type->setLayoutDefinitions($layout); + } + + $list = $filteredList; + } + + $event = new GenericEvent(null, ['list' => $list, 'objectId' => $objectId]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_LIST_PRE_SEND_DATA); + + return new ObjectBrickListResult($event->getArgument('list')); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/GetObjectBrickListPayload.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/GetObjectBrickListPayload.php new file mode 100644 index 00000000..0849790b --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/GetObjectBrickListPayload.php @@ -0,0 +1,40 @@ +query->getString('class_id') ?: null, + fieldName: $request->query->getString('field_name') ?: null, + layoutId: $request->query->getString('layoutId') ?: null, + objectId: $request->query->getInt('object_id'), + ); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/ObjectBrickListResult.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/ObjectBrickListResult.php new file mode 100644 index 00000000..a4e31ae9 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/ObjectBrickListResult.php @@ -0,0 +1,25 @@ +forObjectEditor; + $objectId = $payload->objectId; + $classId = $payload->classId; + $fieldName = $payload->fieldName; + $layoutId = $payload->layoutId; + $adminUser = $this->userContext->getAdminUser(); + $list = (new DataObject\Objectbrick\Definition\Listing())->load(); + + $layoutDefinitions = []; + $groups = []; + $definitions = []; + $object = $objectId > 0 ? DataObject\Concrete::getById($objectId) : null; + + $className = null; + $fieldname = null; + + if ($classId !== null && $fieldName !== null) { + $fieldname = $fieldName; + $className = DataObject\ClassDefinition::getById($classId)->getName(); + } + + foreach ($list as $item) { + $context = []; + if ($forObjectEditor) { + $context = [ + 'containerType' => 'objectbrick', + 'containerKey' => $item->getKey(), + 'outerFieldname' => $fieldname, + ]; + } + + if ($className !== null && $fieldname !== null) { + $keep = false; + foreach ($item->getClassDefinitions() as $cd) { + if ($cd['classname'] == $className && $cd['fieldname'] == $fieldname) { + $keep = true; + break; + } + } + if (!$keep) { + continue; + } + } + + $nodeData = [ + 'id' => $item->getKey(), + 'text' => $item->getKey(), + 'title' => $item->getTitle(), + 'key' => $item->getKey(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_objectbricks', + ]; + + if ($item->getGroup()) { + if (!isset($groups[$item->getGroup()])) { + $groups[$item->getGroup()] = [ + 'id' => 'group_' . $item->getKey(), + 'text' => htmlspecialchars($item->getGroup()), + 'expandable' => true, + 'leaf' => false, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'group' => $item->getGroup(), + 'children' => [], + ]; + } + if ($forObjectEditor) { + $itemLayoutDefinitions = null; + if ($layoutId) { + $layout = DataObject\ClassDefinition\CustomLayout::getById($layoutId . '.brick.' . $item->getKey()); + if ($layout instanceof DataObject\ClassDefinition\CustomLayout) { + $itemLayoutDefinitions = $layout->getLayoutDefinitions(); + } + } + if (!$itemLayoutDefinitions instanceof DataObject\ClassDefinition\Layout) { + $itemLayoutDefinitions = $item->getLayoutDefinitions(); + } + DataObject\Service::enrichLayoutDefinition($itemLayoutDefinitions, $object, $context); + $layoutDefinitions[$item->getKey()] = $itemLayoutDefinitions; + } + $groups[$item->getGroup()]['children'][] = $nodeData; + } else { + if ($forObjectEditor) { + $layout = $item->getLayoutDefinitions(); + if ($layoutId == -1 && $adminUser->isAdmin()) { + DataObject\Service::createSuperLayout($layout); + } elseif ($layoutId) { + $customLayout = DataObject\ClassDefinition\CustomLayout::getById($layoutId . '.brick.' . $item->getKey()); + if ($customLayout instanceof DataObject\ClassDefinition\CustomLayout) { + $layout = $customLayout->getLayoutDefinitions(); + } + } + DataObject\Service::enrichLayoutDefinition($layout, $object, $context); + $layoutDefinitions[$item->getKey()] = $layout; + } + $definitions[] = $nodeData; + } + } + + foreach ($groups as $group) { + $definitions[] = $group; + } + + $event = new GenericEvent(null, [ + 'list' => $definitions, + 'objectId' => $objectId, + 'forObjectEditor' => $forObjectEditor, + 'layoutDefinitions' => $layoutDefinitions, + 'fieldName' => $fieldName, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_LIST_PRE_SEND_DATA); + + return new ObjectBrickTreeResult( + $event->getArgument('list'), + $event->getArgument('layoutDefinitions'), + ); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/GetObjectBrickTreePayload.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/GetObjectBrickTreePayload.php new file mode 100644 index 00000000..20d40c72 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/GetObjectBrickTreePayload.php @@ -0,0 +1,42 @@ +query->getString('forObjectEditor'), + objectId: $request->query->getInt('object_id'), + classId: $request->query->getString('class_id') ?: null, + fieldName: $request->query->getString('field_name') ?: null, + layoutId: $request->query->getString('layoutId') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/ObjectBrickTreeResult.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/ObjectBrickTreeResult.php new file mode 100644 index 00000000..810873d8 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/ObjectBrickTreeResult.php @@ -0,0 +1,26 @@ +id); + + if (!DataObject\ClassDefinition\Service::importObjectBrickFromJson($objectBrick, $payload->json)) { + throw new RuntimeException('Failed to import objectbrick: ' . $payload->id); + } + } +} diff --git a/src/Handler/DataObject/ObjectBrick/ImportObjectBrick/ImportObjectBrickPayload.php b/src/Handler/DataObject/ObjectBrick/ImportObjectBrick/ImportObjectBrickPayload.php new file mode 100644 index 00000000..1856d5cc --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/ImportObjectBrick/ImportObjectBrickPayload.php @@ -0,0 +1,40 @@ +files->get('Filedata'); + + return new static( + id: $request->query->getString('id'), + json: file_get_contents($file->getPathname()), + ); + } +} diff --git a/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickHandler.php b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickHandler.php new file mode 100644 index 00000000..28e40eca --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickHandler.php @@ -0,0 +1,76 @@ +key; + $title = $payload->title; + $group = $payload->group; + $isAdd = $payload->isAdd; + $values = $payload->values; + $configuration = $payload->configuration; + + if ($isAdd) { + $list = new DataObject\Objectbrick\Definition\Listing(); + foreach ($list->loadNames() as $brickName) { + if (strtolower($key) === strtolower($brickName)) { + throw new BadRequestHttpException('Brick with the same name already exists (lower/upper cases may be different)'); + } + } + } + + $brickDef = new DataObject\Objectbrick\Definition(); + $brickDef->setKey($key); + $brickDef->setTitle($title); + $brickDef->setGroup($group); + + if ($values !== null) { + $brickDef->setParentClass($values['parentClass']); + $brickDef->setImplementsInterfaces($values['implementsInterfaces']); + $brickDef->setClassDefinitions($values['classDefinitions']); + } + + if ($configuration !== null) { + $configuration['datatype'] = 'layout'; + $configuration['fieldtype'] = 'panel'; + + $layout = DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($configuration, true); + $brickDef->setLayoutDefinitions($layout); + } + + $event = new GenericEvent(null, ['brickDefinition' => $brickDef]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_UPDATE_DEFINITION); + $brickDef = $event->getArgument('brickDefinition'); + + $brickDef->save(); + + return $brickDef; + } +} diff --git a/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickPayload.php b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickPayload.php new file mode 100644 index 00000000..fd61f445 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickPayload.php @@ -0,0 +1,44 @@ +request->getString('key'), + title: $request->request->getString('title'), + group: $request->request->getString('group'), + isAdd: $request->request->getString('task') === 'add', + values: $request->request->has('values') ? (json_decode($request->request->getString('values'), true) ?? null) : null, + configuration: $request->request->has('configuration') ? (json_decode($request->request->getString('configuration'), true) ?? null) : null, + ); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesHandler.php b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesHandler.php new file mode 100644 index 00000000..c40311e8 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesHandler.php @@ -0,0 +1,56 @@ +unitId); + if (!$fromUnit instanceof Unit) { + throw new BadRequestHttpException('Invalid unit ID provided'); + } + + $baseUnit = $fromUnit->getBaseunit() ?? $fromUnit; + + $units = new Unit\Listing(); + $units->setCondition('baseunit = ' . $units->quote($baseUnit->getId()) . ' AND id != ' . $units->quote($fromUnit->getId())); + + $convertedValues = []; + foreach ($units->getUnits() as $targetUnit) { + $convertedValue = $this->conversionService->convert(new QuantityValue($payload->value, $fromUnit), $targetUnit); + $convertedValues[] = [ + 'unit' => $targetUnit->getAbbreviation(), + 'unitName' => $targetUnit->getLongname(), + 'value' => round($convertedValue->getValue(), 4), + ]; + } + + return new ConvertAllQuantityValuesResult($payload->value, $fromUnit->getAbbreviation(), $convertedValues); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesPayload.php b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesPayload.php new file mode 100644 index 00000000..f4be0a51 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesPayload.php @@ -0,0 +1,36 @@ +query->getString('unit') ?: null, + value: $request->query->getString('value') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesResult.php b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesResult.php new file mode 100644 index 00000000..91416a3f --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesResult.php @@ -0,0 +1,27 @@ +fromUnitId); + $toUnit = Unit::getById($payload->toUnitId); + + if (!$fromUnit instanceof Unit || !$toUnit instanceof Unit) { + throw new BadRequestHttpException('Invalid unit IDs provided'); + } + + $convertedValue = $this->conversionService->convert(new QuantityValue($payload->value, $fromUnit), $toUnit); + + return new ConvertQuantityValueResult($convertedValue->getValue()); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValuePayload.php b/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValuePayload.php new file mode 100644 index 00000000..64e56b6d --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValuePayload.php @@ -0,0 +1,38 @@ +query->getString('fromUnit') ?: null, + toUnitId: $request->query->getString('toUnit') ?: null, + value: $request->query->getString('value') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValueResult.php b/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValueResult.php new file mode 100644 index 00000000..b8aefd8e --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValueResult.php @@ -0,0 +1,25 @@ +data; + + if (isset($data['baseunit']) && $data['baseunit'] === -1) { + $data['baseunit'] = null; + } + + $id = $data['id']; + + if (Unit::getById($id)) { + throw new BadRequestHttpException('unit with ID [' . $id . '] already exists'); + } + + if (mb_strlen($id) > 50) { + throw new BadRequestHttpException('The maximal character length for the unit ID is 50 characters, the provided ID has ' . mb_strlen($id) . ' characters.'); + } + + $unit = new Unit(); + $unit->setValues($data); + $unit->save(); + + return new ManageQuantityValueUnitResult($unit->getObjectVars()); + } +} diff --git a/src/Handler/DataObject/QuantityValue/DeleteQuantityValueUnit/DeleteQuantityValueUnitHandler.php b/src/Handler/DataObject/QuantityValue/DeleteQuantityValueUnit/DeleteQuantityValueUnitHandler.php new file mode 100644 index 00000000..7ddea0f9 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/DeleteQuantityValueUnit/DeleteQuantityValueUnitHandler.php @@ -0,0 +1,38 @@ +data['id']); + if (!$unit) { + throw new NotFoundHttpException('Unit with id ' . $payload->data['id'] . ' not found.'); + } + + $unit->delete(); + + return new ManageQuantityValueUnitResult([]); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ExportQuantityValueUnits/ExportQuantityValueUnitsHandler.php b/src/Handler/DataObject/QuantityValue/ExportQuantityValueUnits/ExportQuantityValueUnitsHandler.php new file mode 100644 index 00000000..924f59ff --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ExportQuantityValueUnits/ExportQuantityValueUnitsHandler.php @@ -0,0 +1,30 @@ +service->generateDefinitionJson(); + } +} diff --git a/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListHandler.php b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListHandler.php new file mode 100644 index 00000000..cb499932 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListHandler.php @@ -0,0 +1,62 @@ +filter; + $list = new Unit\Listing(); + $list->setOrderKey(['baseunit', 'factor', 'abbreviation']); + $list->setOrder(['ASC', 'ASC', 'ASC']); + + if ($filter) { + $array = explode(',', $filter); + $quotedArray = []; + $db = Db::get(); + foreach ($array as $a) { + $quotedArray[] = $db->quote($a); + } + $list->setCondition('id IN (' . implode(',', $quotedArray) . ')'); + } + + $result = []; + foreach ($list->getUnits() as $unit) { + try { + if ($unit->getAbbreviation()) { + $unit->setAbbreviation(Translation::getByKeyLocalized($unit->getAbbreviation(), Translation::DOMAIN_ADMIN, true, true)); + } + if ($unit->getLongname()) { + $unit->setLongname(Translation::getByKeyLocalized($unit->getLongname(), Translation::DOMAIN_ADMIN, true, true)); + } + $result[] = $unit->getObjectVars(); + } catch (Exception) { + // nothing to do + } + } + + return new GetQuantityValueUnitListResult($result, $list->getTotalCount()); + } +} diff --git a/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListPayload.php b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListPayload.php new file mode 100644 index 00000000..36c005ce --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListPayload.php @@ -0,0 +1,30 @@ +query->getString('filter') ?: null); + } +} diff --git a/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListResult.php b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListResult.php new file mode 100644 index 00000000..00c25e82 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListResult.php @@ -0,0 +1,26 @@ +queryAll; + $limit = $payload->limit; + $start = $payload->start; + $filter = $payload->filter; + $list = new Unit\Listing(); + + $order = ['ASC', 'ASC', 'ASC']; + $orderKey = ['baseunit', 'factor', 'abbreviation']; + + $sortingSettings = QueryParams::extractSortingSettings($queryAll); + + if ($sortingSettings['orderKey']) { + array_unshift($orderKey, $sortingSettings['orderKey']); + } + if ($sortingSettings['order']) { + array_unshift($order, $sortingSettings['order']); + } + + $list->setOrder($order); + $list->setOrderKey($orderKey); + $list->setLimit($limit); + $list->setOffset($start); + + if ($filter) { + $condition = '1 = 1'; + $filters = json_decode($filter); + $db = Db::get(); + foreach ($filters as $f) { + if ($f->type === 'string') { + $condition .= ' AND ' . $db->quoteIdentifier($f->property) . ' LIKE ' . $db->quote('%' . $f->value . '%'); + } elseif ($f->type === 'numeric') { + $condition .= ' AND ' . $db->quoteIdentifier($f->property) . ' ' . $this->getOperator($f->comparison) . ' ' . $db->quote($f->value); + } + } + $list->setCondition($condition); + } + + $units = []; + foreach ($list->getUnits() as $u) { + $units[] = $u->getObjectVars(); + } + + return new GetQuantityValueUnitsResult($units, $list->getTotalCount()); + } + + private function getOperator(string $comparison): string + { + return match ($comparison) { + 'lt' => '<', + 'gt' => '>', + default => '=', + }; + } +} diff --git a/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsPayload.php b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsPayload.php new file mode 100644 index 00000000..91268205 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsPayload.php @@ -0,0 +1,40 @@ +query->all(), + limit: $request->query->getInt('limit', 25), + start: $request->query->getInt('start', 0), + filter: $request->query->getString('filter') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsResult.php b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsResult.php new file mode 100644 index 00000000..b6e5d47f --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsResult.php @@ -0,0 +1,26 @@ +service->importDefinitionFromJson($payload->json); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php b/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php new file mode 100644 index 00000000..7e436380 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php @@ -0,0 +1,34 @@ +files->get('Filedata'); + + return new static(json: file_get_contents($file->getPathname())); + } +} diff --git a/src/Handler/DataObject/QuantityValue/ManageQuantityValueUnitResult.php b/src/Handler/DataObject/QuantityValue/ManageQuantityValueUnitResult.php new file mode 100644 index 00000000..78e5d595 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ManageQuantityValueUnitResult.php @@ -0,0 +1,25 @@ +request->getString('data'), true) ?? []); + } +} diff --git a/src/Handler/DataObject/QuantityValue/UpdateQuantityValueUnit/UpdateQuantityValueUnitHandler.php b/src/Handler/DataObject/QuantityValue/UpdateQuantityValueUnit/UpdateQuantityValueUnitHandler.php new file mode 100644 index 00000000..8afb337d --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/UpdateQuantityValueUnit/UpdateQuantityValueUnitHandler.php @@ -0,0 +1,45 @@ +data; + + $unit = Unit::getById($data['id']); + if (!$unit) { + throw new NotFoundHttpException('Unit with id ' . $data['id'] . ' not found.'); + } + + if (($data['baseunit'] ?? null) == -1) { + $data['baseunit'] = null; + } + + $unit->setValues($data); + $unit->save(); + + return new ManageQuantityValueUnitResult($unit->getObjectVars()); + } +} diff --git a/src/Handler/DataObject/SaveDataObject/SaveDataObjectHandler.php b/src/Handler/DataObject/SaveDataObject/SaveDataObjectHandler.php new file mode 100644 index 00000000..2d7992e6 --- /dev/null +++ b/src/Handler/DataObject/SaveDataObject/SaveDataObjectHandler.php @@ -0,0 +1,89 @@ +id); + if (!$objectFromDatabase instanceof DataObject\Concrete) { + throw new NotFoundHttpException('Could not find object'); + } + + $adminUser = $this->userContext->getAdminUser(); + $object = DataObjectVersionHelper::resolveLatestDraft($objectFromDatabase, $adminUser?->getId()); + $object->setUserModification($adminUser->getId()); + + $objectFromVersion = $object !== $objectFromDatabase; + if ($objectFromVersion) { + if (method_exists($object, 'getLocalizedFields')) { + /** @var DataObject\Localizedfield $localizedFields */ + $localizedFields = $object->getLocalizedFields(); + $localizedFields->setLoadedAllLazyData(); + } + + // Mark fields that have changed as dirty + if ($payload->task !== 'autoSave' && $payload->task !== 'unpublish') { + foreach ($object->getClass()->getFieldDefinitions() as $fieldName => $fieldDefinition) { + $getter = 'get' . ucfirst($fieldName); + $oldValue = $objectFromDatabase->$getter(); + $newValue = $object->$getter(); + $isEqual = $fieldDefinition instanceof EqualComparisonInterface + ? $fieldDefinition->isEqual($oldValue, $newValue) + : $oldValue === $newValue; + + if (!$isEqual) { + $object->markFieldDirty($fieldName); + } + } + } + } + + if (($payload->task === 'unpublish' && !$object->isAllowed('unpublish')) || ($payload->task === 'publish' && !$object->isAllowed('publish'))) { + throw new AccessDeniedHttpException(); + } + + $this->mapper->applyPayload($payload, $object, $objectFromDatabase); + + if ($payload->task === 'session') { + $this->sessionService->saveObject($object); + + return new SaveDataObjectResult(modificationDate: 0, versionDate: 0, versionCount: 0, treeData: [], draftData: []); + } + + return $this->coordinator->save($object, $payload->task); + } +} diff --git a/src/Handler/DataObject/SaveDataObject/SaveDataObjectPayload.php b/src/Handler/DataObject/SaveDataObject/SaveDataObjectPayload.php new file mode 100644 index 00000000..0c3eee6c --- /dev/null +++ b/src/Handler/DataObject/SaveDataObject/SaveDataObjectPayload.php @@ -0,0 +1,57 @@ +request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? []) + : []; + + $properties = $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? []) + : []; + + $scheduler = []; + if ($request->request->has('scheduler')) { + $raw = json_decode($request->request->getString('scheduler'), true); + $scheduler = !empty($raw) ? $raw : []; + } + + return new static( + id: $request->request->getInt('id'), + task: $request->query->getString('task'), + data: is_array($data) ? $data : [], + properties: is_array($properties) ? $properties : [], + scheduler: $scheduler, + ); + } +} diff --git a/src/Handler/DataObject/SaveDataObject/SaveDataObjectResult.php b/src/Handler/DataObject/SaveDataObject/SaveDataObjectResult.php new file mode 100644 index 00000000..c6208f64 --- /dev/null +++ b/src/Handler/DataObject/SaveDataObject/SaveDataObjectResult.php @@ -0,0 +1,29 @@ +userContext->getAdminUser(); + $id = $payload->id; + $general = $payload->general; + $propertiesData = $payload->propertiesData; + $object = DataObject::getById($id); + + if (!$object) { + throw new NotFoundHttpException(sprintf('DataObject with id %d not found', $id)); + } + + if (!$object->isAllowed('publish')) { + throw new AccessDeniedHttpException('Missing permission to publish object'); + } + + $object->setValues($general); + $object->setUserModification($adminUser->getId()); + + $this->applyProperties($object, $propertiesData); + + $object->save(); + } + + private function applyProperties(DataObject\AbstractObject $object, ?array $propertiesData): void + { + if ($propertiesData === null) { + return; + } + + $properties = []; + + // preserve inherited properties + foreach ($object->getProperties() as $p) { + if ($p->isInherited()) { + $properties[$p->getName()] = $p; + } + } + + foreach ($propertiesData as $propertyName => $propertyData) { + $value = $propertyData['data']; + + try { + $property = new Model\Property(); + $property->setType($propertyData['type']); + $property->setName($propertyName); + $property->setCtype('object'); + $property->setDataFromEditmode($value); + $property->setInheritable($propertyData['inheritable']); + + $properties[$propertyName] = $property; + } catch (Exception) { + Logger::err("Can't add " . $propertyName . ' to object ' . $object->getRealFullPath()); + } + } + + $object->setProperties($properties); + } +} diff --git a/src/Handler/DataObject/SaveDataObjectFolder/SaveDataObjectFolderPayload.php b/src/Handler/DataObject/SaveDataObjectFolder/SaveDataObjectFolderPayload.php new file mode 100644 index 00000000..c7e85ad2 --- /dev/null +++ b/src/Handler/DataObject/SaveDataObjectFolder/SaveDataObjectFolderPayload.php @@ -0,0 +1,43 @@ +request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + + return new static( + id: $request->request->getInt('id'), + general: json_decode($request->request->getString('general'), true) ?? [], + propertiesData: is_array($propertiesRaw) ? $propertiesRaw : null, + ); + } +} diff --git a/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdHandler.php b/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdHandler.php new file mode 100644 index 00000000..644ada8f --- /dev/null +++ b/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdHandler.php @@ -0,0 +1,181 @@ +node; + $filter = $payload->filter; + $start = $payload->start; + $limit = $payload->limit; + $view = $payload->view; + $fromPaging = $payload->fromPaging; + $requestQueryAll = $payload->allParams; + + $object = DataObject::getById($node); + $objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER]; + + if ($object instanceof DataObject\Concrete) { + $class = $object->getClass(); + if ($class->getShowVariants()) { + $objectTypes = DataObject::$types; + } + } + + $objects = []; + $offset = $total = $filteredTotalCount = 0; + + if ($object->hasChildren($objectTypes)) { + $offset = $start; + + $filterForCondition = $filter; + $effectiveLimit = $limit; + if (!is_null($filterForCondition)) { + if (!str_ends_with($filterForCondition, '*')) { + $filterForCondition .= '*'; + } + $filterForCondition = str_replace('*', '%', $filterForCondition); + $effectiveLimit = 100; + } + + $childrenList = new DataObject\Listing(); + $childrenList->setCondition($this->buildChildrenCondition($object, $filterForCondition, $view)); + $childrenList->setLimit($effectiveLimit); + $childrenList->setOffset($offset); + + if ($object->getChildrenSortBy() === 'index') { + $childrenList->setOrderKey('objects.index ASC', false); + } else { + $childrenList->setOrderKey( + sprintf( + 'CAST(objects.%s AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci %s', + $object->getChildrenSortBy(), $object->getChildrenSortOrder() + ), + false + ); + } + $childrenList->setObjectTypes($objectTypes); + + $cv = $view ? ($this->elementService->getCustomViewById($view) ?? []) : []; + Element\Service::addTreeFilterJoins($cv, $childrenList); + + $beforeListLoadEvent = new GenericEvent($this, [ + 'list' => $childrenList, + 'context' => $requestQueryAll, + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); + + /** @var DataObject\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + $result = ($this->childrenHandler)( + object: $object, + childrenList: $childrenList, + view: $view, + filter: $filter, + limit: $limit, + offset: $offset, + fromPaging: $fromPaging, + objectTypes: $objectTypes, + ); + + $objects = $result->objects; + $offset = $result->offset; + $limit = $result->limit; + $total = $result->total; + $filteredTotalCount = $result->filteredTotalCount; + $filter = $result->filter; + } + + $event = new GenericEvent($this, ['objects' => $objects]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); + $objects = $event->getArgument('objects'); + + return new GetDataObjectChildrenResult( + objects: $objects, + offset: $offset, + limit: $limit, + total: $total, + filteredTotalCount: $filteredTotalCount, + filter: $filter, + fromPaging: $fromPaging, + ); + } + + private function buildChildrenCondition(DataObject\AbstractObject $object, ?string $filter, ?string $view): string + { + $condition = "objects.parentId = '" . $object->getId() . "'"; + + if ($view) { + $cv = $this->elementService->getCustomViewById($view); + + if (!empty($cv['classes'])) { + $cvConditions = []; + $cvClasses = $cv['classes']; + foreach ($cvClasses as $key => $cvClass) { + $cvConditions[] = "objects.classId = '" . $key . "'"; + } + + $cvConditions[] = "objects.type = 'folder'"; + $condition .= ' AND (' . implode(' OR ', $cvConditions) . ')'; + } + } + + $adminUser = $this->userContext->getAdminUser(); + if (!$adminUser->isAdmin()) { + $userIds = $adminUser->getRoles(); + $currentUserId = $adminUser->getId(); + $userIds[] = $currentUserId; + + $inheritedPermission = $object->getDao()->isInheritingPermission('list', $userIds); + + $anyAllowedRowOrChildren = 'EXISTS(SELECT list FROM users_workspaces_object uwo WHERE userId IN (' . implode(',', $userIds) . ') AND list=1 AND LOCATE(CONCAT(objects.path,objects.key),cpath)=1 AND + NOT EXISTS(SELECT list FROM users_workspaces_object WHERE userId =' . $currentUserId . ' AND list=0 AND cpath = uwo.cpath))'; + $isDisallowedCurrentRow = 'EXISTS(SELECT list FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') AND cid = objects.id AND list=0)'; + + $condition .= ' AND IF(' . $anyAllowedRowOrChildren . ',1,IF(' . $inheritedPermission . ', ' . $isDisallowedCurrentRow . ' = 0, 0)) = 1'; + } + + if (!is_null($filter)) { + $db = Db::get(); + $condition .= ' AND CAST(objects.key AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ' . $db->quote($filter); + } + + return $condition; + } +} diff --git a/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdPayload.php b/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdPayload.php new file mode 100644 index 00000000..1469d646 --- /dev/null +++ b/src/Handler/DataObject/TreeGetChildrenById/TreeGetChildrenByIdPayload.php @@ -0,0 +1,48 @@ +query->getInt('node'), + filter: $request->query->getString('filter') ?: null, + start: $request->query->getInt('start'), + limit: $request->query->getInt('limit', 100000000), + view: $request->query->getString('view'), + fromPaging: $request->query->getInt('fromPaging'), + allParams: $request->query->all(), + inSearch: $request->query->getInt('inSearch'), + ); + } +} diff --git a/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectHandler.php b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectHandler.php new file mode 100644 index 00000000..72bf7587 --- /dev/null +++ b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectHandler.php @@ -0,0 +1,263 @@ +ids as $id) { + $result = $this->processOne($id, $payload->values); + } + + return $result ?? new UpdateDataObjectResult(treeData: []); + } + + private function processOne(int $id, array $values): UpdateDataObjectResult + { + $object = DataObject::getById($id); + if (!$object instanceof DataObject) { + throw new DataObjectNotFoundException($id); + } + + $adminUser = $this->userContext->getAdminUser(); + if ($object instanceof DataObject\Concrete) { + $object->setOmitMandatoryCheck(true); + } + + // this prevents the user from renaming, relocating (actions in the tree) if the newest version isn't the published one + // the reason is that otherwise the content of the newer not published version will be overwritten + if ($object instanceof DataObject\Concrete) { + $latestVersion = $object->getLatestVersion(); + if ($latestVersion && $latestVersion->getData()->getModificationDate() != $object->getModificationDate()) { + throw new RuntimeException("You can't rename or relocate if there's a newer not published version"); + } + } + + $key = $values['key'] ?? null; + if ($key) { + $key = Service::getValidKey($key, 'object'); + } + + if ($object->isAllowed('settings')) { + if ($key) { + if ($object->isAllowed('rename')) { + $object->setKey($key); + } elseif ($key !== $object->getKey()) { + Logger::debug('prevented renaming object because of missing permissions '); + } + } + + if (!empty($values['parentId'])) { + $parent = DataObject::getById((int) $values['parentId']); + + //check if parent is changed + if ($object->getParentId() !== $parent->getId()) { + if (!$parent->isAllowed('create')) { + throw new AccessDeniedHttpException('Prevented moving object - no create permission on new parent '); + } + + $objectWithSamePath = DataObject::getByPath($parent->getRealFullPath() . '/' . $object->getKey()); + + if ($objectWithSamePath != null) { + throw new RuntimeException('prevented creating object because object with same path+key already exists'); + } + + if ($object->isLocked()) { + throw new RuntimeException('prevented moving object, because it is locked: ID: ' . $object->getId()); + } + + $object->setParentId($values['parentId']); + } + } + + if (array_key_exists('locked', $values)) { + $object->setLocked($values['locked']); + } + + $object->setModificationDate(time()); + $object->setUserModification($adminUser->getId()); + + $isIndexUpdate = isset($values['indices']); + + if ($isIndexUpdate) { + // Ensure the update sort index is already available in the postUpdate eventListener + $indexUpdate = is_int($values['indices']) ? $values['indices'] : $values['indices'][$object->getId()]; + $object->setIndex($indexUpdate); + } + + $object->save(); + + if ($isIndexUpdate) { + $this->updateIndexesOfObjectSiblings($object, $indexUpdate); + } + + return new UpdateDataObjectResult( + treeData: $this->elementService->getElementTreeNodeConfig($object), + ); + } + + if ($key && $object->isAllowed('rename')) { + $result = $this->dataObjectGridService->renameObject($object, $key); + if (!$result['success']) { + throw new RuntimeException($result['message'] ?? 'Failed to rename object'); + } + + return new UpdateDataObjectResult( + treeData: $this->elementService->getElementTreeNodeConfig($object), + ); + } + + Logger::debug('prevented update object because of missing permissions.'); + + // Return current tree data even when no changes were applied + return new UpdateDataObjectResult( + treeData: $this->elementService->getElementTreeNodeConfig($object), + ); + } + + private function updateIndexesOfObjectSiblings(DataObject\AbstractObject $updatedObject, int $newIndex): void + { + $fn = function () use ($updatedObject, $newIndex): void { + $list = new DataObject\Listing(); + $updatedObject->saveIndex($newIndex); + + // The cte and the limit are needed to order the data before the newIndex is set + $db = Db::get(); + $db->executeStatement( + 'UPDATE ' . $list->getDao()->getTableName() . ' o, + ( + SELECT newIndex, id + FROM ( + With cte As (SELECT `index`, id FROM ' . $list->getDao()->getTableName() . ' WHERE parentId = ? AND id != ? AND `type` IN (\'' . implode( + "','", [ + DataObject::OBJECT_TYPE_OBJECT, + DataObject::OBJECT_TYPE_VARIANT, + DataObject::OBJECT_TYPE_FOLDER, + ] + ) . '\') ORDER BY `index` LIMIT ' . $updatedObject->getParent()->getChildAmount([ + DataObject::OBJECT_TYPE_OBJECT, + DataObject::OBJECT_TYPE_VARIANT, + DataObject::OBJECT_TYPE_FOLDER, + ]) . ') + SELECT @n := IF(@n = ? - 1,@n + 2,@n + 1) AS newIndex, id + FROM cte, + (SELECT @n := -1) variable + ) tmp + ) order_table + SET o.index = order_table.newIndex + WHERE o.id=order_table.id', + [ + $updatedObject->getParentId(), + $updatedObject->getId(), + $newIndex, + ] + ); + + $siblings = $db->fetchAllAssociative( + 'SELECT id, modificationDate, versionCount, `key`, `index` FROM objects + WHERE parentId = ? AND id != ? AND `type` IN ("object", "variant", "folder") ORDER BY `index` ASC', + [$updatedObject->getParentId(), $updatedObject->getId()] + ); + $index = 0; + + foreach ($siblings as $sibling) { + if ($index === $newIndex) { + $index++; + } + + $this->updateLatestVersionIndex($sibling['id'], $index); + $index++; + + DataObject::clearDependentCacheByObjectId($sibling['id']); + } + }; + + $this->executeInsideTransaction($fn); + } + + private function updateLatestVersionIndex(int $objectId, int $newIndex): void + { + $object = DataObject\Concrete::getById($objectId); + + if ( + $object && + $object->getType() !== DataObject::OBJECT_TYPE_FOLDER && + $latestVersion = $object->getLatestVersion() + ) { + // don't renew references (which means loading the target elements) + // Not needed as we just save a new version with the updated index + $object = $latestVersion->loadData(false); + if ($newIndex !== $object->getIndex()) { + $object->setIndex($newIndex); + } + $latestVersion->save(); + } + } + + private function executeInsideTransaction(callable $fn): void + { + $maxRetries = 5; + for ($retries = 0; $retries < $maxRetries; $retries++) { + try { + Db::get()->beginTransaction(); + + $fn(); + + Db::get()->commit(); + + break; + } catch (Exception $e) { + Db::get()->rollBack(); + + // we try to start the transaction $maxRetries times again (deadlocks, ...) + if ($retries < ($maxRetries - 1)) { + $run = $retries + 1; + $waitTime = random_int(1, 5) * 100000; // microseconds + Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')'); + + usleep($waitTime); // wait specified time until we restart the transaction + } else { + // if the transaction still fail after $maxRetries retries, we throw out the exception + Logger::error('Finally giving up restarting the same transaction again and again, last message: ' . $e->getMessage()); + + throw $e; + } + } + } + } +} diff --git a/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectPayload.php b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectPayload.php new file mode 100644 index 00000000..b4d4989e --- /dev/null +++ b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectPayload.php @@ -0,0 +1,39 @@ +request->getString('id'), true); + + return new static( + ids: is_array($rawIds) ? array_map('intval', $rawIds) : [(int) ($rawIds ?? 0)], + values: json_decode($request->request->getString('values'), true) ?? [], + ); + } +} diff --git a/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectResult.php b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectResult.php new file mode 100644 index 00000000..59c7d479 --- /dev/null +++ b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectResult.php @@ -0,0 +1,25 @@ +objectId; + $allParams = $payload->allParams; + $requestedLanguage = $payload->requestedLanguage; + + $parentObject = DataObject\Concrete::getById($parentObjectId); + if (!$parentObject) { + throw new NotFoundHttpException('No Object found with id ' . $parentObjectId); + } + + if (!$parentObject->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + $allParams['folderId'] = $parentObject->getId(); + $allParams['classId'] = $parentObject->getClassId(); + + return new GetVariantsResult( + $this->dataObjectGridService->gridProxy($allParams, DataObject::OBJECT_TYPE_VARIANT, $requestedLanguage) + ); + } +} diff --git a/src/Handler/DataObject/Variants/GetVariants/GetVariantsPayload.php b/src/Handler/DataObject/Variants/GetVariants/GetVariantsPayload.php new file mode 100644 index 00000000..f4c1ea73 --- /dev/null +++ b/src/Handler/DataObject/Variants/GetVariants/GetVariantsPayload.php @@ -0,0 +1,44 @@ +request->all(), ...$request->query->all()]; + $languageFromParams = $request->request->getString('language'); + $requestedLanguage = ($languageFromParams !== '' && $languageFromParams !== 'default') + ? $languageFromParams + : $request->getLocale(); + + return new static( + objectId: $request->request->getInt('objectId'), + allParams: $allParams, + requestedLanguage: $requestedLanguage, + ); + } +} diff --git a/src/Handler/DataObject/Variants/GetVariants/GetVariantsResult.php b/src/Handler/DataObject/Variants/GetVariants/GetVariantsResult.php new file mode 100644 index 00000000..0c0a86f9 --- /dev/null +++ b/src/Handler/DataObject/Variants/GetVariants/GetVariantsResult.php @@ -0,0 +1,25 @@ +id; + $key = $payload->key; + + $object = DataObject\Concrete::getById($id); + + if (!$object) { + throw new NotFoundHttpException('No Object found for given id.'); + } + + return new UpdateObjectKeyResult( + $this->dataObjectGridService->renameObject($object, $key), + ); + } +} diff --git a/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyPayload.php b/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyPayload.php new file mode 100644 index 00000000..6dec5bb5 --- /dev/null +++ b/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyPayload.php @@ -0,0 +1,36 @@ +request->getInt('id'), + key: $request->request->getString('key'), + ); + } +} diff --git a/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyResult.php b/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyResult.php new file mode 100644 index 00000000..1d330543 --- /dev/null +++ b/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyResult.php @@ -0,0 +1,25 @@ +from); + $object1 = $version1?->loadData(); + + if (!$object1 instanceof DataObject\AbstractObject) { + DataObject::setDoNotRestoreKeyAndPath(false); + throw new DataObjectNotFoundException($payload->from); + } + + if (method_exists($object1, 'getLocalizedFields')) { + /** @var DataObject\Localizedfield $localizedFields1 */ + $localizedFields1 = $object1->getLocalizedFields(); + $localizedFields1->setLoadedAllLazyData(); + } + + $version2 = Version::getById($payload->to); + $object2 = $version2?->loadData(); + + if (!$object2 instanceof DataObject\AbstractObject) { + DataObject::setDoNotRestoreKeyAndPath(false); + throw new DataObjectNotFoundException($payload->to); + } + + if (method_exists($object2, 'getLocalizedFields')) { + /** @var DataObject\Localizedfield $localizedFields2 */ + $localizedFields2 = $object2->getLocalizedFields(); + $localizedFields2->setLoadedAllLazyData(); + } + + DataObject::setDoNotRestoreKeyAndPath(false); + + if (!$object1->isAllowed('versions') || !$object2->isAllowed('versions')) { + throw new AccessDeniedHttpException('Permission denied for version ids [' . $payload->from . ', ' . $payload->to . ']'); + } + + return new DiffVersionsResult($object1, $version1, $object2, $version2); + } +} diff --git a/src/Handler/DataObject/Version/DiffVersions/DiffVersionsPayload.php b/src/Handler/DataObject/Version/DiffVersions/DiffVersionsPayload.php new file mode 100644 index 00000000..3b611bdd --- /dev/null +++ b/src/Handler/DataObject/Version/DiffVersions/DiffVersionsPayload.php @@ -0,0 +1,38 @@ +attributes->getInt('from'), + to: $request->attributes->getInt('to'), + userTimezone: $request->query->getString('userTimezone') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Version/DiffVersions/DiffVersionsResult.php b/src/Handler/DataObject/Version/DiffVersions/DiffVersionsResult.php new file mode 100644 index 00000000..d12277b2 --- /dev/null +++ b/src/Handler/DataObject/Version/DiffVersions/DiffVersionsResult.php @@ -0,0 +1,31 @@ +id); + $object = $version?->loadData(); + + if (!$object instanceof DataObject\AbstractObject) { + DataObject::setDoNotRestoreKeyAndPath(false); + throw new DataObjectNotFoundException($payload->id); + } + + if (method_exists($object, 'getLocalizedFields')) { + /** @var DataObject\Localizedfield $localizedFields */ + $localizedFields = $object->getLocalizedFields(); + $localizedFields->setLoadedAllLazyData(); + } + + DataObject::setDoNotRestoreKeyAndPath(false); + + if (!$object->isAllowed('versions')) { + throw new AccessDeniedHttpException('Permission denied for version id [' . $payload->id . ']'); + } + + return new PreviewVersionResult($object, $version); + } +} diff --git a/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionPayload.php b/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionPayload.php new file mode 100644 index 00000000..107014a0 --- /dev/null +++ b/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionPayload.php @@ -0,0 +1,38 @@ +query->getInt('id'), + userTimezone: $request->query->getString('userTimezone') ?: null, + ); + } +} diff --git a/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionResult.php b/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionResult.php new file mode 100644 index 00000000..8e61903c --- /dev/null +++ b/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionResult.php @@ -0,0 +1,29 @@ +userContext->getAdminUser()?->getId() ?? 0; + $version = Version::getById($payload->id); + $object = $version?->loadData(); + + if (!$object instanceof DataObject\Concrete) { + throw new DataObjectNotFoundException($payload->id); + } + + $currentObject = DataObject::getById($object->getId()); + if (!$currentObject?->isAllowed('publish')) { + throw new AccessDeniedHttpException('Missing permission to publish object version'); + } + + $object->setPublished(true); + $object->setUserModification($userId); + $object->save(); + + $treeData = []; + $this->adminStyleEnricher->forTree($object, $treeData); + + return new PublishVersionResult( + modificationDate: (int) $object->getModificationDate(), + treeData: $treeData, + ); + } +} diff --git a/src/Handler/DataObject/Version/PublishVersion/PublishVersionResult.php b/src/Handler/DataObject/Version/PublishVersion/PublishVersionResult.php new file mode 100644 index 00000000..0df78dcd --- /dev/null +++ b/src/Handler/DataObject/Version/PublishVersion/PublishVersionResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $parentDocument = Document::getById($payload->parentId); + + if (!$parentDocument || !$parentDocument->isAllowed('create')) { + throw new AccessDeniedHttpException('Prevented adding a document because of missing permissions'); + } + + $intendedPath = $parentDocument->getRealFullPath() . '/' . $payload->key; + + if (Document\Service::pathExists($intendedPath)) { + throw new BadRequestHttpException( + sprintf('Prevented adding a document because document with same path+key [%s] already exists', $intendedPath) + ); + } + + $createValues = [ + 'userOwner' => $adminUser->getId(), + 'userModification' => $adminUser->getId(), + 'published' => false, + ]; + + $createValues['key'] = Service::getValidKey($payload->key, 'document'); + + // determine template / controller from docType or translationsBaseDocument + $docType = Document\DocType::getById($payload->docTypeId ?? ''); + + if ($docType) { + $createValues['template'] = $docType->getTemplate(); + $createValues['controller'] = $docType->getController(); + $createValues['staticGeneratorEnabled'] = $docType->getStaticGeneratorEnabled(); + } elseif ($payload->translationsBaseDocumentId !== null) { + $translationsBaseDocument = Document::getById((int) $payload->translationsBaseDocumentId); + if ($translationsBaseDocument instanceof Document\PageSnippet) { + $createValues['template'] = $translationsBaseDocument->getTemplate(); + $createValues['controller'] = $translationsBaseDocument->getController(); + } + } elseif (in_array($payload->type, ['page', 'snippet', 'email'])) { + $createValues['controller'] = $this->defaultDocumentController; + } + + if ($payload->inheritanceSource !== null) { + $createValues['contentMainDocumentId'] = $payload->inheritanceSource; + } + + $document = match ($payload->type) { + 'page' => $this->createPage($parentDocument, $createValues, $payload->title, $payload->name), + 'snippet' => Document\Snippet::create($parentDocument->getId(), $createValues), + 'email' => Document\Email::create($parentDocument->getId(), $createValues), + 'link' => Document\Link::create($parentDocument->getId(), $createValues), + 'hardlink' => Document\Hardlink::create($parentDocument->getId(), $createValues), + 'folder' => $this->createFolder($parentDocument, $createValues), + default => $this->createCustomType($payload->type, $parentDocument, $createValues), + }; + + // link translation if translationsBaseDocument given + if ($payload->translationsBaseDocumentId !== null) { + $translationsBaseDocument = Document::getById((int) $payload->translationsBaseDocumentId); + if ($translationsBaseDocument) { + $properties = $translationsBaseDocument->getProperties(); + $properties = [...$properties, ...$document->getProperties()]; + $document->setProperties($properties); + $document->setProperty('language', 'text', $payload->language, false, true); + $document->save(); + + $service = $this->serviceFactory->createDocumentService(); + $service->addTranslation($translationsBaseDocument, $document); + } + } + + return new AddDocumentResult($document); + } + + private function createPage(Document $parentDocument, array $createValues, ?string $title, ?string $name): Document\Page + { + $document = Document\Page::create($parentDocument->getId(), $createValues, false); + $document->setTitle((string) $title); + $document->setProperty('navigation_name', 'text', $name, false, false); + $document->save(); + + return $document; + } + + private function createFolder(Document $parentDocument, array $createValues): Document\Folder + { + $document = Document\Folder::create($parentDocument->getId(), $createValues); + $document->setPublished(true); + $document->save(); + + return $document; + } + + private function createCustomType(string $type, Document $parentDocument, array $createValues): Document + { + $classname = $this->documentClassResolver->resolve($type); + + if ($classname !== null && Tool::classExists($classname)) { + $document = $classname::create($parentDocument->getId(), $createValues); + $document->save(); + + return $document; + } + + Logger::debug("Unknown document type, can't add [ $type ] "); + + throw new BadRequestHttpException(sprintf("Unknown document type '%s'", $type)); + } +} diff --git a/src/Handler/Document/AddDocument/AddDocumentPayload.php b/src/Handler/Document/AddDocument/AddDocumentPayload.php new file mode 100644 index 00000000..6e499103 --- /dev/null +++ b/src/Handler/Document/AddDocument/AddDocumentPayload.php @@ -0,0 +1,38 @@ +request->getInt('parentId'), + type: $request->request->getString('type'), + key: $request->request->getString('key'), + docTypeId: $request->request->get('docTypeId'), + translationsBaseDocumentId: $request->request->get('translationsBaseDocument'), + language: $request->request->get('language'), + inheritanceSource: $request->request->has('inheritanceSource') ? $request->request->get('inheritanceSource') : null, + title: $request->request->get('title'), + name: $request->request->get('name'), + ); + } +} diff --git a/src/Handler/Document/AddDocument/AddDocumentResult.php b/src/Handler/Document/AddDocument/AddDocumentResult.php new file mode 100644 index 00000000..28056d40 --- /dev/null +++ b/src/Handler/Document/AddDocument/AddDocumentResult.php @@ -0,0 +1,26 @@ +id); + if (!$doc instanceof PageSnippet) { + return; + } + + $doc->setEditables([]); + $doc->setContentMainDocumentId($payload->contentMainDocumentPath, true); + $doc->saveVersion(); + } +} diff --git a/src/Handler/Document/ChangeMainDocument/ChangeMainDocumentPayload.php b/src/Handler/Document/ChangeMainDocument/ChangeMainDocumentPayload.php new file mode 100644 index 00000000..605dc1b3 --- /dev/null +++ b/src/Handler/Document/ChangeMainDocument/ChangeMainDocumentPayload.php @@ -0,0 +1,36 @@ +request->getInt('id'), + contentMainDocumentPath: $request->request->getString('contentMainDocumentPath'), + ); + } +} diff --git a/src/Handler/Document/ConvertDocument/ConvertDocumentHandler.php b/src/Handler/Document/ConvertDocument/ConvertDocumentHandler.php new file mode 100644 index 00000000..60434bd1 --- /dev/null +++ b/src/Handler/Document/ConvertDocument/ConvertDocumentHandler.php @@ -0,0 +1,61 @@ +id); + if (!$document) { + throw new NotFoundHttpException('Document not found'); + } + + $class = '\\OpenDxp\\Model\\Document\\' . ucfirst($payload->type); + if (!Tool::classExists($class)) { + return; + } + + $new = new $class; + + // overwrite internal store to avoid "duplicate full path" error + RuntimeCache::set('document_' . $document->getId(), $new); + + $props = $document->getObjectVars(); + foreach ($props as $name => $value) { + if (in_array($name, ['children', 'siblings', 'scheduledTasks', 'controller', 'template'])) { + continue; + } + $new->setValue($name, $value); + } + + if ($payload->type === 'hardlink' || $payload->type === 'folder') { + foreach (['name', 'title', 'target', 'exclude', 'class', 'anchor', 'parameters', 'relation', 'accesskey', 'tabindex'] as $propertyName) { + $new->removeProperty('navigation_' . $propertyName); + } + } + + $new->setType($payload->type); + $new->save(); + } +} diff --git a/src/Handler/Document/ConvertDocument/ConvertDocumentPayload.php b/src/Handler/Document/ConvertDocument/ConvertDocumentPayload.php new file mode 100644 index 00000000..d8ca4256 --- /dev/null +++ b/src/Handler/Document/ConvertDocument/ConvertDocumentPayload.php @@ -0,0 +1,36 @@ +request->getInt('id'), + type: $request->request->getString('type'), + ); + } +} diff --git a/src/Handler/Document/Copy/CopyDocument/CopyDocumentHandler.php b/src/Handler/Document/Copy/CopyDocument/CopyDocumentHandler.php new file mode 100644 index 00000000..e2e7d08d --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentHandler.php @@ -0,0 +1,87 @@ +userContext->getAdminUser()?->getId() ?? 0; + $source = Document::getById($payload->sourceId); + + if ($payload->sourceParentId !== null && $payload->targetParentId !== null) { + $sourceParent = Document::getById($payload->sourceParentId) ?? throw new NotFoundHttpException('Source parent not found'); + $resolvedTargetParentId = $payload->sessionParentId ?? $payload->targetParentId; + $targetParent = Document::getById($resolvedTargetParentId) ?? throw new NotFoundHttpException('Target parent not found'); + $targetPath = preg_replace('@^' . $sourceParent->getRealFullPath() . '@', $targetParent . '/', $source->getRealPath()); + $target = Document::getByPath($targetPath); + } else { + $target = Document::getById($payload->targetId); + } + + if (!$target instanceof Document) { + throw new NotFoundHttpException('Target document not found'); + } + + if (!$target->isAllowed('create')) { + Logger::error('could not execute copy/paste because of missing permissions on target [ ' . $payload->targetId . ' ]'); + throw new AccessDeniedHttpException(); + } + + if ($source === null) { + throw new NotFoundHttpException('Source document not found'); + } + + if ($source instanceof Document\PageSnippet && $latestVersion = $source->getLatestVersion()) { + $source = $latestVersion->loadData(); + $source->setPublished(false); + } + + $documentService = $this->serviceFactory->createDocumentService(); + + if ($payload->type === 'child') { + if ($payload->language !== null && !Tool::isValidLanguage($payload->language)) { + throw new BadRequestHttpException('Invalid language: ' . $payload->language); + } + + $newDocument = $documentService->copyAsChild($target, $source, $payload->enableInheritance, $payload->resetIndex, $payload->language); + + return new CopyDocumentResult($payload->sourceId, $newDocument); + } + + if ($payload->type === 'replace') { + $documentService->copyContents($target, $source); + } + + return new CopyDocumentResult($payload->sourceId); + } +} diff --git a/src/Handler/Document/Copy/CopyDocument/CopyDocumentPayload.php b/src/Handler/Document/Copy/CopyDocument/CopyDocumentPayload.php new file mode 100644 index 00000000..c2d90e99 --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentPayload.php @@ -0,0 +1,61 @@ +request->getString('transactionId'); + $sessionBag = Session::getSessionBag($request->getSession(), 'opendxp_copy')->get($transactionId) ?? []; + $hasTargetParentId = (bool) $request->request->getString('targetParentId'); + + return new static( + sourceId: $request->request->getInt('sourceId'), + targetId: $request->request->getInt('targetId'), + type: $request->request->getString('type'), + sourceParentId: $hasTargetParentId ? $request->request->getInt('sourceParentId') : null, + targetParentId: $hasTargetParentId ? $request->request->getInt('targetParentId') : null, + sessionParentId: !empty($sessionBag['parentId']) ? (int) $sessionBag['parentId'] : null, + enableInheritance: $request->request->getString('enableInheritance') === 'true', + resetIndex: $request->request->getString('resetIndex') === 'true', + language: $request->request->getString('language') ?: null, + transactionId: $transactionId, + saveParentId: (bool) $request->request->getString('saveParentId'), + sessionBag: $sessionBag, + ); + } +} diff --git a/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php b/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php new file mode 100644 index 00000000..2c512ab8 --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php @@ -0,0 +1,28 @@ +type === 'recursive' || $payload->type === 'recursive-update-references') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_document_document_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => 'child', + 'language' => $payload->language, + 'enableInheritance' => $payload->enableInheritance, + 'transactionId' => $transactionId, + 'saveParentId' => true, + 'resetIndex' => true, + ], + ]]; + + $document = Document::getById($payload->sourceId) ?? throw new DocumentNotFoundException($payload->sourceId); + $childIds = []; + + if ($document->hasChildren()) { + $list = new Document\Listing(); + $list->setCondition('`path` LIKE ?', [$list->escapeLike($document->getRealFullPath()) . '/%']); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('ASC'); + $childIds = $list->loadIdList(); + } + + foreach ($childIds as $id) { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_document_document_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $id, + 'targetParentId' => $payload->targetId, + 'sourceParentId' => $payload->sourceId, + 'type' => 'child', + 'language' => $payload->language, + 'enableInheritance' => $payload->enableInheritance, + 'transactionId' => $transactionId, + ], + ]]; + } + + if ($payload->type === 'recursive-update-references') { + for ($i = 0; $i < (count($childIds) + 1); $i++) { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_document_document_copyrewriteids'), + 'method' => 'PUT', + 'params' => [ + 'transactionId' => $transactionId, + 'enableInheritance' => $payload->enableInheritance, + '_dc' => uniqid('', false), + ], + ]]; + } + } + } elseif ($payload->type === 'child' || $payload->type === 'replace') { + $pasteJobs[] = [[ + 'url' => $this->router->generate('opendxp_admin_document_document_copy'), + 'method' => 'POST', + 'params' => [ + 'sourceId' => $payload->sourceId, + 'targetId' => $payload->targetId, + 'type' => $payload->type, + 'language' => $payload->language, + 'enableInheritance' => $payload->enableInheritance, + 'transactionId' => $transactionId, + 'resetIndex' => ($payload->type === 'child'), + ], + ]]; + } + + return new CopyInfoResult($transactionId, $pasteJobs); + } +} diff --git a/src/Handler/Document/Copy/CopyInfo/CopyInfoPayload.php b/src/Handler/Document/Copy/CopyInfo/CopyInfoPayload.php new file mode 100644 index 00000000..5104c2bf --- /dev/null +++ b/src/Handler/Document/Copy/CopyInfo/CopyInfoPayload.php @@ -0,0 +1,43 @@ +query->getString('type') ?: null, + sourceId: $request->query->getInt('sourceId'), + targetId: $request->query->getString('targetId') ?: null, + language: $request->query->getString('language') ?: null, + enableInheritance: $request->query->getString('enableInheritance') ?: null, + ); + } +} diff --git a/src/Handler/Document/Copy/CopyInfo/CopyInfoResult.php b/src/Handler/Document/Copy/CopyInfo/CopyInfoResult.php new file mode 100644 index 00000000..2c834772 --- /dev/null +++ b/src/Handler/Document/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $document = Document::getById($payload->documentId); + + if ($document === null) { + return; + } + + $document = Document\Service::rewriteIds($document, ['document' => $payload->idMapping], [ + 'enableInheritance' => $payload->enableInheritance, + ]); + $document->setUserModification($userId); + $document->save(); + } +} diff --git a/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php new file mode 100644 index 00000000..7bb1f1f6 --- /dev/null +++ b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php @@ -0,0 +1,52 @@ +request->getString('transactionId'); + $idStore = Session::getSessionBag($request->getSession(), 'opendxp_copy')->get($transactionId) ?? []; + + if (!array_key_exists('rewrite-stack', $idStore)) { + $idStore['rewrite-stack'] = array_values($idStore['idMapping'] ?? []); + } + + $documentId = (int) array_shift($idStore['rewrite-stack']); + + return new static( + documentId: $documentId, + idMapping: $idStore['idMapping'] ?? [], + enableInheritance: $request->request->getString('enableInheritance') === 'true', + transactionId: $transactionId, + updatedIdStore: $idStore, + ); + } +} diff --git a/src/Handler/Document/DeleteDocument/DeleteDocumentHandler.php b/src/Handler/Document/DeleteDocument/DeleteDocumentHandler.php new file mode 100644 index 00000000..b05d3546 --- /dev/null +++ b/src/Handler/Document/DeleteDocument/DeleteDocumentHandler.php @@ -0,0 +1,73 @@ +type === 'children') { + $parentDocument = Document::getById($payload->id); + + if (!$parentDocument) { + throw new NotFoundHttpException('Parent document not found'); + } + + $list = new Document\Listing(); + $list->setCondition('`path` LIKE ?', [$list->escapeLike($parentDocument->getRealFullPath()) . '/%']); + $list->setLimit($payload->amount); + $list->setOrderKey('LENGTH(`path`)', false); + $list->setOrder('DESC'); + + $documents = $list->load(); + + $deletedItems = []; + foreach ($documents as $document) { + $deletedItems[$document->getId()] = $document->getRealFullPath(); + if ($document->isAllowed('delete') && !$document->isLocked()) { + $document->delete(); + } + } + + return new DeleteDocumentResult($deletedItems); + } + + $document = Document::getById($payload->id); + + if (!$document) { + throw new NotFoundHttpException('Document not found'); + } + + if (!$document->isAllowed('delete')) { + throw new AccessDeniedHttpException('Access denied: missing delete permission'); + } + + if ($document->isLocked()) { + throw new RuntimeException('Prevented deleting document, because it is locked: ID: ' . $document->getId()); + } + + $document->delete(); + + return new DeleteDocumentResult(); + } +} diff --git a/src/Handler/Document/DeleteDocument/DeleteDocumentPayload.php b/src/Handler/Document/DeleteDocument/DeleteDocumentPayload.php new file mode 100644 index 00000000..8ffe1b63 --- /dev/null +++ b/src/Handler/Document/DeleteDocument/DeleteDocumentPayload.php @@ -0,0 +1,38 @@ +request->getString('type'), + id: $request->request->getInt('id'), + amount: $request->request->getInt('amount'), + ); + } +} diff --git a/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php b/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php new file mode 100644 index 00000000..231dbc78 --- /dev/null +++ b/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php @@ -0,0 +1,24 @@ +isWriteable()) { + throw new ConfigWriteException(); + } + $type = DocType::create(); + $type->setValues($payload->data); + $type->save(); + $responseData = $type->getObjectVars(); + $responseData['writeable'] = $type->isWriteable(); + + return new CreateDocTypeResult(data: $responseData); + } +} diff --git a/src/Handler/Document/DocTypes/CreateDocType/CreateDocTypeResult.php b/src/Handler/Document/DocTypes/CreateDocType/CreateDocTypeResult.php new file mode 100644 index 00000000..c09de389 --- /dev/null +++ b/src/Handler/Document/DocTypes/CreateDocType/CreateDocTypeResult.php @@ -0,0 +1,12 @@ +id); + if (!$type->isWriteable()) { + throw new ConfigWriteException(); + } + $type->delete(); + + return new DeleteDocTypeResult(); + } +} diff --git a/src/Handler/Document/DocTypes/DeleteDocType/DeleteDocTypeResult.php b/src/Handler/Document/DocTypes/DeleteDocType/DeleteDocTypeResult.php new file mode 100644 index 00000000..884f52b1 --- /dev/null +++ b/src/Handler/Document/DocTypes/DeleteDocType/DeleteDocTypeResult.php @@ -0,0 +1,12 @@ +request->get('data', ''), true) ?? []; + $id = (string) ($data['id'] ?? ''); + unset($data['id']); + + return new static(id: $id, data: $data); + } +} diff --git a/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListHandler.php b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListHandler.php new file mode 100644 index 00000000..7656943e --- /dev/null +++ b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListHandler.php @@ -0,0 +1,46 @@ +userContext->getAdminUser(); + $list = new DocType\Listing(); + + $docTypes = []; + foreach ($list->getDocTypes() as $type) { + if ($adminUser->isAllowed($type->getId(), 'docType')) { + $data = $type->getObjectVars(); + $data['writeable'] = $type->isWriteable(); + $docTypes[] = $data; + } + } + + return new GetDocTypesListResult($docTypes, count($docTypes)); + } +} diff --git a/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListResult.php b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListResult.php new file mode 100644 index 00000000..1f0b11e4 --- /dev/null +++ b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListResult.php @@ -0,0 +1,26 @@ +id); + if (!$type->isWriteable()) { + throw new ConfigWriteException(); + } + $type->setValues($payload->data); + $type->save(); + $responseData = $type->getObjectVars(); + $responseData['writeable'] = $type->isWriteable(); + + return new UpdateDocTypeResult(data: $responseData); + } +} diff --git a/src/Handler/Document/DocTypes/UpdateDocType/UpdateDocTypeResult.php b/src/Handler/Document/DocTypes/UpdateDocType/UpdateDocTypeResult.php new file mode 100644 index 00000000..e7691644 --- /dev/null +++ b/src/Handler/Document/DocTypes/UpdateDocType/UpdateDocTypeResult.php @@ -0,0 +1,12 @@ +id); + if (!$email) { + throw new NotFoundHttpException('Email not found'); + } + + if ($email->isAllowed('save') || $email->isAllowed('publish') || $email->isAllowed('unpublish') || $email->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($email->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $email); + } + + $email = clone $email; + $draftVersion = null; + $email = DocumentVersionHelper::resolveLatestDraft($email, $draftVersion, $this->userContext->getAdminUser()?->getId()); + + $versions = Element\Service::getSafeVersionInfo($email->getVersions()); + $email->setVersions(array_splice($versions, -1, 1)); + $email->setParent(null); + + // unset useless data + $email->setEditables(null); + $email->setChildren(null); + + $data = $email->getObjectVars(); + $data['locked'] = $email->isLocked(); + $data['url'] = $email->getUrl(); + + $this->documentMetaEnricher->enrich($email, $data); + $this->adminStyleEnricher->forEditor($email, $data); + $this->userNamesEnricher->enrich($email, $data); + $this->propertiesEnricher->enrich($email, $data); + $this->translationEnricher->enrich($email, $data); + $this->draftEnricher->enrich($email, $data, $draftVersion); + + return new GetEmailDataResult(email: $email, data: $data, draftVersion: $draftVersion); + } +} diff --git a/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php b/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php new file mode 100644 index 00000000..d633a339 --- /dev/null +++ b/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php @@ -0,0 +1,30 @@ +id); + if (!$email) { + throw new NotFoundHttpException('Email not found'); + } + + if ($sessionAware) { + $sessionEmail = $this->sessionService->getDocument($email); + if ($sessionEmail instanceof Email) { + $email = $sessionEmail; + } else { + $email = DocumentVersionHelper::resolveLatestDraft($email, userId: $this->userContext->getAdminUser()?->getId()); + } + } + + $this->mapper->applyPagePayload($payload, $email); + + $result = $this->coordinator->save($email, $payload->task); + + if ($sessionAware) { + $this->sessionService->saveDocument($result->document); + } + + return new SaveEmailResult( + email: $result->document instanceof Email ? $result->document : $email, + task: $result->task, + version: $result->version, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Email/SaveEmail/SaveEmailPayload.php b/src/Handler/Document/Email/SaveEmail/SaveEmailPayload.php new file mode 100644 index 00000000..087eb421 --- /dev/null +++ b/src/Handler/Document/Email/SaveEmail/SaveEmailPayload.php @@ -0,0 +1,22 @@ +id); + if (!$folder) { + throw new NotFoundHttpException('Folder not found'); + } + + $folder = clone $folder; + $folder->setParent(null); + + $data = $folder->getObjectVars(); + $data['locked'] = $folder->isLocked(); + + $this->documentMetaEnricher->enrich($folder, $data); + $this->adminStyleEnricher->forEditor($folder, $data); + $this->userNamesEnricher->enrich($folder, $data); + $this->propertiesEnricher->enrich($folder, $data); + $this->translationEnricher->enrich($folder, $data); + + return new GetFolderDataResult(folder: $folder, data: $data); + } +} diff --git a/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php b/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php new file mode 100644 index 00000000..882d91cc --- /dev/null +++ b/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Folder/GetFolderData/GetFolderDataResult.php b/src/Handler/Document/Folder/GetFolderData/GetFolderDataResult.php new file mode 100644 index 00000000..bc9a5c5c --- /dev/null +++ b/src/Handler/Document/Folder/GetFolderData/GetFolderDataResult.php @@ -0,0 +1,28 @@ +id); + if (!$folder) { + throw new NotFoundHttpException('Folder not found'); + } + + $this->mapper->applyFolderPayload($payload, $folder); + $result = $this->coordinator->save($folder, 'publish'); + + return new SaveFolderResult( + folder: $result->document instanceof Folder ? $result->document : $folder, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Folder/SaveFolder/SaveFolderPayload.php b/src/Handler/Document/Folder/SaveFolder/SaveFolderPayload.php new file mode 100644 index 00000000..f8e41396 --- /dev/null +++ b/src/Handler/Document/Folder/SaveFolder/SaveFolderPayload.php @@ -0,0 +1,39 @@ +request->getInt('id'), + properties: $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Document/Folder/SaveFolder/SaveFolderResult.php b/src/Handler/Document/Folder/SaveFolder/SaveFolderResult.php new file mode 100644 index 00000000..99ce133e --- /dev/null +++ b/src/Handler/Document/Folder/SaveFolder/SaveFolderResult.php @@ -0,0 +1,28 @@ +type) { + if (!Document\Service::isValidType($payload->type)) { + throw new BadRequestHttpException('Invalid type: ' . $payload->type); + } + $list->setFilter(static fn (DocType $docType) => $docType->getType() === $payload->type); + } + + $docTypes = []; + foreach ($list->getDocTypes() as $docType) { + $docTypes[] = $docType->getObjectVars(); + } + + return new GetDocTypesByTypeResult($docTypes); + } +} diff --git a/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypePayload.php b/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypePayload.php new file mode 100644 index 00000000..85709e11 --- /dev/null +++ b/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypePayload.php @@ -0,0 +1,34 @@ +query->get('type'), + ); + } +} diff --git a/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypeResult.php b/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypeResult.php new file mode 100644 index 00000000..2e0cd1d5 --- /dev/null +++ b/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypeResult.php @@ -0,0 +1,25 @@ +load(); + + $documents = []; + foreach ($childrenList as $childDocument) { + $documentTreeNode = $this->elementService->getElementTreeNodeConfig($childDocument); + // the !isset is for printContainer case, there are no permissions set there + if (!isset($documentTreeNode['permissions']['list']) || $documentTreeNode['permissions']['list'] == 1) { + $documents[] = $documentTreeNode; + } + } + + return new GetDocumentChildrenResult($documents); + } +} diff --git a/src/Handler/Document/GetDocumentChildren/GetDocumentChildrenResult.php b/src/Handler/Document/GetDocumentChildren/GetDocumentChildrenResult.php new file mode 100644 index 00000000..ef29fc11 --- /dev/null +++ b/src/Handler/Document/GetDocumentChildren/GetDocumentChildrenResult.php @@ -0,0 +1,24 @@ +id); + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + if (!$document->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + if ($document->isAllowed('save') || $document->isAllowed('publish') || $document->isAllowed('unpublish') || $document->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($document->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $document); + } + + $document = clone $document; + $data = $document->getObjectVars(); + + $this->documentMetaEnricher->enrich($document, $data); + $this->adminStyleEnricher->forEditor($document, $data); + $this->userNamesEnricher->enrich($document, $data); + $this->propertiesEnricher->enrich($document, $data); + $this->translationEnricher->enrich($document, $data); + + $event = new GenericEvent($this, ['data' => $data, 'document' => $document]); + $this->eventDispatcher->dispatch($event, AdminEvents::DOCUMENT_GET_PRE_SEND_DATA); + $data = $event->getArgument('data'); + + return new GetDocumentDataResult($data); + } +} diff --git a/src/Handler/Document/GetDocumentData/GetDocumentDataPayload.php b/src/Handler/Document/GetDocumentData/GetDocumentDataPayload.php new file mode 100644 index 00000000..14b5d0cc --- /dev/null +++ b/src/Handler/Document/GetDocumentData/GetDocumentDataPayload.php @@ -0,0 +1,34 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php b/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php new file mode 100644 index 00000000..ed5ac4b4 --- /dev/null +++ b/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php @@ -0,0 +1,24 @@ +path); + if (!$document) { + return null; + } + + return new GetDocumentIdForPathResult($document->getId(), $document->getType()); + } +} diff --git a/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathPayload.php b/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathPayload.php new file mode 100644 index 00000000..a76fe167 --- /dev/null +++ b/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathPayload.php @@ -0,0 +1,34 @@ +query->has('path') ? $request->query->getString('path') : null, + ); + } +} diff --git a/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathResult.php b/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathResult.php new file mode 100644 index 00000000..dcb34ea1 --- /dev/null +++ b/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathResult.php @@ -0,0 +1,26 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Hardlink not found'); + } + + if ($link->isAllowed('save') || $link->isAllowed('publish') || $link->isAllowed('unpublish') || $link->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($link->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $link); + } + + $cloned = clone $link; + $cloned->setParent(null); + + $data = $cloned->getObjectVars(); + $data['locked'] = $cloned->isLocked(); + $data['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $cloned->getScheduledTasks() + ); + + if ($cloned->getSourceDocument()) { + $data['sourcePath'] = $cloned->getSourceDocument()->getRealFullPath(); + } + + $this->documentMetaEnricher->enrich($cloned, $data); + $this->adminStyleEnricher->forEditor($cloned, $data); + $this->userNamesEnricher->enrich($cloned, $data); + $this->propertiesEnricher->enrich($cloned, $data); + $this->translationEnricher->enrich($cloned, $data); + + return new GetHardlinkDataResult(original: $link, link: $cloned, data: $data); + } +} diff --git a/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php new file mode 100644 index 00000000..37e60c67 --- /dev/null +++ b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php @@ -0,0 +1,31 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Hardlink not found'); + } + + $this->mapper->applyHardlinkPayload($payload, $link); + $result = $this->coordinator->save($link, $payload->task); + + return new SaveHardlinkResult( + link: $result->document instanceof Hardlink ? $result->document : $link, + task: $result->task, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkPayload.php b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkPayload.php new file mode 100644 index 00000000..53554df9 --- /dev/null +++ b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkPayload.php @@ -0,0 +1,49 @@ +request->getInt('id'), + task: strtolower($request->query->getString('task')), + data: $request->request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? null) + : null, + properties: $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? null) + : null, + scheduler: $request->request->has('scheduler') + ? (json_decode($request->request->getString('scheduler'), true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php new file mode 100644 index 00000000..1c73721a --- /dev/null +++ b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php @@ -0,0 +1,29 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Link not found'); + } + + if ($link->isAllowed('save') || $link->isAllowed('publish') || $link->isAllowed('unpublish') || $link->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($link->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $link); + } + + $cloned = clone $link; + $cloned->setElement(null); + $cloned->setParent(null); + + $data = $this->serializer->serialize($cloned->getObjectVars(), 'json', []); + $data = json_decode($data, true); + $data['locked'] = $cloned->isLocked(); + $data['rawHref'] = $cloned->getRawHref(); + $data['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $cloned->getScheduledTasks() + ); + + $this->documentMetaEnricher->enrich($cloned, $data); + $this->adminStyleEnricher->forEditor($cloned, $data); + $this->userNamesEnricher->enrich($cloned, $data); + $this->propertiesEnricher->enrich($cloned, $data); + $this->translationEnricher->enrich($cloned, $data); + + return new GetLinkDataResult(original: $link, link: $cloned, data: $data); + } +} diff --git a/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php b/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php new file mode 100644 index 00000000..e9c8e1c1 --- /dev/null +++ b/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php @@ -0,0 +1,31 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Link not found'); + } + + $this->mapper->applyLinkPayload($payload, $link); + $result = $this->coordinator->save($link, $payload->task); + + return new SaveLinkResult( + link: $result->document instanceof Link ? $result->document : $link, + task: $result->task, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Link/SaveLink/SaveLinkPayload.php b/src/Handler/Document/Link/SaveLink/SaveLinkPayload.php new file mode 100644 index 00000000..6a1b5be7 --- /dev/null +++ b/src/Handler/Document/Link/SaveLink/SaveLinkPayload.php @@ -0,0 +1,49 @@ +request->getInt('id'), + task: strtolower($request->query->getString('task')), + data: $request->request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? null) + : null, + properties: $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? null) + : null, + scheduler: $request->request->has('scheduler') + ? (json_decode($request->request->getString('scheduler'), true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Document/Link/SaveLink/SaveLinkResult.php b/src/Handler/Document/Link/SaveLink/SaveLinkResult.php new file mode 100644 index 00000000..9e004f06 --- /dev/null +++ b/src/Handler/Document/Link/SaveLink/SaveLinkResult.php @@ -0,0 +1,29 @@ +path, '/'); + + if ($path === '') { + return new CheckPrettyUrlResult(success: true, messages: []); + } + + $messages = []; + $success = true; + + // must start with / + if (!str_starts_with($path, '/')) { + $success = false; + $messages[] = 'URL must start with /.'; + } + + if (strlen($path) < 2) { + $success = false; + $messages[] = 'URL must be at least 2 characters long.'; + } + + if (!Element\Service::isValidPath($path, 'document')) { + $success = false; + $messages[] = 'URL is invalid.'; + } + + if ($success) { + $list = new Document\Listing(); + $list->setCondition('(CONCAT(`path`, `key`) = ? OR id IN (SELECT id from documents_page WHERE prettyUrl = ?)) + AND id != ?', [ + $path, $path, $payload->id, + ]); + + if ($list->getTotalCount() > 0) { + $checkDocument = Document::getById($payload->id); + $checkSite = Frontend::getSiteForDocument($checkDocument); + $checkSiteId = empty($checkSite) ? 0 : $checkSite->getId(); + + foreach ($list as $document) { + if (empty($document)) { + continue; + } + + $site = Frontend::getSiteForDocument($document); + $siteId = empty($site) ? 0 : $site->getId(); + + if ($siteId === $checkSiteId) { + $success = false; + $messages[] = 'URL path already exists.'; + + break; + } + } + } + } + + return new CheckPrettyUrlResult(success: $success, messages: $messages); + } +} diff --git a/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlPayload.php b/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlPayload.php new file mode 100644 index 00000000..ecad17cf --- /dev/null +++ b/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlPayload.php @@ -0,0 +1,37 @@ +request->getInt('id'), + path: trim($request->request->get('path', '')), + ); + } +} diff --git a/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlResult.php b/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlResult.php new file mode 100644 index 00000000..9d1ca77e --- /dev/null +++ b/src/Handler/Document/Page/CheckPrettyUrl/CheckPrettyUrlResult.php @@ -0,0 +1,27 @@ +setCondition('`type` = ?', ['page']); + + // @todo: this seems completely wrong. + foreach ($list->loadIdList() as $docId) { + $this->messengerBusOpendxpCore->dispatch( + new GeneratePagePreviewMessage($docId, \OpenDxp\Tool::getHostUrl()) + ); + + break; + } + } +} diff --git a/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodeHandler.php b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodeHandler.php new file mode 100644 index 00000000..1051e305 --- /dev/null +++ b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodeHandler.php @@ -0,0 +1,49 @@ +id); + + if (!$page) { + throw new NotFoundHttpException('Page not found'); + } + + $url = $page->getUrl(); + + $result = Builder::create() + ->writer(new PngWriter()) + ->data($url) + ->size($payload->download ? 4000 : 500) + ->build(); + + $tmpFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/qr-code-' . uniqid('', false) . '.png'; + $result->saveToFile($tmpFile); + + return $tmpFile; + } +} diff --git a/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodePayload.php b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodePayload.php new file mode 100644 index 00000000..9d67f6ba --- /dev/null +++ b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodePayload.php @@ -0,0 +1,37 @@ +query->getInt('id'), + download: (bool) $request->query->get('download'), + ); + } +} diff --git a/src/Handler/Document/Page/GetPageData/GetPageDataHandler.php b/src/Handler/Document/Page/GetPageData/GetPageDataHandler.php new file mode 100644 index 00000000..cad71aab --- /dev/null +++ b/src/Handler/Document/Page/GetPageData/GetPageDataHandler.php @@ -0,0 +1,100 @@ +id); + if (!$page) { + throw new NotFoundHttpException('Page not found'); + } + + if ($page->isAllowed('save') || $page->isAllowed('publish') || $page->isAllowed('unpublish') || $page->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($page->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $page); + } + + $page = clone $page; + $draftVersion = null; + $page = DocumentVersionHelper::resolveLatestDraft($page, $draftVersion, $this->userContext->getAdminUser()?->getId()); + + $pageVersions = Element\Service::getSafeVersionInfo($page->getVersions()); + $page->setVersions(array_splice($pageVersions, -1, 1)); + $page->setParent(null); + + // unset useless data + $page->setEditables(null); + $page->setChildren(null); + + $data = $page->getObjectVars(); + $data['locked'] = $page->isLocked(); + + if ($page->getContentMainDocument()) { + $data['contentMainDocumentPath'] = $page->getContentMainDocument()->getRealFullPath(); + } + + if ($page->getStaticGeneratorEnabled()) { + $data['staticLastGenerated'] = $this->staticPageGenerator->getLastModified($page); + } + + $data['url'] = $page->getUrl(); + $data['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $page->getScheduledTasks() + ); + + $this->documentMetaEnricher->enrich($page, $data); + $this->adminStyleEnricher->forEditor($page, $data); + $this->userNamesEnricher->enrich($page, $data); + $this->propertiesEnricher->enrich($page, $data); + $this->translationEnricher->enrich($page, $data); + $this->draftEnricher->enrich($page, $data, $draftVersion); + + return new GetPageDataResult(page: $page, data: $data, draftVersion: $draftVersion); + } +} diff --git a/src/Handler/Document/Page/GetPageData/GetPageDataPayload.php b/src/Handler/Document/Page/GetPageData/GetPageDataPayload.php new file mode 100644 index 00000000..7f8ef6e9 --- /dev/null +++ b/src/Handler/Document/Page/GetPageData/GetPageDataPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Page/GetPageData/GetPageDataResult.php b/src/Handler/Document/Page/GetPageData/GetPageDataResult.php new file mode 100644 index 00000000..d7d37360 --- /dev/null +++ b/src/Handler/Document/Page/GetPageData/GetPageDataResult.php @@ -0,0 +1,30 @@ +id); + if (!$document instanceof Document\Page) { + throw new NotFoundHttpException('Page not found'); + } + + return $document->getPreviewImageFilesystemPath(); + } +} diff --git a/src/Handler/Document/Page/GetPagePreviewImagePath/GetPagePreviewImagePathPayload.php b/src/Handler/Document/Page/GetPagePreviewImagePath/GetPagePreviewImagePathPayload.php new file mode 100644 index 00000000..eca33487 --- /dev/null +++ b/src/Handler/Document/Page/GetPagePreviewImagePath/GetPagePreviewImagePathPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Page/PagePayload.php b/src/Handler/Document/Page/PagePayload.php new file mode 100644 index 00000000..7c394705 --- /dev/null +++ b/src/Handler/Document/Page/PagePayload.php @@ -0,0 +1,60 @@ +request->getInt('id'), + task: strtolower($request->query->getString('task')), + settings: $request->request->has('settings') + ? (json_decode($request->request->getString('settings'), true) ?? null) + : null, + editables: $request->request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? null) + : null, + appendEditables: (bool) $request->request->get('appendEditables'), + properties: $request->request->has('properties') + ? (json_decode($request->request->getString('properties'), true) ?? null) + : null, + scheduler: $request->request->has('scheduler') + ? (json_decode($request->request->getString('scheduler'), true) ?? null) + : null, + missingRequiredEditable: $request->request->has('missingRequiredEditable') + ? $request->request->getString('missingRequiredEditable') === 'true' + : null, + ); + } +} diff --git a/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeHandler.php b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeHandler.php new file mode 100644 index 00000000..3b7947a3 --- /dev/null +++ b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeHandler.php @@ -0,0 +1,63 @@ +documentId); + if (!$document) { + throw new NotFoundHttpException(); + } + + $document = clone $document; + $document->setEditables([]); + + $this->blockStateStack->loadArray($payload->blockStateStack); + $this->localeService->setLocale($document->getProperty('language')); + + /** @var Document\Editable\Areablock $areablock */ + $areablock = $this->editableRenderer->getEditable($document, 'areablock', $payload->realName, $payload->areaBlockConfig, true); + $areablock->setRealName($payload->realName); + $areablock->setEditmode(true); + $areablock->setDataFromEditmode($payload->areaBrickData); + $htmlCode = trim($areablock->renderIndex($payload->index, true)); + + return new RenderAreabrickIndexEditmodeResult( + document: $document, + editableDefinitions: $this->definitionCollector->getDefinitions(), + htmlCode: $htmlCode, + ); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodePayload.php b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodePayload.php new file mode 100644 index 00000000..1a67035d --- /dev/null +++ b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodePayload.php @@ -0,0 +1,45 @@ +request->get('documentId'), + blockStateStack: json_decode($request->request->getString('blockStateStack'), true), + realName: $request->request->getString('realName'), + areaBlockConfig: json_decode($request->request->getString('areablockConfig'), true), + areaBrickData: json_decode($request->request->getString('areablockData'), true), + index: (int) $request->request->get('index'), + ); + } +} diff --git a/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeResult.php b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeResult.php new file mode 100644 index 00000000..fce9c458 --- /dev/null +++ b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeResult.php @@ -0,0 +1,29 @@ +id); + + if (!$doc) { + throw new NotFoundHttpException('Document not found'); + } + + foreach ($doc->getEditables() as $editable) { + // remove all but target group data + // Hardcoded the TARGET_GROUP_EDITABLE_PREFIX prefix here as we shouldn't remove the bundle specific editables even if bundle is not enabled/installed + if (!preg_match('/^' . preg_quote('persona_ -', '/') . '/', $editable->getName())) { + $doc->removeEditable($editable->getName()); + } + } + + $this->sessionService->saveDocument($doc, useForSave: true); + } +} diff --git a/src/Handler/Document/Page/ResetEditablesSession/ResetEditablesSessionPayload.php b/src/Handler/Document/Page/ResetEditablesSession/ResetEditablesSessionPayload.php new file mode 100644 index 00000000..6aaf5fcf --- /dev/null +++ b/src/Handler/Document/Page/ResetEditablesSession/ResetEditablesSessionPayload.php @@ -0,0 +1,35 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Page/SavePage/SavePageHandler.php b/src/Handler/Document/Page/SavePage/SavePageHandler.php new file mode 100644 index 00000000..95809982 --- /dev/null +++ b/src/Handler/Document/Page/SavePage/SavePageHandler.php @@ -0,0 +1,82 @@ +id); + if (!$oldPage) { + throw new NotFoundHttpException('Page not found'); + } + + if ($sessionAware) { + $sessionPage = $this->sessionService->getDocument($oldPage); + if ($sessionPage instanceof Page) { + $page = $sessionPage; + } else { + $page = DocumentVersionHelper::resolveLatestDraft($oldPage, userId: $this->userContext->getAdminUser()?->getId()); + } + } else { + $page = $oldPage; + } + + $this->mapper->applyPagePayload($payload, $page); + + $result = $this->coordinator->save($page, $payload->task); + + $this->dispatchEvent( + new DocumentEvent($result->document, ['oldPage' => $oldPage, 'task' => $result->task]), + DocumentEvents::PAGE_POST_SAVE_ACTION, + ); + + if ($sessionAware && !in_array($payload->task, ['publish', 'unpublish'], true)) { + $this->sessionService->saveDocument($result->document); + } + + return new SavePageResult( + page: $result->document instanceof Page ? $result->document : $page, + oldPage: $oldPage, + task: $result->task, + version: $result->version, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Page/SavePage/SavePagePayload.php b/src/Handler/Document/Page/SavePage/SavePagePayload.php new file mode 100644 index 00000000..ce59ac65 --- /dev/null +++ b/src/Handler/Document/Page/SavePage/SavePagePayload.php @@ -0,0 +1,22 @@ +sessionService->removeDocument($payload->id); + } +} diff --git a/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletHandler.php b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletHandler.php new file mode 100644 index 00000000..85e0c75a --- /dev/null +++ b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletHandler.php @@ -0,0 +1,90 @@ +id && $payload->type) { + $element = Service::getElementById($payload->type, $payload->id); + } + + if (!$element instanceof ElementInterface) { + throw new NotFoundHttpException(sprintf('Element with type %s and ID %d was not found', $payload->type ?? 'null', $payload->id ?? 0)); + } + + if (!$element->isAllowed('view')) { + throw new AccessDeniedHttpException(sprintf('Access to element with type %s and ID %d is not allowed', $payload->type, $payload->id)); + } + + $this->eventDispatcher->dispatch( + new GenericEvent(null, ['requestParams' => $payload->query, 'element' => $element]), + DocumentEvents::EDITABLE_RENDERLET_PRE_RENDER, + ); + + $attributes = []; + + if ($payload->parentDocumentId) { + $document = Document\PageSnippet::getById((int) $payload->parentDocumentId); + if ($document) { + $attributes = $this->actionRenderer->addDocumentAttributes($document, $attributes); + unset($attributes[DynamicRouter::CONTENT_TEMPLATE]); + } + } + + if ($payload->template) { + $attributes[DynamicRouter::CONTENT_TEMPLATE] = $payload->template; + } + + $query = $payload->query; + foreach (['controller', 'action', 'module', 'bundle'] as $key) { + unset($query[$key]); + } + + if (isset($attributes['_locale'])) { + $this->localeService->setLocale($attributes['_locale']); + } + + return new RenderRenderletResult( + html: $this->editableHandler->renderAction($payload->controller, $attributes, $query), + ); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletPayload.php b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletPayload.php new file mode 100644 index 00000000..4ee24282 --- /dev/null +++ b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletPayload.php @@ -0,0 +1,45 @@ +query->get('type'), + id: $request->query->getInt('id') ?: null, + controller: $request->query->getString('controller') ?: null, + parentDocumentId: $request->query->getString('opendxp_parentDocument') ?: null, + template: $request->query->getString('template') ?: null, + query: $request->query->all(), + ); + } +} diff --git a/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletResult.php b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletResult.php new file mode 100644 index 00000000..09d7de97 --- /dev/null +++ b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletResult.php @@ -0,0 +1,25 @@ +id) { + return; + } + + $document = $this->sessionService->getOrLoadDocument($payload->id); + if (!$document) { + throw new NotFoundHttpException('Document not found in session'); + } + + $document->setInDumpState(true); + + if ($document instanceof Document\Email) { + $this->mapper->applyPagePayload($payload->email, $document); + } elseif ($document instanceof Document\PageSnippet) { + $this->mapper->applyPagePayload($payload->page, $document); + } elseif ($document instanceof Document\Link) { + $this->mapper->applyLinkPayload($payload->link, $document); + } elseif ($document instanceof Document\Hardlink) { + $this->mapper->applyHardlinkPayload($payload->hardlink, $document); + } elseif ($document instanceof Document\Folder) { + $this->mapper->applyFolderPayload($payload->folder, $document); + } + + $this->sessionService->saveDocument($document); + } +} diff --git a/src/Handler/Document/SaveToSession/SaveToSessionPayload.php b/src/Handler/Document/SaveToSession/SaveToSessionPayload.php new file mode 100644 index 00000000..2c013aed --- /dev/null +++ b/src/Handler/Document/SaveToSession/SaveToSessionPayload.php @@ -0,0 +1,55 @@ +request->getInt('id'); + if (!$id) { + return new static(id: 0, page: null, email: null, link: null, hardlink: null, folder: null); + } + + return new static( + id: $id, + page: PagePayload::fromRequest($request), + email: SaveEmailPayload::fromRequest($request), + link: SaveLinkPayload::fromRequest($request), + hardlink: SaveHardlinkPayload::fromRequest($request), + folder: SaveFolderPayload::fromRequest($request), + ); + } +} diff --git a/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsHandler.php b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsHandler.php new file mode 100644 index 00000000..25d3fde4 --- /dev/null +++ b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsHandler.php @@ -0,0 +1,40 @@ +id); + + $event = new SiteCustomSettingsEvent($site); + $this->eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS); + + return new GetSiteCustomSettingsResult($event->getConfigNodes()); + } +} diff --git a/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsPayload.php b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsPayload.php new file mode 100644 index 00000000..14501c62 --- /dev/null +++ b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsPayload.php @@ -0,0 +1,34 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsResult.php b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsResult.php new file mode 100644 index 00000000..ac7c3ac0 --- /dev/null +++ b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsResult.php @@ -0,0 +1,25 @@ +id)->delete(); + } +} diff --git a/src/Handler/Document/Site/RemoveSite/RemoveSitePayload.php b/src/Handler/Document/Site/RemoveSite/RemoveSitePayload.php new file mode 100644 index 00000000..1570d0a1 --- /dev/null +++ b/src/Handler/Document/Site/RemoveSite/RemoveSitePayload.php @@ -0,0 +1,34 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Site/UpdateSite/UpdateSiteHandler.php b/src/Handler/Document/Site/UpdateSite/UpdateSiteHandler.php new file mode 100644 index 00000000..819d1e04 --- /dev/null +++ b/src/Handler/Document/Site/UpdateSite/UpdateSiteHandler.php @@ -0,0 +1,67 @@ +rootId)) { + $site = Site::create(['rootId' => $payload->rootId]); + } + + $event = new SiteCustomSettingsEvent($site); + $this->eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS); + + $customSettings = []; + foreach ($event->getConfigNodes() as $scope => $nodes) { + foreach ($nodes as $node) { + $requestValueName = sprintf('customSettings_%s_%s', $scope, $node['name']); + if (isset($payload->requestCustomSettings[$requestValueName])) { + $value = $payload->requestCustomSettings[$requestValueName]; + if ($node['type'] === SiteCustomConfigNodeType::CHECKBOX->value) { + $value = $value === 'true'; + } + $customSettings[$scope][$node['name']] = $value; + } + } + } + + $site->setDomains($payload->domains); + $site->setMainDomain($payload->mainDomain); + $site->setErrorDocument($payload->errorDocument); + $site->setLocalizedErrorDocuments($payload->localizedErrorDocuments); + $site->setRedirectToMainDomain($payload->redirectToMainDomain); + $site->setCustomSettings(count($customSettings) === 0 ? null : $customSettings); + $site->save(); + + $site->setRootDocument(null); + + return new UpdateSiteResult(siteVars: $site->getObjectVars()); + } +} diff --git a/src/Handler/Document/Site/UpdateSite/UpdateSitePayload.php b/src/Handler/Document/Site/UpdateSite/UpdateSitePayload.php new file mode 100644 index 00000000..1b52a45c --- /dev/null +++ b/src/Handler/Document/Site/UpdateSite/UpdateSitePayload.php @@ -0,0 +1,58 @@ +request->getString('domains')); + $domains = $domainsRaw ? explode("\n", $domainsRaw) : []; + + $localizedErrorDocuments = []; + foreach (Tool::getValidLanguages() as $language) { + $requestValue = $request->request->get(sprintf('errorDocument_localized_%s', $language)); + if (isset($requestValue)) { + $localizedErrorDocuments[$language] = $requestValue; + } + } + + return new static( + rootId: $request->request->getInt('id'), + domains: $domains, + mainDomain: $request->request->getString('mainDomain'), + errorDocument: $request->request->getString('errorDocument'), + localizedErrorDocuments: $localizedErrorDocuments, + redirectToMainDomain: $request->request->getBoolean('redirectToMainDomain'), + requestCustomSettings: $request->request->all(), + ); + } +} diff --git a/src/Handler/Document/Site/UpdateSite/UpdateSiteResult.php b/src/Handler/Document/Site/UpdateSite/UpdateSiteResult.php new file mode 100644 index 00000000..9468ccbf --- /dev/null +++ b/src/Handler/Document/Site/UpdateSite/UpdateSiteResult.php @@ -0,0 +1,25 @@ +id); + if (!$snippet) { + throw new NotFoundHttpException('Snippet not found'); + } + + if ($snippet->isAllowed('save') || $snippet->isAllowed('publish') || $snippet->isAllowed('unpublish') || $snippet->isAllowed('delete')) { + $this->editLockService->checkAndAcquire($snippet->getId(), 'document', AdminEvents::DOCUMENT_GET_IS_LOCKED, $snippet); + } + + $snippet = clone $snippet; + $draftVersion = null; + $snippet = DocumentVersionHelper::resolveLatestDraft($snippet, $draftVersion, $this->userContext->getAdminUser()?->getId()); + + $versions = Element\Service::getSafeVersionInfo($snippet->getVersions()); + $snippet->setVersions(array_splice($versions, -1, 1)); + $snippet->setParent(null); + $snippet->setEditables(null); + + $data = $snippet->getObjectVars(); + $data['locked'] = $snippet->isLocked(); + $data['url'] = $snippet->getUrl(); + $data['scheduledTasks'] = array_map( + static fn (Task $task) => $task->getObjectVars(), + $snippet->getScheduledTasks() + ); + + if ($snippet->getContentMainDocument()) { + $data['contentMainDocumentPath'] = $snippet->getContentMainDocument()->getRealFullPath(); + } + + $this->documentMetaEnricher->enrich($snippet, $data); + $this->adminStyleEnricher->forEditor($snippet, $data); + $this->userNamesEnricher->enrich($snippet, $data); + $this->propertiesEnricher->enrich($snippet, $data); + $this->translationEnricher->enrich($snippet, $data); + $this->draftEnricher->enrich($snippet, $data, $draftVersion); + + return new GetSnippetDataResult(snippet: $snippet, data: $data, draftVersion: $draftVersion); + } +} diff --git a/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php b/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php new file mode 100644 index 00000000..115793a9 --- /dev/null +++ b/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php @@ -0,0 +1,30 @@ +id); + if (!$snippet) { + throw new NotFoundHttpException('Snippet not found'); + } + + if ($sessionAware) { + $sessionSnippet = $this->sessionService->getDocument($snippet); + if ($sessionSnippet instanceof Snippet) { + $snippet = $sessionSnippet; + } else { + $snippet = DocumentVersionHelper::resolveLatestDraft($snippet, userId: $this->userContext->getAdminUser()?->getId()); + } + } + + $this->mapper->applyPagePayload($payload, $snippet); + + $result = $this->coordinator->save($snippet, $payload->task); + + if ($sessionAware) { + $this->sessionService->saveDocument($result->document); + } + + return new SaveSnippetResult( + snippet: $result->document instanceof Snippet ? $result->document : $snippet, + task: $result->task, + version: $result->version, + treeData: $result->treeData, + ); + } +} diff --git a/src/Handler/Document/Snippet/SaveSnippet/SaveSnippetPayload.php b/src/Handler/Document/Snippet/SaveSnippet/SaveSnippetPayload.php new file mode 100644 index 00000000..dfb6fef4 --- /dev/null +++ b/src/Handler/Document/Snippet/SaveSnippet/SaveSnippetPayload.php @@ -0,0 +1,22 @@ +sourceId); + $targetDocument = Document::getByPath($payload->targetPath); + + if (!$sourceDocument || !$targetDocument) { + return; + } + + if (empty($sourceDocument->getProperty('language'))) { + throw new Exception(sprintf('Source Document(ID:%s) Language(Properties) missing', $sourceDocument->getId())); + } + + if (empty($targetDocument->getProperty('language'))) { + throw new Exception(sprintf('Target Document(ID:%s) Language(Properties) missing', $targetDocument->getId())); + } + + $service = $this->serviceFactory->createDocumentService(); + if ($service->getTranslationSourceId($targetDocument) != $targetDocument->getId()) { + throw new Exception('Target Document already linked to Source Document ID(' . $service->getTranslationSourceId($targetDocument) . '). Please unlink existing relation first.'); + } + + $service->addTranslation($sourceDocument, $targetDocument); + } +} diff --git a/src/Handler/Document/Translation/AddDocumentTranslation/AddDocumentTranslationPayload.php b/src/Handler/Document/Translation/AddDocumentTranslation/AddDocumentTranslationPayload.php new file mode 100644 index 00000000..2dc4438a --- /dev/null +++ b/src/Handler/Document/Translation/AddDocumentTranslation/AddDocumentTranslationPayload.php @@ -0,0 +1,36 @@ +request->getInt('sourceId'), + targetPath: $request->request->getString('targetPath'), + ); + } +} diff --git a/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageHandler.php b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageHandler.php new file mode 100644 index 00000000..bcd6ca08 --- /dev/null +++ b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageHandler.php @@ -0,0 +1,41 @@ +path); + if (!$document) { + return new CheckTranslationLanguageResult(false, null, null); + } + + $language = $document->getProperty('language'); + $found = !empty($language); + $translationLinks = array_keys($this->serviceFactory->createDocumentService()->getTranslations($document)); + + return new CheckTranslationLanguageResult($found, $language ?: null, $translationLinks); + } +} diff --git a/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguagePayload.php b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguagePayload.php new file mode 100644 index 00000000..4cd0d476 --- /dev/null +++ b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguagePayload.php @@ -0,0 +1,34 @@ +query->has('path') ? $request->query->getString('path') : null, + ); + } +} diff --git a/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageResult.php b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageResult.php new file mode 100644 index 00000000..107b0077 --- /dev/null +++ b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageResult.php @@ -0,0 +1,26 @@ +id); + if (!$document) { + return new DetermineTranslationParentResult(false, null, null); + } + + $service = $this->serviceFactory->createDocumentService(); + $document = $document->getId() === 1 ? $document : $document->getParent(); + + $translations = $service->getTranslations($document); + if (isset($translations[$payload->language])) { + $targetDocument = Document::getById($translations[$payload->language]); + + return new DetermineTranslationParentResult( + true, + $targetDocument?->getRealFullPath(), + $targetDocument?->getId(), + ); + } + + return new DetermineTranslationParentResult(false, null, null); + } +} diff --git a/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentPayload.php b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentPayload.php new file mode 100644 index 00000000..72991a3d --- /dev/null +++ b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentPayload.php @@ -0,0 +1,36 @@ +query->getInt('id'), + language: $request->query->has('language') ? $request->query->getString('language') : null, + ); + } +} diff --git a/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentResult.php b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentResult.php new file mode 100644 index 00000000..e8c75bba --- /dev/null +++ b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentResult.php @@ -0,0 +1,26 @@ +node); + + if (!$document) { + throw new NotFoundHttpException('Document not found'); + } + + $nodes = []; + foreach ($document->getChildren() as $child) { + $nodes[] = $this->getTranslationTreeNodeConfig($child, $payload->languages); + } + + return new GetLanguageTreeResult($nodes); + } + + private function getTranslationTreeNodeConfig(Document $document, array $languages, ?array $translations = null): array + { + $service = $this->serviceFactory->createDocumentService(); + $adminUser = $this->userContext->getAdminUser(); + + $config = $this->elementService->getElementTreeNodeConfig($document); + + $translations = $translations ?? $service->getTranslations($document); + + foreach ($languages as $language) { + if ($languageDocumentId = $translations[$language] ?? false) { + $languageDocument = Document::getById((int) $languageDocumentId); + $config[$language] = [ + 'text' => $languageDocument->getKey(), + 'id' => $languageDocument->getId(), + 'type' => $languageDocument->getType(), + 'fullPath' => $languageDocument->getFullPath(), + 'published' => $languageDocument->getPublished(), + 'itemType' => 'document', + 'permissions' => $languageDocument->getUserPermissions($adminUser), + ]; + } elseif (!$document instanceof Document\Folder) { + $config[$language] = [ + 'text' => '--', + 'itemType' => 'empty', + ]; + } + } + + return $config; + } +} diff --git a/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreePayload.php b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreePayload.php new file mode 100644 index 00000000..c5adffe7 --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreePayload.php @@ -0,0 +1,36 @@ +query->getInt('node'), + languages: explode(',', $request->query->getString('languages')), + ); + } +} diff --git a/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeResult.php b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeResult.php new file mode 100644 index 00000000..a94254ee --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeResult.php @@ -0,0 +1,24 @@ +id); + + if (!$document) { + throw new NotFoundHttpException('Document not found'); + } + + $service = $this->serviceFactory->createDocumentService(); + $adminUser = $this->userContext->getAdminUser(); + + $locales = Tool::getSupportedLocales(); + + $lang = $document->getProperty('language'); + + $columns = [ + [ + 'xtype' => 'treecolumn', + 'text' => $lang ? $locales[$lang] : '', + 'dataIndex' => 'text', + 'cls' => $lang ? 'x-column-header_' . strtolower($lang) : null, + 'width' => 300, + 'sortable' => false, + ], + ]; + + $translations = $service->getTranslations($document); + + $combinedTranslations = $translations; + + if ($parentDocument = $document->getParent()) { + $parentTranslations = $service->getTranslations($parentDocument); + foreach ($parentTranslations as $language => $languageDocumentId) { + $combinedTranslations[$language] = $translations[$language] ?? $languageDocumentId; + } + } + + foreach ($combinedTranslations as $language => $languageDocumentId) { + $languageDocument = Document::getById($languageDocumentId); + + if ($languageDocument && $languageDocument->isAllowed('list') && $language != $document->getProperty('language')) { + $columns[] = [ + 'text' => $locales[$language], + 'dataIndex' => $language, + 'cls' => 'x-column-header_' . strtolower($language), + 'width' => 300, + 'sortable' => false, + ]; + } + } + + $root = $this->getTranslationTreeNodeConfig($document, array_keys($translations), $translations, $adminUser); + + return new GetLanguageTreeRootResult( + root: $root, + columns: $columns, + languages: array_keys($translations), + ); + } + + private function getTranslationTreeNodeConfig( + Document $document, + array $languages, + ?array $translations, + ?User $adminUser, + ): array { + $service = $this->serviceFactory->createDocumentService(); + + $config = $this->elementService->getElementTreeNodeConfig($document); + + $translations = $translations ?? $service->getTranslations($document); + + foreach ($languages as $language) { + if ($languageDocumentId = $translations[$language] ?? false) { + $languageDocument = Document::getById((int) $languageDocumentId); + $config[$language] = [ + 'text' => $languageDocument->getKey(), + 'id' => $languageDocument->getId(), + 'type' => $languageDocument->getType(), + 'fullPath' => $languageDocument->getFullPath(), + 'published' => $languageDocument->getPublished(), + 'itemType' => 'document', + 'permissions' => $languageDocument->getUserPermissions($adminUser), + ]; + } elseif (!$document instanceof Document\Folder) { + $config[$language] = [ + 'text' => '--', + 'itemType' => 'empty', + ]; + } + } + + return $config; + } +} diff --git a/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootPayload.php b/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootPayload.php new file mode 100644 index 00000000..fab95e7d --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootPayload.php @@ -0,0 +1,34 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootResult.php b/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootResult.php new file mode 100644 index 00000000..ac5323a4 --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootResult.php @@ -0,0 +1,26 @@ +sourceId); + $targetDocument = Document::getById($payload->targetId); + + if (!$sourceDocument || !$targetDocument) { + return; + } + + $this->serviceFactory->createDocumentService()->removeTranslationLink($sourceDocument, $targetDocument); + } +} diff --git a/src/Handler/Document/Translation/RemoveDocumentTranslation/RemoveDocumentTranslationPayload.php b/src/Handler/Document/Translation/RemoveDocumentTranslation/RemoveDocumentTranslationPayload.php new file mode 100644 index 00000000..ee4c5f6b --- /dev/null +++ b/src/Handler/Document/Translation/RemoveDocumentTranslation/RemoveDocumentTranslationPayload.php @@ -0,0 +1,36 @@ +request->getInt('sourceId'), + targetId: $request->request->getInt('targetId'), + ); + } +} diff --git a/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenHandler.php b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenHandler.php new file mode 100644 index 00000000..5bcb3d7e --- /dev/null +++ b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenHandler.php @@ -0,0 +1,124 @@ +allParams['limit']); + $limit = (int)($payload->allParams['limit'] ?? 100000000); + $offset = (int)($payload->allParams['start'] ?? 0); + + $filter = $payload->allParams['filter'] ?? null; + if (!is_null($filter)) { + if (!str_ends_with($filter, '*')) { + $filter .= '*'; + } + $filter = str_replace('*', '%', $filter); + $limit = 100; + $offset = 0; + } + + $document = Document::getById((int) $payload->allParams['node']); + if (!$document) { + throw new NotFoundHttpException('Document was not found'); + } + + $adminUser = $this->userContext->getAdminUser(); + $documents = []; + $cv = []; + if ($document->hasChildren()) { + if ($payload->allParams['view'] ?? null) { + $cv = $this->elementService->getCustomViewById($payload->allParams['view']); + } + + $db = Db::get(); + $list = new Document\Listing(); + $condition = 'parentId = ' . $db->quote($document->getId()); + if (!$adminUser->isAdmin()) { + $userIds = $adminUser->getRoles(); + $currentUserId = $adminUser->getId(); + $userIds[] = $currentUserId; + + $inheritedPermission = $document->getDao()->isInheritingPermission('list', $userIds); + + $anyAllowedRowOrChildren = 'EXISTS(SELECT list FROM users_workspaces_document uwd WHERE userId IN (' . implode(',', $userIds) . ') AND list=1 AND LOCATE(CONCAT(`path`,`key`),cpath)=1 AND + NOT EXISTS(SELECT list FROM users_workspaces_document WHERE userId =' . $currentUserId . ' AND list=0 AND cpath = uwd.cpath))'; + $isDisallowedCurrentRow = 'EXISTS(SELECT list FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') AND cid = id AND list=0)'; + + $condition .= ' AND IF(' . $anyAllowedRowOrChildren . ',1,IF(' . $inheritedPermission . ', ' . $isDisallowedCurrentRow . ' = 0, 0)) = 1'; + } + + if ($filter) { + $condition = '(' . $condition . ')' . ' AND CAST(documents.key AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ' . $db->quote($filter); + } + + $list->setCondition($condition); + $list->setOrderKey(['index', 'id']); + $list->setOrder(['asc', 'asc']); + $list->setLimit($limit); + $list->setOffset($offset); + + Service::addTreeFilterJoins($cv, $list); + + $beforeListLoadEvent = new GenericEvent($this, [ + 'list' => $list, + 'context' => $payload->allParams, + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::DOCUMENT_LIST_BEFORE_LIST_LOAD); + + /** @var Document\Listing $list */ + $list = $beforeListLoadEvent->getArgument('list'); + + $result = ($this->childrenHandler)($list); + $documents = $result->documents; + } + + $event = new GenericEvent($this, ['documents' => $documents]); + $this->eventDispatcher->dispatch($event, AdminEvents::DOCUMENT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); + $documents = $event->getArgument('documents'); + + return new TreeGetDocumentChildrenResult( + documents: $documents, + offset: $offset, + limit: $limit, + total: $document->getChildAmount($adminUser), + filter: $filter, + paginated: $paginated, + ); + } +} diff --git a/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenPayload.php b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenPayload.php new file mode 100644 index 00000000..d43530fd --- /dev/null +++ b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenPayload.php @@ -0,0 +1,39 @@ +query->getInt('node'), + inSearch: $request->query->getInt('inSearch'), + allParams: $request->query->all(), + ); + } +} diff --git a/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenResult.php b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenResult.php new file mode 100644 index 00000000..2ba6cdd0 --- /dev/null +++ b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenResult.php @@ -0,0 +1,30 @@ +id); + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + $oldPath = (string) $document->getDao()->getCurrentFullPath(); + $oldDocument = Document::getById($payload->id, ['force' => true]); + + $adminUser = $this->userContext->getAdminUser(); + $allowUpdate = true; + + // prevent rename/relocate when newer unpublished version exists + if ($document instanceof Document\PageSnippet) { + $latestVersion = $document->getLatestVersion(); + if ($latestVersion && + $latestVersion->getData()->getModificationDate() != $document->getModificationDate() + ) { + throw new BadRequestHttpException("You can't rename or relocate if there's a newer not published version"); + } + } + + if ($document->isAllowed('settings')) { + if (isset($payload->updateData['parentId'])) { + $parentDocument = Document::getById((int) $payload->updateData['parentId']); + + if ($document->getParentId() !== $parentDocument->getId()) { + if (!$parentDocument->isAllowed('create')) { + throw new RuntimeException('Prevented moving document - no create permission on new parent.'); + } + + $intendedPath = $parentDocument->getRealPath(); + $pKey = $parentDocument->getKey(); + if (!empty($pKey)) { + $intendedPath .= $parentDocument->getKey() . '/'; + } + + $documentWithSamePath = Document::getByPath($intendedPath . $document->getKey()); + + if ($documentWithSamePath != null) { + $allowUpdate = false; + } + + if ($document->isLocked()) { + $allowUpdate = false; + } + } + } + + if ($allowUpdate) { + $blockedVars = ['id', 'controller', 'action', 'module']; + + if (!$document->isAllowed('rename') && isset($payload->updateData['key'])) { + $blockedVars[] = 'key'; + Logger::debug('prevented renaming document because of missing permissions '); + } + + foreach ($payload->updateData as $key => $value) { + if (!in_array($key, $blockedVars)) { + $document->setValue($key, $value); + } + } + + $document->setUserModification($adminUser->getId()); + $document->save(); + + if (isset($payload->updateData['index'])) { + $this->updateIndexesOfDocumentSiblings($document, (int) $payload->updateData['index']); + } + + if ($oldPath && $oldPath != $document->getRealFullPath()) { + $this->firePostMoveEvent($document, $oldDocument, $oldPath); + } + + return new UpdateDocumentResult( + treeData: $this->elementService->getElementTreeNodeConfig($document), + ); + } + + $msg = 'prevented moving document, because document with same path+key already exists' . + ' or the document is locked. ID: ' . $document->getId(); + Logger::debug($msg); + + throw new BadRequestHttpException($msg); + } + + if ($document->isAllowed('rename') && isset($payload->updateData['key'])) { + $document->setKey($payload->updateData['key']); + $document->setUserModification($adminUser->getId()); + $document->save(); + + if ($oldPath && $oldPath != $document->getRealFullPath()) { + $this->firePostMoveEvent($document, $oldDocument, $oldPath); + } + + return new UpdateDocumentResult( + treeData: $this->elementService->getElementTreeNodeConfig($document), + ); + } + + Logger::debug('Prevented update document, because of missing permissions.'); + + throw new BadRequestHttpException('Prevented update document, because of missing permissions.'); + } + + private function firePostMoveEvent(Document $document, Document $oldDocument, string $oldPath): void + { + $arguments = [ + 'oldPath' => $oldPath, + 'oldDocument' => $oldDocument, + ]; + $documentEvent = new DocumentEvent($document, $arguments); + $this->eventDispatcher->dispatch($documentEvent, DocumentEvents::POST_MOVE_ACTION); + } + + private function updateIndexesOfDocumentSiblings(Document $document, int $newIndex): void + { + $updateLatestVersionIndex = function (Document $document, int $newIndex): void { + if ($document instanceof Document\PageSnippet && $latestVersion = $document->getLatestVersion()) { + $document = $latestVersion->loadData(); + $document->setIndex($newIndex); + $latestVersion->save(); + } + }; + + $document->saveIndex($newIndex); + + $list = new Document\Listing(); + $list->setCondition('parentId = ? AND id != ?', [$document->getParentId(), $document->getId()]); + $list->setOrderKey('index'); + $list->setOrder('asc'); + $childrenList = $list->load(); + + $count = 0; + foreach ($childrenList as $child) { + if ($count === $newIndex) { + $count++; + } + $child->saveIndex($count); + $updateLatestVersionIndex($child, $count); + $count++; + } + } +} diff --git a/src/Handler/Document/UpdateDocument/UpdateDocumentPayload.php b/src/Handler/Document/UpdateDocument/UpdateDocumentPayload.php new file mode 100644 index 00000000..9eb9bae2 --- /dev/null +++ b/src/Handler/Document/UpdateDocument/UpdateDocumentPayload.php @@ -0,0 +1,36 @@ +request->get('id'), + updateData: [...$request->request->all(), ...$request->query->all()], + ); + } +} diff --git a/src/Handler/Document/UpdateDocument/UpdateDocumentResult.php b/src/Handler/Document/UpdateDocument/UpdateDocumentResult.php new file mode 100644 index 00000000..7152e173 --- /dev/null +++ b/src/Handler/Document/UpdateDocument/UpdateDocumentResult.php @@ -0,0 +1,24 @@ +from); + $docFrom = $versionFrom?->loadData(); + + if (!$docFrom instanceof Document\PageSnippet) { + throw new DocumentNotFoundException($payload->from); + } + + $versionTo = Version::getById($payload->to); + $docTo = $versionTo?->loadData(); + + if (!$docTo instanceof Document\PageSnippet) { + throw new DocumentNotFoundException($payload->to); + } + + $comparisonId = uniqid(date('Y-m-d') . '-', true); + $tempFileTemplate = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/version-diff-tmp-' . $comparisonId . '-%s.%s'; + $fromImageFile = sprintf($tempFileTemplate, 'from', 'png'); + $toImageFile = sprintf($tempFileTemplate, 'to', 'png'); + $fromHtmlFile = sprintf($tempFileTemplate, 'from', 'html'); + $toHtmlFile = sprintf($tempFileTemplate, 'to', 'html'); + + file_put_contents($fromHtmlFile, $this->documentRenderer->render($docFrom)); + file_put_contents($toHtmlFile, $this->documentRenderer->render($docTo)); + + $prefix = Config::getSystemConfiguration('documents')['preview_url_prefix'] ?: $payload->schemeAndHost; + + try { + HtmlToImage::convert($prefix . $this->router->generate('opendxp_admin_document_document_diff_versions_html', ['id' => basename($fromHtmlFile)]), $fromImageFile); + HtmlToImage::convert($prefix . $this->router->generate('opendxp_admin_document_document_diff_versions_html', ['id' => basename($toHtmlFile)]), $toImageFile); + } finally { + unlink($fromHtmlFile); + unlink($toHtmlFile); + } + + $image1 = new Imagick($fromImageFile); + $image2 = new Imagick($toImageFile); + + if ($image1->getImageWidth() === $image2->getImageWidth() && $image1->getImageHeight() === $image2->getImageHeight()) { + $diff = $image1->compareImages($image2, Imagick::METRIC_MEANSQUAREERROR); + $diff[0]->setImageFormat('png'); + $image = base64_encode($diff[0]->getImageBlob()); + $diff[0]->clear(); + $diff[0]->destroy(); + $result = new DiffVersionsResult(supported: true, image: $image); + } else { + $result = new DiffVersionsResult( + supported: true, + image1: base64_encode(file_get_contents($fromImageFile)), + image2: base64_encode(file_get_contents($toImageFile)), + ); + } + + $image1->clear(); + $image1->destroy(); + $image2->clear(); + $image2->destroy(); + + unlink($fromImageFile); + unlink($toImageFile); + + return $result; + } +} diff --git a/src/Handler/Document/Version/DiffVersions/DiffVersionsPayload.php b/src/Handler/Document/Version/DiffVersions/DiffVersionsPayload.php new file mode 100644 index 00000000..c62f95e8 --- /dev/null +++ b/src/Handler/Document/Version/DiffVersions/DiffVersionsPayload.php @@ -0,0 +1,38 @@ +attributes->getInt('from'), + to: $request->attributes->getInt('to'), + schemeAndHost: $request->getSchemeAndHttpHost(), + ); + } +} diff --git a/src/Handler/Document/Version/DiffVersions/DiffVersionsResult.php b/src/Handler/Document/Version/DiffVersions/DiffVersionsResult.php new file mode 100644 index 00000000..ec8806a3 --- /dev/null +++ b/src/Handler/Document/Version/DiffVersions/DiffVersionsResult.php @@ -0,0 +1,28 @@ +userContext->getAdminUser()?->getId() ?? 0; + $version = Version::getById($payload->id); + $document = $version?->loadData(); + + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + $this->sessionService->saveDocument($document); + + $currentDocument = Document::getById($document->getId()); + if (!$currentDocument?->isAllowed('publish')) { + throw new AccessDeniedHttpException('Missing permission to publish document version'); + } + + $document->setPublished(true); + $document->setKey($currentDocument->getKey()); + $document->setPath($currentDocument->getRealPath()); + $document->setUserModification($userId); + $document->save(); + + $treeData = []; + $this->adminStyleEnricher->forEditor($document, $treeData); + + return new PublishVersionResult(treeData: $treeData); + } +} diff --git a/src/Handler/Document/Version/PublishVersion/PublishVersionResult.php b/src/Handler/Document/Version/PublishVersion/PublishVersionResult.php new file mode 100644 index 00000000..f061c88e --- /dev/null +++ b/src/Handler/Document/Version/PublishVersion/PublishVersionResult.php @@ -0,0 +1,25 @@ +id); + $document = $version?->loadData(); + + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + $this->sessionService->saveDocument($document); + } +} diff --git a/src/Handler/Element/AddNote/AddNoteHandler.php b/src/Handler/Element/AddNote/AddNoteHandler.php new file mode 100644 index 00000000..c7f246ef --- /dev/null +++ b/src/Handler/Element/AddNote/AddNoteHandler.php @@ -0,0 +1,23 @@ +setCid($payload->cid); + $note->setCtype($payload->ctype); + $note->setDate(time()); + $note->setTitle($payload->title); + $note->setDescription($payload->description); + $note->setType($payload->type); + $note->setLocked(false); + $note->save(); + } +} diff --git a/src/Handler/Element/AddNote/AddNotePayload.php b/src/Handler/Element/AddNote/AddNotePayload.php new file mode 100644 index 00000000..0c56a0d5 --- /dev/null +++ b/src/Handler/Element/AddNote/AddNotePayload.php @@ -0,0 +1,30 @@ +request->get('cid'), + ctype: $request->request->get('ctype'), + title: $request->request->get('title'), + description: $request->request->get('description'), + type: $request->request->get('type'), + ); + } +} diff --git a/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsHandler.php b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsHandler.php new file mode 100644 index 00000000..f4dee9ee --- /dev/null +++ b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsHandler.php @@ -0,0 +1,30 @@ +userId) { + $userList = []; + if ($user = Model\User::getById($payload->userId)) { + $userList[] = $user; + } + } else { + $userList = new Model\User\Listing(); + $userList->setCondition('`type` = ?', ['user']); + $userList = $userList->load(); + } + + $element = Element\Service::getElementById($payload->elementType, $payload->elementId); + $result = Element\PermissionChecker::check($element, $userList); + + return new AnalyzePermissionsResult(data: $result); + } +} diff --git a/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsPayload.php b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsPayload.php new file mode 100644 index 00000000..3b41038a --- /dev/null +++ b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsPayload.php @@ -0,0 +1,26 @@ +request->getInt('userId') ?: null, + elementType: $request->request->get('elementType'), + elementId: $request->request->getInt('elementId'), + ); + } +} diff --git a/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsResult.php b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsResult.php new file mode 100644 index 00000000..f2057c0e --- /dev/null +++ b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsResult.php @@ -0,0 +1,12 @@ +setCondition( + 'cid = ' . $versions->quote($payload->id) . + ' AND date <> ' . $versions->quote($payload->date) . + ' AND ctype = ' . $versions->quote($payload->type) + ); + foreach ($versions->load() as $version) { + $version->delete(); + } + } +} diff --git a/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsPayload.php b/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsPayload.php new file mode 100644 index 00000000..cb8566ff --- /dev/null +++ b/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsPayload.php @@ -0,0 +1,26 @@ +request->getInt('id'), + date: $request->request->get('date'), + type: $request->request->get('type'), + ); + } +} diff --git a/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsResult.php b/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsResult.php new file mode 100644 index 00000000..a9f7dbfa --- /dev/null +++ b/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsResult.php @@ -0,0 +1,10 @@ +id); + + if ($version) { + $version->delete(); + } + } +} diff --git a/src/Handler/Element/DeleteNote/DeleteNoteHandler.php b/src/Handler/Element/DeleteNote/DeleteNoteHandler.php new file mode 100644 index 00000000..e8aaf40c --- /dev/null +++ b/src/Handler/Element/DeleteNote/DeleteNoteHandler.php @@ -0,0 +1,27 @@ +id); + + if (!$note) { + return; + } + + if ($note->getLocked()) { + throw new BadRequestHttpException('note_is_locked'); + } + + $note->delete(); + } +} diff --git a/src/Handler/Element/DeleteVersion/DeleteVersionHandler.php b/src/Handler/Element/DeleteVersion/DeleteVersionHandler.php new file mode 100644 index 00000000..6c98e270 --- /dev/null +++ b/src/Handler/Element/DeleteVersion/DeleteVersionHandler.php @@ -0,0 +1,16 @@ +id)?->delete(); + } +} diff --git a/src/Handler/Element/FindUsages/FindUsagesHandler.php b/src/Handler/Element/FindUsages/FindUsagesHandler.php new file mode 100644 index 00000000..a176749e --- /dev/null +++ b/src/Handler/Element/FindUsages/FindUsagesHandler.php @@ -0,0 +1,67 @@ +id) { + $element = Element\Service::getElementById($payload->type, $payload->id); + } elseif ($payload->path) { + $element = Element\Service::getElementByPath($payload->type, $payload->path); + } + + if (!$element instanceof Element\ElementInterface) { + throw new NotFoundHttpException('Element not found'); + } + + $total = $element->getDependencies()->getRequiredByTotalCount(); + $results = []; + $hasHidden = false; + + if ($payload->sort !== null) { + $sort = json_decode($payload->sort)[0]; + $orderBy = $sort->property; + $orderDirection = $sort->direction; + } else { + $orderBy = null; + $orderDirection = null; + } + + $queryOffset = $payload->start; + $queryLimit = $payload->limit; + + while (count($results) < min($payload->limit, $total) && $queryOffset < $total) { + $elements = $element->getDependencies() + ->getRequiredByWithPath($queryOffset, $queryLimit, $orderBy, $orderDirection); + + foreach ($elements as $el) { + $item = Element\Service::getElementById($el['type'], (int) $el['id']); + + if ($item instanceof Element\ElementInterface) { + if ($item->isAllowed('list')) { + $results[] = $el; + } else { + $hasHidden = true; + } + } + } + + $queryOffset += count($elements); + $queryLimit = $payload->limit - count($results); + } + + return new FindUsagesResult( + data: $results, + total: $total, + hasHidden: $hasHidden, + ); + } +} diff --git a/src/Handler/Element/FindUsages/FindUsagesPayload.php b/src/Handler/Element/FindUsages/FindUsagesPayload.php new file mode 100644 index 00000000..8e22ff99 --- /dev/null +++ b/src/Handler/Element/FindUsages/FindUsagesPayload.php @@ -0,0 +1,32 @@ +query->getInt('id') ?: null, + type: $request->query->get('type'), + path: $request->query->get('path'), + limit: $request->query->getInt('limit', 50), + start: $request->query->getInt('start', 0), + sort: $request->query->get('sort'), + ); + } +} diff --git a/src/Handler/Element/FindUsages/FindUsagesResult.php b/src/Handler/Element/FindUsages/FindUsagesResult.php new file mode 100644 index 00000000..9a024bb4 --- /dev/null +++ b/src/Handler/Element/FindUsages/FindUsagesResult.php @@ -0,0 +1,14 @@ +id ?? ''; + $type = $payload->type ?? ''; + $baseUrl = $payload->baseUrl; + + $idList = explode(',', $ids); + + foreach ($idList as $id) { + try { + $element = Service::getElementById($type, (int) $id); + if (!$element) { + continue; + } + + if (!$hasDependency) { + $hasDependency = $element->getDependencies()->isRequired(); + } + } catch (Exception) { + Logger::err('failed to access element with id: ' . $id); + + continue; + } + + $event = null; + $eventName = null; + + if ($element instanceof Asset) { + $event = new AssetDeleteInfoEvent($element); + $eventName = AssetEvents::DELETE_INFO; + } elseif ($element instanceof Document) { + $event = new DocumentDeleteInfoEvent($element); + $eventName = DocumentEvents::DELETE_INFO; + } elseif ($element instanceof AbstractObject) { + $event = new DataObjectDeleteInfoEvent($element); + $eventName = DataObjectEvents::DELETE_INFO; + } + + if ($element->isLocked()) { + $itemResults[] = [ + 'id' => $element->getId(), + 'type' => $element->getType(), + 'key' => $element->getKey(), + 'reason' => 'Element is locked', + 'allowed' => false, + ]; + $errors |= true; + + continue; + } + + $this->eventDispatcher->dispatch($event, $eventName); + + if (!$event->getDeletionAllowed()) { + $itemResults[] = [ + 'id' => $element->getId(), + 'type' => $element->getType(), + 'key' => $element->getKey(), + 'reason' => $event->getReason(), + 'allowed' => false, + ]; + $errors |= true; + + continue; + } + + $itemResults[] = [ + 'id' => $element->getId(), + 'type' => $element->getType(), + 'key' => $element->getKey(), + 'path' => $element->getPath(), + 'allowed' => true, + ]; + + $deleteJobs[] = [[ + 'url' => $this->urlGenerator->generate('opendxp_admin_recyclebin_add'), + 'method' => 'POST', + 'params' => [ + 'type' => $type, + 'id' => $element->getId(), + ], + ]]; + + $hasChildren = $element->hasChildren(); + if (!$hasDependency) { + $hasDependency = $hasChildren; + } + + if ($hasChildren) { + $list = $element::getList(['unpublished' => true]); + $pathColumn = 'path'; + $list->setCondition($pathColumn . ' LIKE ?', [$element->getRealFullPath() . '/%']); + $children = $list->getTotalCount(); + $totalChildren += $children; + + if ($children > 0) { + $deleteObjectsPerRequest = 5; + for ($i = 0, $iMax = ceil($children / $deleteObjectsPerRequest); $i < $iMax; $i++) { + $deleteJobs[] = [[ + 'url' => $baseUrl . '/admin/' . $type . '/delete', + 'method' => 'DELETE', + 'params' => [ + 'step' => $i, + 'amount' => $deleteObjectsPerRequest, + 'type' => 'children', + 'id' => $element->getId(), + ], + ]]; + } + } + } + + $deleteJobs[] = [[ + 'url' => $baseUrl . '/admin/' . $type . '/delete', + 'method' => 'DELETE', + 'params' => [ + 'id' => $element->getId(), + ], + ]]; + } + + $elementKey = false; + if (count($idList) === 1) { + $element = Service::getElementById($type, (int) $idList[0]); + + if ($element instanceof ElementInterface) { + $elementKey = $element->getKey(); + } + } + + return [ + 'hasDependencies' => $hasDependency, + 'children' => $totalChildren, + 'deletejobs' => $deleteJobs, + 'batchDelete' => count($idList) > 1, + 'elementKey' => $elementKey, + 'errors' => $errors, + 'itemResults' => $itemResults, + ]; + } +} diff --git a/src/Handler/Element/GetDeleteInfo/GetDeleteInfoPayload.php b/src/Handler/Element/GetDeleteInfo/GetDeleteInfoPayload.php new file mode 100644 index 00000000..c6c9f0f0 --- /dev/null +++ b/src/Handler/Element/GetDeleteInfo/GetDeleteInfoPayload.php @@ -0,0 +1,26 @@ +query->get('id'), + type: $request->query->get('type'), + baseUrl: $request->getBaseUrl(), + ); + } +} diff --git a/src/Handler/Element/GetDependencies/GetDependenciesResult.php b/src/Handler/Element/GetDependencies/GetDependenciesResult.php new file mode 100644 index 00000000..b1e70081 --- /dev/null +++ b/src/Handler/Element/GetDependencies/GetDependenciesResult.php @@ -0,0 +1,12 @@ +query->getInt('id', 0), + type: $request->query->get('elementType'), + start: $request->query->getInt('start', 0), + limit: $request->query->getInt('limit', 25), + filter: $request->query->get('filter'), + ); + } +} diff --git a/src/Handler/Element/GetNicePath/GetNicePathHandler.php b/src/Handler/Element/GetNicePath/GetNicePathHandler.php new file mode 100644 index 00000000..9d5841f5 --- /dev/null +++ b/src/Handler/Element/GetNicePath/GetNicePathHandler.php @@ -0,0 +1,130 @@ +source; + if ($source['type'] !== 'object') { + throw new BadRequestHttpException('currently only objects as source elements are supported'); + } + + $result = []; + $id = $source['id']; + $sourceObject = DataObject\Concrete::getById($id); + + $ownerType = $payload->context['containerType']; + $fieldname = $payload->context['fieldname']; + + $fd = $this->getFieldDefinition($sourceObject, $payload->context); + $result = $this->convertResultWithPathFormatter($sourceObject, $payload->context, $result, $payload->targets); + + if ($payload->loadEditModeData) { + $methodName = 'get' . ucfirst($fieldname); + if ($ownerType === 'object' && method_exists($sourceObject, $methodName)) { + $data = DataObject\Service::useInheritedValues(true, [$sourceObject, $methodName]); + $editModeData = $fd->getDataForEditmode($data, $sourceObject); + // Inherited values show as an empty array + if (is_array($editModeData) && $editModeData !== []) { + foreach ($editModeData as $relationObjectAttribute) { + $relationObjectAttribute['$$nicepath'] = isset($relationObjectAttribute[$payload->idProperty], $result[$relationObjectAttribute[$payload->idProperty]]) + ? $result[$relationObjectAttribute[$payload->idProperty]] + : null; + + $result[$relationObjectAttribute[$payload->idProperty]] = $relationObjectAttribute; + } + } else { + foreach ($result as $resultItemId => $resultItem) { + $result[$resultItemId] = ['$$nicepath' => $resultItem]; + } + } + } else { + Logger::error('Loading edit mode data is not supported for ownertype: ' . $ownerType); + } + } + + return new GetNicePathResult(data: $result); + } + + /** + * @throws Exception + */ + private function getFieldDefinition(DataObject\Concrete $source, array $context): DataObject\ClassDefinition\Data|bool|null + { + $ownerType = $context['containerType']; + $fieldname = $context['fieldname']; + $fd = null; + + if ($ownerType === 'object') { + $subContainerType = $context['subContainerType'] ?? null; + if ($subContainerType) { + $subContainerKey = $context['subContainerKey']; + $subContainer = $source->getClass()->getFieldDefinition($subContainerKey); + if (method_exists($subContainer, 'getFieldDefinition')) { + $fd = $subContainer->getFieldDefinition($fieldname); + } + } else { + $fd = $source->getClass()->getFieldDefinition($fieldname); + } + } elseif ($ownerType === 'localizedfield') { + $localizedfields = $source->getClass()->getFieldDefinition('localizedfields'); + if ($localizedfields instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $fd = $localizedfields->getFieldDefinition($fieldname); + } + } elseif ($ownerType === 'objectbrick') { + $fdBrick = DataObject\Objectbrick\Definition::getByKey($context['containerKey']); + $fd = $fdBrick->getFieldDefinition($fieldname); + } elseif ($ownerType === 'fieldcollection') { + $containerKey = $context['containerKey']; + $fdCollection = DataObject\Fieldcollection\Definition::getByKey($containerKey); + if (($context['subContainerType'] ?? null) === 'localizedfield') { + /** @var DataObject\ClassDefinition\Data\Localizedfields $fdLocalizedFields */ + $fdLocalizedFields = $fdCollection->getFieldDefinition('localizedfields'); + $fd = $fdLocalizedFields->getFieldDefinition($fieldname); + } else { + $fd = $fdCollection->getFieldDefinition($fieldname); + } + } + + return $fd; + } + + /** + * @throws Exception + */ + private function convertResultWithPathFormatter(DataObject\Concrete $source, array $context, array $result, array $targets): array + { + $fd = $this->getFieldDefinition($source, $context); + + if ($fd instanceof DataObject\ClassDefinition\PathFormatterAwareInterface) { + $formatter = $fd->getPathFormatterClass(); + + if (null !== $formatter) { + $pathFormatter = DataObject\ClassDefinition\Helper\PathFormatterResolver::resolvePathFormatter( + $fd->getPathFormatterClass() + ); + + if ($pathFormatter instanceof DataObject\ClassDefinition\PathFormatterInterface) { + $result = $pathFormatter->formatPath($result, $source, $targets, [ + 'fd' => $fd, + 'context' => $context, + ]); + } + } + } + + return $result; + } +} diff --git a/src/Handler/Element/GetNicePath/GetNicePathPayload.php b/src/Handler/Element/GetNicePath/GetNicePathPayload.php new file mode 100644 index 00000000..93b898e1 --- /dev/null +++ b/src/Handler/Element/GetNicePath/GetNicePathPayload.php @@ -0,0 +1,47 @@ +|null */ + public readonly ?array $source; + + /** @var array|null */ + public readonly ?array $context; + + /** @var array|null */ + public readonly ?array $targets; + + public function __construct( + ?array $source, + ?array $context, + ?array $targets, + public readonly bool $loadEditModeData, + public readonly string $idProperty = 'id', + ) { + $this->source = $source; + $this->context = $context; + $this->targets = $targets; + } + + public static function fromRequest(Request $request): static + { + $source = $request->request->get('source'); + $context = $request->request->has('context') ? $request->request->get('context') : []; + $targets = $request->request->get('targets'); + + return new static( + source: $source !== null ? (json_decode($source, true) ?? null) : null, + context: $context !== [] ? (json_decode($context, true) ?? null) : null, + targets: $targets !== null ? (json_decode($targets, true) ?? null) : null, + loadEditModeData: $request->request->getBoolean('loadEditModeData'), + idProperty: $request->request->get('idProperty', 'id'), + ); + } +} diff --git a/src/Handler/Element/GetNicePath/GetNicePathResult.php b/src/Handler/Element/GetNicePath/GetNicePathResult.php new file mode 100644 index 00000000..9457e231 --- /dev/null +++ b/src/Handler/Element/GetNicePath/GetNicePathResult.php @@ -0,0 +1,12 @@ +setLimit($payload->limit); + $list->setOffset($payload->offset); + + if ($payload->sortingSettings['orderKey'] && $payload->sortingSettings['order']) { + $list->setOrderKey($payload->sortingSettings['orderKey']); + $list->setOrder($payload->sortingSettings['order']); + } else { + $list->setOrderKey(['date', 'id']); + $list->setOrder(['DESC', 'DESC']); + } + + $conditions = []; + + if ($payload->filterText) { + $conditions[] = '(' + . '`title` LIKE ' . $list->quote('%' . $payload->filterText . '%') + . ' OR `description` LIKE ' . $list->quote('%' . $payload->filterText . '%') + . ' OR `type` LIKE ' . $list->quote('%' . $payload->filterText . '%') + . ' OR `user` IN (SELECT `id` FROM `users` WHERE `name` LIKE ' . $list->quote('%' . $payload->filterText . '%') . ')' + . " OR DATE_FORMAT(FROM_UNIXTIME(`date`), '%Y-%m-%d') LIKE " . $list->quote('%' . $payload->filterText . '%') + . ')'; + } + + if ($payload->filterJson) { + $db = Db::get(); + $filters = json_decode($payload->filterJson, true) ?? []; + $propertyKey = 'property'; + $comparisonKey = 'operator'; + + foreach ($filters as $filter) { + $operator = '='; + + if ($filter['type'] === 'string') { + $operator = 'LIKE'; + } elseif ($filter['type'] === 'numeric') { + $operator = match ($filter[$comparisonKey] ?? '') { + 'lt' => '<', + 'gt' => '>', + default => '=', + }; + } elseif ($filter['type'] === 'date') { + $operator = match ($filter[$comparisonKey] ?? '') { + 'lt' => '<', + 'gt' => '>', + default => '=', + }; + $filter['value'] = strtotime($filter['value']); + } elseif ($filter[$comparisonKey] === 'list') { + $operator = '='; + } elseif ($filter[$comparisonKey] === 'boolean') { + $operator = '='; + $filter['value'] = (int) $filter['value']; + } + + $value = ($filter['value'] ?? ''); + if ($operator === 'LIKE') { + $value = '%' . $value . '%'; + } + + if ($filter[$propertyKey] === 'user') { + $conditions[] = '`user` IN (SELECT `id` FROM `users` WHERE `name` LIKE ' . $list->quote($value) . ')'; + } elseif ($filter['type'] === 'date' && ($filter[$comparisonKey] ?? '') === 'eq') { + $maxTime = $value + (86400 - 1); + $conditions[] = '`' . $filter[$propertyKey] . '` BETWEEN ' . $db->quote($value) . ' AND ' . $db->quote($maxTime); + } else { + $conditions[] = $db->quoteIdentifier($filter[$propertyKey]) . ' ' . $operator . ' ' . $db->quote($value); + } + } + } + + if ($payload->cid !== null && $payload->ctype !== null) { + $conditions[] = '(cid = ' . $list->quote($payload->cid) . ' AND ctype = ' . $list->quote($payload->ctype) . ')'; + } + + if ($conditions !== []) { + $list->setCondition(implode(' AND ', $conditions)); + } + + $list->load(); + + $notes = []; + foreach ($list->getNotes() as $note) { + $notes[] = Element\Service::getNoteData($note); + } + + return new GetNoteListResult(data: $notes, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Element/GetNoteList/GetNoteListResult.php b/src/Handler/Element/GetNoteList/GetNoteListResult.php new file mode 100644 index 00000000..677282ac --- /dev/null +++ b/src/Handler/Element/GetNoteList/GetNoteListResult.php @@ -0,0 +1,13 @@ +ctype) { + 'document' => $this->documentNoteTypes, + 'asset' => $this->assetNoteTypes, + 'object' => $this->objectNoteTypes, + default => [], + }; + + $noteTypes = []; + foreach ($config as $noteType) { + $noteTypes[] = ['name' => $noteType]; + } + + return new GetNoteTypesResult(noteTypes: $noteTypes); + } +} diff --git a/src/Handler/Element/GetNoteTypes/GetNoteTypesPayload.php b/src/Handler/Element/GetNoteTypes/GetNoteTypesPayload.php new file mode 100644 index 00000000..6d082207 --- /dev/null +++ b/src/Handler/Element/GetNoteTypes/GetNoteTypesPayload.php @@ -0,0 +1,22 @@ +query->get('ctype'), + ); + } +} diff --git a/src/Handler/Element/GetNoteTypes/GetNoteTypesResult.php b/src/Handler/Element/GetNoteTypes/GetNoteTypesResult.php new file mode 100644 index 00000000..1d29e82a --- /dev/null +++ b/src/Handler/Element/GetNoteTypes/GetNoteTypesResult.php @@ -0,0 +1,12 @@ +elementType, $allowedTypes, true)) { + $list = new Model\Property\Predefined\Listing(); + $list->setFilter(function (Property\Predefined $predefined) use ($payload) { + if (!str_contains($predefined->getCtype(), $payload->elementType)) { + return false; + } + + return !($payload->query && stripos($this->translator->trans($predefined->getName(), [], 'admin'), (string) $payload->query) === false); + }); + + foreach ($list->getProperties() as $predefined) { + $properties[] = $predefined->getObjectVars(); + } + } + + return new GetPredefinedPropertiesResult(properties: $properties); + } +} diff --git a/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesPayload.php b/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesPayload.php new file mode 100644 index 00000000..6dd7bfb9 --- /dev/null +++ b/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesPayload.php @@ -0,0 +1,24 @@ +query->get('elementType'), + query: $request->query->get('query'), + ); + } +} diff --git a/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesResult.php b/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesResult.php new file mode 100644 index 00000000..97a6384f --- /dev/null +++ b/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesResult.php @@ -0,0 +1,12 @@ +id) { + $element = Service::getElementById($payload->type, $payload->id); + } elseif ($payload->path) { + $element = Service::getElementByPath($payload->type, $payload->path); + } + + if (!$element instanceof ElementInterface) { + throw new NotFoundHttpException(); + } + + return new GetReplaceAssignmentsBatchJobsResult(jobs: $element->getDependencies()->getRequiredBy()); + } +} diff --git a/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsPayload.php b/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsPayload.php new file mode 100644 index 00000000..06103e5e --- /dev/null +++ b/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsPayload.php @@ -0,0 +1,26 @@ +query->getInt('id') ?: null, + type: $request->query->get('type'), + path: $request->query->get('path'), + ); + } +} diff --git a/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsResult.php b/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsResult.php new file mode 100644 index 00000000..becd4c84 --- /dev/null +++ b/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsResult.php @@ -0,0 +1,12 @@ +id || !in_array($payload->type, $allowedTypes)) { + return new GetDependenciesResult(data: false); + } + + $element = Model\Element\Service::getElementById($payload->type, $payload->id); + $dependencies = $element->getDependencies(); + + if ($payload->filter) { + $filters = json_decode($payload->filter, true) ?? []; + $value = null; + $elements = null; + + foreach ($filters as $filter) { + if ($filter['type'] === 'string') { + $value = ($filter['value'] ?? ''); + } + $elements = $element->getDependencies()->getFilterRequiredByPath($payload->start, $payload->limit, $value); + } + + if ($elements !== null && count($elements) > 0) { + $result = Model\Element\Service::getFilterRequiredByPathForFrontend($elements); + $result['total'] = count($result['requiredBy']); + + return new GetDependenciesResult(data: $result); + } + + return new GetDependenciesResult(data: $elements ?? []); + } + + if ($element instanceof Model\Element\ElementInterface) { + $dependenciesResult = Model\Element\Service::getRequiredByDependenciesForFrontend($dependencies, $payload->start, $payload->limit); + + $dependenciesResult['start'] = $payload->start; + $dependenciesResult['limit'] = $payload->limit; + $dependenciesResult['total'] = $dependencies->getRequiredByTotalCount(); + + return new GetDependenciesResult(data: $dependenciesResult); + } + + return new GetDependenciesResult(data: false); + } +} diff --git a/src/Handler/Element/GetRequiredByDependencies/GetRequiredByDependenciesPayload.php b/src/Handler/Element/GetRequiredByDependencies/GetRequiredByDependenciesPayload.php new file mode 100644 index 00000000..ed83002b --- /dev/null +++ b/src/Handler/Element/GetRequiredByDependencies/GetRequiredByDependenciesPayload.php @@ -0,0 +1,30 @@ +query->getInt('id', 0), + type: $request->query->get('elementType'), + start: $request->query->getInt('start', 0), + limit: $request->query->getInt('limit', 25), + filter: $request->query->get('filter'), + ); + } +} diff --git a/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesHandler.php b/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesHandler.php new file mode 100644 index 00000000..92d31212 --- /dev/null +++ b/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesHandler.php @@ -0,0 +1,58 @@ +id || !in_array($payload->type, $allowedTypes)) { + return new GetDependenciesResult(data: false); + } + + $element = Model\Element\Service::getElementById($payload->type, $payload->id); + $dependencies = $element->getDependencies(); + + if ($payload->filter) { + $filters = json_decode($payload->filter, true) ?? []; + $value = null; + $elements = null; + + foreach ($filters as $filter) { + if ($filter['type'] === 'string') { + $value = ($filter['value'] ?? ''); + } + $elements = $element->getDependencies()->getFilterRequiresByPath($payload->start, $payload->limit, $value); + } + + if ($elements !== null && count($elements) > 0) { + $result = Model\Element\Service::getFilterRequiresForFrontend($elements); + $result['total'] = count($result['requires']); + + return new GetDependenciesResult(data: $result); + } + + return new GetDependenciesResult(data: $elements ?? []); + } + + if ($element instanceof Model\Element\ElementInterface) { + $dependenciesResult = Model\Element\Service::getRequiresDependenciesForFrontend($dependencies, $payload->start, $payload->limit); + + $dependenciesResult['start'] = $payload->start; + $dependenciesResult['limit'] = $payload->limit; + $dependenciesResult['total'] = $dependencies->getRequiresTotalCount(); + + return new GetDependenciesResult(data: $dependenciesResult); + } + + return new GetDependenciesResult(data: false); + } +} diff --git a/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesPayload.php b/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesPayload.php new file mode 100644 index 00000000..17f659c8 --- /dev/null +++ b/src/Handler/Element/GetRequiresDependencies/GetRequiresDependenciesPayload.php @@ -0,0 +1,30 @@ +query->getInt('id', 0), + type: $request->query->get('elementType'), + start: $request->query->getInt('start', 0), + limit: $request->query->getInt('limit', 25), + filter: $request->query->get('filter'), + ); + } +} diff --git a/src/Handler/Element/GetSubtype/GetSubtypeHandler.php b/src/Handler/Element/GetSubtype/GetSubtypeHandler.php new file mode 100644 index 00000000..6700e2eb --- /dev/null +++ b/src/Handler/Element/GetSubtype/GetSubtypeHandler.php @@ -0,0 +1,50 @@ +id); + + $event = new ResolveElementEvent($payload->type, $idOrPath); + OpenDxp::getEventDispatcher()->dispatch($event, AdminEvents::RESOLVE_ELEMENT); + $idOrPath = $event->getId(); + $resolvedType = $event->getType(); + + if (is_numeric($idOrPath)) { + $el = Element\Service::getElementById($resolvedType, (int) $idOrPath); + } elseif ($resolvedType === 'document') { + $el = Document\Service::getByUrl($idOrPath); + } else { + $el = Element\Service::getElementByPath($resolvedType, $idOrPath); + } + + if (!$el) { + throw new NotFoundHttpException('Element not found'); + } + + $subtype = null; + if ($el instanceof Asset || $el instanceof Document) { + $subtype = $el->getType(); + } elseif ($el instanceof DataObject\Concrete) { + $subtype = $el->getClassName(); + } elseif ($el instanceof DataObject\Folder) { + $subtype = 'folder'; + } + + return new GetSubtypeResult(subtype: $subtype, id: $el->getId(), type: $payload->type); + } +} diff --git a/src/Handler/Element/GetSubtype/GetSubtypePayload.php b/src/Handler/Element/GetSubtype/GetSubtypePayload.php new file mode 100644 index 00000000..5120e023 --- /dev/null +++ b/src/Handler/Element/GetSubtype/GetSubtypePayload.php @@ -0,0 +1,24 @@ +query->getString('id'), + type: $request->query->get('type'), + ); + } +} diff --git a/src/Handler/Element/GetSubtype/GetSubtypeResult.php b/src/Handler/Element/GetSubtype/GetSubtypeResult.php new file mode 100644 index 00000000..ffd365a2 --- /dev/null +++ b/src/Handler/Element/GetSubtype/GetSubtypeResult.php @@ -0,0 +1,14 @@ +userContext->getAdminUser(); + $allowedTypes = ['asset', 'document', 'object']; + + if (!$payload->id || !in_array($payload->elementType, $allowedTypes)) { + throw new NotFoundHttpException('Element type not found'); + } + + $element = Model\Element\Service::getElementById($payload->elementType, $payload->id); + if (!$element) { + throw new NotFoundHttpException($payload->elementType . ' with id [' . $payload->id . "] doesn't exist"); + } + + if (!$element->isAllowed('versions')) { + throw new AccessDeniedHttpException('Permission denied, ' . $payload->elementType . ' id [' . $payload->id . ']'); + } + + $schedule = $element->getScheduledTasks(); + $schedules = []; + foreach ($schedule as $task) { + if ($task->getActive()) { + $schedules[$task->getVersion()] = $task->getDate(); + } + } + + $list = new Version\Listing(); + $list->setLoadAutoSave(true); + $list->setCondition('cid = ? AND ctype = ? AND (autoSave=0 OR (autoSave=1 AND userId = ?)) ', [ + $element->getId(), + Element\Service::getElementType($element), + $adminUser->getId(), + ]) + ->setOrderKey('date') + ->setOrder('ASC'); + + $versions = $list->load(); + $versions = Model\Element\Service::getSafeVersionInfo($versions); + $versions = array_reverse($versions); + + foreach ($versions as &$version) { + $version['scheduled'] = null; + if (array_key_exists($version['id'], $schedules)) { + $version['scheduled'] = $schedules[$version['id']]; + } + } + + return new GetVersionsResult(versions: $versions); + } +} diff --git a/src/Handler/Element/GetVersions/GetVersionsPayload.php b/src/Handler/Element/GetVersions/GetVersionsPayload.php new file mode 100644 index 00000000..12436b20 --- /dev/null +++ b/src/Handler/Element/GetVersions/GetVersionsPayload.php @@ -0,0 +1,24 @@ +query->getInt('id', 0), + elementType: $request->query->get('elementType'), + ); + } +} diff --git a/src/Handler/Element/GetVersions/GetVersionsResult.php b/src/Handler/Element/GetVersions/GetVersionsResult.php new file mode 100644 index 00000000..32e84760 --- /dev/null +++ b/src/Handler/Element/GetVersions/GetVersionsResult.php @@ -0,0 +1,12 @@ +id, $payload->type, $payload->sessionId); + } +} diff --git a/src/Handler/Element/LockElement/LockElementPayload.php b/src/Handler/Element/LockElement/LockElementPayload.php new file mode 100644 index 00000000..b912759e --- /dev/null +++ b/src/Handler/Element/LockElement/LockElementPayload.php @@ -0,0 +1,26 @@ +request->getInt('id'), + type: $request->request->getString('type'), + sessionId: $request->getSession()->getId(), + ); + } +} diff --git a/src/Handler/Element/LockElement/LockElementResult.php b/src/Handler/Element/LockElement/LockElementResult.php new file mode 100644 index 00000000..ed674b39 --- /dev/null +++ b/src/Handler/Element/LockElement/LockElementResult.php @@ -0,0 +1,10 @@ +request->has('data')) { + $data = json_decode($request->request->getString('data'), true) ?? []; + + return new static( + hasData: true, + id: (int) ($data['id'] ?? 0), + ); + } + + return new static( + hasData: false, + offset: $request->request->getInt('start', 0), + limit: $request->request->getInt('limit') ?: null, + sortingSettings: QueryParams::extractSortingSettings($request->request->all()), + filterText: $request->request->get('filterText'), + filterJson: $request->request->get('filter'), + cid: $request->request->has('cid') ? $request->request->getString('cid') : null, + ctype: $request->request->has('ctype') ? $request->request->getString('ctype') : null, + ); + } +} diff --git a/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsHandler.php b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsHandler.php new file mode 100644 index 00000000..ebb343fa --- /dev/null +++ b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsHandler.php @@ -0,0 +1,59 @@ +userContext->getAdminUser(); + $element = Element\Service::getElementById($payload->type, $payload->id); + $sourceEl = Element\Service::getElementById($payload->sourceType, $payload->sourceId); + $targetEl = Element\Service::getElementById($payload->targetType, $payload->targetId); + + if (!$element || !$sourceEl || !$targetEl) { + throw new NotFoundHttpException('One or more elements not found'); + } + + if ($payload->sourceType !== $payload->targetType || $sourceEl->getType() !== $targetEl->getType()) { + throw new BadRequestHttpException('source-type and target-type do not match'); + } + + if (!$element->isAllowed('save')) { + throw new AccessDeniedHttpException(); + } + + $rewriteConfig = [ + $payload->sourceType => [ + $sourceEl->getId() => $targetEl->getId(), + ], + ]; + + if ($element instanceof Document) { + $element = Document\Service::rewriteIds($element, $rewriteConfig); + } elseif ($element instanceof DataObject\AbstractObject) { + $element = DataObject\Service::rewriteIds($element, $rewriteConfig); + } elseif ($element instanceof Asset) { + $element = Asset\Service::rewriteIds($element, $rewriteConfig); + } + + $element->setUserModification($adminUser->getId()); + $element->save(); + } +} diff --git a/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsPayload.php b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsPayload.php new file mode 100644 index 00000000..3f56a998 --- /dev/null +++ b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsPayload.php @@ -0,0 +1,32 @@ +request->get('type'), + id: $request->request->getInt('id'), + sourceType: $request->request->get('sourceType'), + sourceId: $request->request->getInt('sourceId'), + targetType: $request->request->get('targetType'), + targetId: $request->request->getInt('targetId'), + ); + } +} diff --git a/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsResult.php b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsResult.php new file mode 100644 index 00000000..f10e6945 --- /dev/null +++ b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsResult.php @@ -0,0 +1,10 @@ +type === 'asset') { + $element = Asset::getById($payload->id); + } elseif ($payload->type === 'document') { + $element = Document::getById($payload->id); + } else { + $element = DataObject::getById($payload->id); + } + + if (!$element) { + throw new NotFoundHttpException('Element not found'); + } + + return new TypePathResult( + index: method_exists($element, 'getIndex') ? (int) $element->getIndex() : 0, + idPath: Element\Service::getIdPath($element), + typePath: Element\Service::getTypePath($element), + fullpath: $element->getRealFullPath(), + sortIndexPath: $payload->type !== 'asset' ? Element\Service::getSortIndexPath($element) : null, + ); + } +} diff --git a/src/Handler/Element/TypePath/TypePathPayload.php b/src/Handler/Element/TypePath/TypePathPayload.php new file mode 100644 index 00000000..9b3d86ae --- /dev/null +++ b/src/Handler/Element/TypePath/TypePathPayload.php @@ -0,0 +1,24 @@ +query->getInt('id', 0), + type: $request->query->get('type'), + ); + } +} diff --git a/src/Handler/Element/TypePath/TypePathResult.php b/src/Handler/Element/TypePath/TypePathResult.php new file mode 100644 index 00000000..3a603e32 --- /dev/null +++ b/src/Handler/Element/TypePath/TypePathResult.php @@ -0,0 +1,16 @@ +id, $payload->type); + } +} diff --git a/src/Handler/Element/UnlockElement/UnlockElementPayload.php b/src/Handler/Element/UnlockElement/UnlockElementPayload.php new file mode 100644 index 00000000..dc57f11e --- /dev/null +++ b/src/Handler/Element/UnlockElement/UnlockElementPayload.php @@ -0,0 +1,24 @@ +request->getInt('id'), + type: $request->request->getString('type'), + ); + } +} diff --git a/src/Handler/Element/UnlockElement/UnlockElementResult.php b/src/Handler/Element/UnlockElement/UnlockElementResult.php new file mode 100644 index 00000000..0c2d1e72 --- /dev/null +++ b/src/Handler/Element/UnlockElement/UnlockElementResult.php @@ -0,0 +1,10 @@ +elements as $element) { + Editlock::unlock((int) $element['id'], $element['type']); + } + } +} diff --git a/src/Handler/Element/UnlockElements/UnlockElementsPayload.php b/src/Handler/Element/UnlockElements/UnlockElementsPayload.php new file mode 100644 index 00000000..200ba2bc --- /dev/null +++ b/src/Handler/Element/UnlockElements/UnlockElementsPayload.php @@ -0,0 +1,25 @@ + $elements */ + public function __construct( + public readonly array $elements, + ) {} + + public static function fromRequest(Request $request): static + { + $body = json_decode($request->getContent(), true) ?? []; + + return new static( + elements: $body['elements'] ?? [], + ); + } +} diff --git a/src/Handler/Element/UnlockElements/UnlockElementsResult.php b/src/Handler/Element/UnlockElements/UnlockElementsResult.php new file mode 100644 index 00000000..8014c1dd --- /dev/null +++ b/src/Handler/Element/UnlockElements/UnlockElementsResult.php @@ -0,0 +1,10 @@ +type, $payload->id); + if (!$element) { + return new UnlockPropagateResult(success: false); + } + + $element->unlockPropagate(); + + return new UnlockPropagateResult(success: true); + } +} diff --git a/src/Handler/Element/UnlockPropagate/UnlockPropagatePayload.php b/src/Handler/Element/UnlockPropagate/UnlockPropagatePayload.php new file mode 100644 index 00000000..08667d92 --- /dev/null +++ b/src/Handler/Element/UnlockPropagate/UnlockPropagatePayload.php @@ -0,0 +1,24 @@ +request->get('type'), + id: $request->request->getInt('id'), + ); + } +} diff --git a/src/Handler/Element/UnlockPropagate/UnlockPropagateResult.php b/src/Handler/Element/UnlockPropagate/UnlockPropagateResult.php new file mode 100644 index 00000000..d4100da1 --- /dev/null +++ b/src/Handler/Element/UnlockPropagate/UnlockPropagateResult.php @@ -0,0 +1,12 @@ +data ?? []; + $version = Version::getById($data['id']); + + if ($version && ($data['public'] != $version->getPublic() || $data['note'] != $version->getNote())) { + $version->setPublic($data['public']); + $version->setNote($data['note']); + $version->save(); + } + } +} diff --git a/src/Handler/Element/VersionUpdate/VersionUpdatePayload.php b/src/Handler/Element/VersionUpdate/VersionUpdatePayload.php new file mode 100644 index 00000000..b043301d --- /dev/null +++ b/src/Handler/Element/VersionUpdate/VersionUpdatePayload.php @@ -0,0 +1,30 @@ +|null */ + public readonly ?array $data; + + public function __construct(?array $data) + { + $this->data = $data; + } + + public static function fromRequest(Request $request): static + { + $data = $request->request->get('data'); + + return new static( + data: $data !== null + ? (json_decode($data, true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Element/VersionUpdate/VersionUpdateResult.php b/src/Handler/Element/VersionUpdate/VersionUpdateResult.php new file mode 100644 index 00000000..f2fd2bb6 --- /dev/null +++ b/src/Handler/Element/VersionUpdate/VersionUpdateResult.php @@ -0,0 +1,10 @@ +data; + unset($data['id']); + + $address = new Tool\Email\Blocklist(); + $address->setValues($data); + $address->save(); + + return $address->getObjectVars(); + } +} diff --git a/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryHandler.php b/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryHandler.php new file mode 100644 index 00000000..877f133d --- /dev/null +++ b/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryHandler.php @@ -0,0 +1,17 @@ +data['address']); + $entry->delete(); + } +} diff --git a/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryHandler.php b/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryHandler.php new file mode 100644 index 00000000..5b7c3614 --- /dev/null +++ b/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryHandler.php @@ -0,0 +1,20 @@ +data['address']); + $address->setValues($payload->data); + $address->save(); + + return $address->getObjectVars(); + } +} diff --git a/src/Handler/Email/BlocklistPayload.php b/src/Handler/Email/BlocklistPayload.php new file mode 100644 index 00000000..c7623216 --- /dev/null +++ b/src/Handler/Email/BlocklistPayload.php @@ -0,0 +1,50 @@ +request->has('data')) { + $data = json_decode($request->request->getString('data'), true) ?? []; + + if (is_array($data)) { + foreach ($data as $key => &$value) { + if (is_string($value)) { + if ($key === 'address') { + $value = filter_var($value, FILTER_SANITIZE_EMAIL); + } + $value = trim($value); + } + } + unset($value); + } + + return new static(hasData: true, data: $data); + } + + return new static( + hasData: false, + limit: $request->request->getInt('limit', 50), + offset: $request->request->getInt('start', 0), + sortingSettings: QueryParams::extractSortingSettings($request->request->all()), + filter: $request->request->has('filter') ? $request->request->getString('filter') : null, + ); + } +} diff --git a/src/Handler/Email/DeleteEmailLog/DeleteEmailLogHandler.php b/src/Handler/Email/DeleteEmailLog/DeleteEmailLogHandler.php new file mode 100644 index 00000000..232b75b8 --- /dev/null +++ b/src/Handler/Email/DeleteEmailLog/DeleteEmailLogHandler.php @@ -0,0 +1,22 @@ +id); + if (!$emailLog instanceof Tool\Email\Log) { + throw new NotFoundHttpException('Email log with ID ' . $payload->id . ' not found.'); + } + + $emailLog->delete(); + } +} diff --git a/src/Handler/Email/GetBlocklist/GetBlocklistHandler.php b/src/Handler/Email/GetBlocklist/GetBlocklistHandler.php new file mode 100644 index 00000000..e13e3750 --- /dev/null +++ b/src/Handler/Email/GetBlocklist/GetBlocklistHandler.php @@ -0,0 +1,36 @@ +setLimit($payload->limit); + $list->setOffset($payload->offset); + + if ($payload->sortingSettings['orderKey']) { + $list->setOrderKey($payload->sortingSettings['orderKey']); + $list->setOrder($payload->sortingSettings['order']); + } + + if ($payload->filter !== null) { + $list->setCondition('`address` LIKE ' . $list->quote('%' . $payload->filter . '%')); + } + + $data = $list->load(); + $jsonData = []; + foreach ($data as $entry) { + $jsonData[] = $entry->getObjectVars(); + } + + return new GetBlocklistResult(data: $jsonData, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Email/GetBlocklist/GetBlocklistResult.php b/src/Handler/Email/GetBlocklist/GetBlocklistResult.php new file mode 100644 index 00000000..eff03732 --- /dev/null +++ b/src/Handler/Email/GetBlocklist/GetBlocklistResult.php @@ -0,0 +1,13 @@ +documentId !== null) { + $list->setCondition('documentId = ' . $payload->documentId); + } + + $list->setLimit($payload->limit); + $list->setOffset($payload->start); + $list->setOrderKey('sentDate'); + $list->setOrder('DESC'); + + if ($payload->filter !== null) { + $filter = $payload->filter === '*' ? '' : $payload->filter; + + $filter = str_replace('%', '*', $filter); + $filter = htmlspecialchars($filter, ENT_QUOTES); + + if (strpos($filter, '@')) { + $parts = explode(' ', $filter); + $parts = array_map(static function ($part) { + if (strpos($part, '@')) { + return '"' . $part . '"'; + } + + return $part; + }, $parts); + $filter = implode(' ', $parts); + } + + if (str_starts_with($filter, '@')) { + $filter = str_replace('@', '', $filter); + } + + $condition = '( MATCH (`from`,`to`,`cc`,`bcc`,`subject`,`params`) AGAINST (' . $list->quote($filter) . ' IN BOOLEAN MODE) )'; + + if ($payload->documentId !== null) { + $condition .= 'AND documentId = ' . $payload->documentId; + } + + $list->setCondition($condition); + } + + $data = $list->load(); + $jsonData = []; + + foreach ($data as $entry) { + $tmp = $entry->getObjectVars(); + unset($tmp['bodyHtml'], $tmp['bodyText']); + $jsonData[] = $tmp; + } + + return new GetEmailLogsResult(data: $jsonData, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Email/GetEmailLogs/GetEmailLogsPayload.php b/src/Handler/Email/GetEmailLogs/GetEmailLogsPayload.php new file mode 100644 index 00000000..c425777a --- /dev/null +++ b/src/Handler/Email/GetEmailLogs/GetEmailLogsPayload.php @@ -0,0 +1,28 @@ +request->has('documentId') ? (int) $request->request->get('documentId') : null, + limit: (int) $request->request->get('limit', 50), + start: (int) $request->request->get('start', 0), + filter: $request->request->has('filter') ? $request->request->get('filter') : null, + ); + } +} diff --git a/src/Handler/Email/GetEmailLogs/GetEmailLogsResult.php b/src/Handler/Email/GetEmailLogs/GetEmailLogsResult.php new file mode 100644 index 00000000..7a26f488 --- /dev/null +++ b/src/Handler/Email/GetEmailLogs/GetEmailLogsResult.php @@ -0,0 +1,13 @@ +id); + if (!$emailLog instanceof Tool\Email\Log) { + throw new NotFoundHttpException('Email log with ID ' . $payload->id . ' not found.'); + } + + $mail = new Mail(); + $mail->preventDebugInformationAppending(); + $mail->setIgnoreDebugMode(true); + + if (!empty($payload->fieldOverrides['to'])) { + $emailLog->setTo(null); + $emailLog->setCc(null); + $emailLog->setBcc(null); + } else { + $mail->disableLogging(); + } + + if ($html = $emailLog->getHtmlLog()) { + $mail->html($html); + } + + if ($text = $emailLog->getTextLog()) { + $mail->text($text); + } + + foreach (['From', 'To', 'Cc', 'Bcc', 'ReplyTo'] as $field) { + $overrideKey = strtolower($field); + if (!empty($payload->fieldOverrides[$overrideKey])) { + $values = $payload->fieldOverrides[$overrideKey]; + } else { + $getter = 'get' . $field; + $values = $emailLog->{$getter}(); + } + + $values = \OpenDxp\Helper\Mail::parseEmailAddressField($values); + + if ($values) { + [$value] = $values; + $mail->{'add' . $field}(new Address($value['email'], $value['name'])); + } + } + + $mail->subject($emailLog->getSubject()); + + if ($emailLog->getDocumentId()) { + $mail->setDocument($emailLog->getDocumentId()); + } + + try { + $params = $emailLog->getParams(); + } catch (\Exception) { + Logger::warning('Could not decode JSON param string'); + $params = []; + } + + foreach ($params as $entry) { + $data = null; + $hasChildren = isset($entry['children']) && is_array($entry['children']); + + if ($hasChildren) { + $childData = []; + foreach ($entry['children'] as $childParam) { + $childData[$childParam['key']] = $this->parseLoggingParamObject($childParam); + } + $data = $childData; + } else { + $data = $this->parseLoggingParamObject($entry); + } + + $mail->setParam($entry['key'], $data); + } + + $mail->send(); + } + + private function parseLoggingParamObject(array $params): mixed + { + if ($params['data']['type'] === 'object') { + $class = '\\' . ltrim($params['data']['objectClass'], '\\'); + $reflection = new ReflectionClass($class); + + if (!empty($params['data']['objectId']) && $reflection->implementsInterface(ElementInterface::class)) { + $obj = $class::getById($params['data']['objectId']); + if (!is_null($obj)) { + return $obj; + } + } + + return null; + } + + return $params['data']['value']; + } +} diff --git a/src/Handler/Email/ResendEmail/ResendEmailPayload.php b/src/Handler/Email/ResendEmail/ResendEmailPayload.php new file mode 100644 index 00000000..a24ee5d2 --- /dev/null +++ b/src/Handler/Email/ResendEmail/ResendEmailPayload.php @@ -0,0 +1,35 @@ + */ + public readonly array $fieldOverrides; + + public function __construct( + public readonly int $id, + ?array $fieldOverrides, + ) { + $this->fieldOverrides = $fieldOverrides ?? []; + } + + public static function fromRequest(Request $request): static + { + return new static( + id: (int) $request->request->get('id'), + fieldOverrides: [ + 'from' => $request->request->get('from') ?: null, + 'to' => $request->request->get('to') ?: null, + 'cc' => $request->request->get('cc') ?: null, + 'bcc' => $request->request->get('bcc') ?: null, + 'replyto' => $request->request->get('replyto') ?: null, + ], + ); + } +} diff --git a/src/Handler/Email/SendTestEmail/SendTestEmailHandler.php b/src/Handler/Email/SendTestEmail/SendTestEmailHandler.php new file mode 100644 index 00000000..65b51a87 --- /dev/null +++ b/src/Handler/Email/SendTestEmail/SendTestEmailHandler.php @@ -0,0 +1,57 @@ +emailType === 'text') { + $mail->text(strip_tags($payload->content ?? '')); + } elseif ($payload->emailType === 'html') { + $mail->html($payload->content ?? ''); + } elseif ($payload->emailType === 'document') { + $doc = Document::getByPath($payload->documentPath ?? ''); + + if (!$doc instanceof Document\Email) { + throw new BadRequestHttpException('Email document not found!'); + } + + $mail->setDocument($doc); + + if ($payload->mailParameters) { + foreach ($payload->mailParameters as $mailParam) { + if ($mailParam['key']) { + $mail->setParam($mailParam['key'], $mailParam['value']); + } + } + } + } + + if ($payload->from) { + $addressArray = \OpenDxp\Helper\Mail::parseEmailAddressField($payload->from); + if ($addressArray) { + [$cleanedFromAddress] = $addressArray; + $mail->from(new Address($cleanedFromAddress['email'], $cleanedFromAddress['name'])); + } + } + + $toAddresses = \OpenDxp\Helper\Mail::parseEmailAddressField($payload->to); + foreach ($toAddresses as $cleanedToAddress) { + $mail->addTo($cleanedToAddress['email'], $cleanedToAddress['name']); + } + + $mail->subject($payload->subject); + $mail->setIgnoreDebugMode(true); + $mail->send(); + } +} diff --git a/src/Handler/Email/SendTestEmail/SendTestEmailPayload.php b/src/Handler/Email/SendTestEmail/SendTestEmailPayload.php new file mode 100644 index 00000000..2015af4e --- /dev/null +++ b/src/Handler/Email/SendTestEmail/SendTestEmailPayload.php @@ -0,0 +1,44 @@ +|null */ + public readonly ?array $mailParameters; + + public function __construct( + public readonly string $emailType, + public readonly ?string $content, + public readonly ?string $documentPath, + ?array $mailParameters, + public readonly ?string $from, + public readonly string $to, + public readonly string $subject, + ) { + $this->mailParameters = $mailParameters; + } + + public static function fromRequest(Request $request): static + { + $mailParameters = null; + if ($request->request->has('mailParamaters')) { + $mailParameters = json_decode($request->request->get('mailParamaters'), true) ?: null; + } + + return new static( + emailType: (string) $request->request->get('emailType'), + content: $request->request->get('content'), + documentPath: $request->request->get('documentPath'), + mailParameters: $mailParameters, + from: $request->request->get('from'), + to: (string) $request->request->get('to'), + subject: (string) $request->request->get('subject'), + ); + } +} diff --git a/src/Handler/Email/ShowEmailLog/GetEmailLogParams/GetEmailLogParamsHandler.php b/src/Handler/Email/ShowEmailLog/GetEmailLogParams/GetEmailLogParamsHandler.php new file mode 100644 index 00000000..36dfa928 --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogParams/GetEmailLogParamsHandler.php @@ -0,0 +1,106 @@ +getParams(); + } catch (\Exception) { + Logger::warning('Could not decode JSON param string'); + $params = []; + } + + foreach ($params as &$entry) { + $this->enhanceLoggingData($entry); + } + + return $params; + } + + private function enhanceLoggingData(?array &$data, ?array &$fullEntry = null): void + { + if (!is_array($data)) { + return; + } + + if (!empty($data['objectClass'])) { + $class = '\\' . ltrim($data['objectClass'], '\\'); + $reflection = new ReflectionClass($class); + + if (!empty($data['objectId']) && $reflection->implementsInterface(ElementInterface::class)) { + $obj = $class::getById($data['objectId']); + $data['objectPath'] = is_null($obj) ? '' : $obj->getRealFullPath(); + + if (stristr($class, '\\OpenDxp\\Model') === false) { + $niceClassName = '\\' . ltrim($reflection->getParentClass()->getName(), '\\'); + } else { + $niceClassName = $class; + } + + $niceClassName = str_replace(['\\OpenDxp\\Model\\', '_'], ['', '\\'], $niceClassName); + + $tmp = explode('\\', $niceClassName); + if (in_array($tmp[0], ['DataObject', 'Document', 'Asset'])) { + $data['objectClassBase'] = $tmp[0]; + $data['objectClassSubType'] = $tmp[1]; + } + } + } + + foreach ($data as &$value) { + if (!is_array($value)) { + continue; + } + + $this->enhanceLoggingData($value, $data); + } + + unset($value); + + if ($data['children'] ?? false) { + foreach ($data['children'] as $key => $entry) { + if (is_string($key)) { + unset($data['children'][$key]); + } + } + $data['iconCls'] = 'opendxp_icon_folder'; + $data['data'] = ['type' => 'simple', 'value' => 'Children (' . count($data['children']) . ')']; + } else { + if (empty($data['iconCls'])) { + if (($data['objectClassBase'] ?? '') === 'DataObject') { + $fullEntry['iconCls'] = 'opendxp_icon_object'; + } elseif (($data['objectClassBase'] ?? '') === 'Asset') { + $data['iconCls'] = match ($data['objectClassSubType']) { + 'Image' => 'opendxp_icon_image', + 'Video' => 'opendxp_icon_wmv', + 'Text' => 'opendxp_icon_txt', + 'Document' => 'opendxp_icon_pdf', + default => 'opendxp_icon_asset', + }; + } elseif (str_starts_with($data['objectClass'] ?? '', 'Document')) { + $fullEntry['iconCls'] = 'opendxp_icon_' . strtolower($data['objectClassSubType']); + } else { + $data['iconCls'] = 'opendxp_icon_text'; + } + } + + $data['leaf'] = true; + } + } +} diff --git a/src/Handler/Email/ShowEmailLog/GetEmailLogResult.php b/src/Handler/Email/ShowEmailLog/GetEmailLogResult.php new file mode 100644 index 00000000..15c00d2e --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogResult.php @@ -0,0 +1,14 @@ +getTextLog() ?: null, + htmlLog: $log->getHtmlLog() ?: null, + objectVars: $log->getObjectVars(), + ); + } +} diff --git a/src/Handler/Email/ShowEmailLog/ShowEmailLogPayload.php b/src/Handler/Email/ShowEmailLog/ShowEmailLogPayload.php new file mode 100644 index 00000000..75cfce8a --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/ShowEmailLogPayload.php @@ -0,0 +1,24 @@ +query->get('type'), + id: $request->query->getInt('id', 0), + ); + } +} diff --git a/src/Handler/GDPR/Asset/ExportAsset/ExportAssetHandler.php b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetHandler.php new file mode 100644 index 00000000..0bc094a0 --- /dev/null +++ b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetHandler.php @@ -0,0 +1,44 @@ +id); + if (!$asset) { + throw new NotFoundHttpException('Asset not found'); + } + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('Export denied'); + } + + return new ExportAssetResult($this->assets->doExportData($asset)); + } +} diff --git a/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php new file mode 100644 index 00000000..9838ea12 --- /dev/null +++ b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php @@ -0,0 +1,27 @@ +assets->searchData( + $payload->id, + $payload->firstname, + $payload->lastname, + $payload->email, + $payload->start, + $payload->limit, + $payload->sort, + ); + + return new SearchAssetsResult($result); + } +} diff --git a/src/Handler/GDPR/Asset/SearchAssets/SearchAssetsResult.php b/src/Handler/GDPR/Asset/SearchAssets/SearchAssetsResult.php new file mode 100644 index 00000000..41bd58d6 --- /dev/null +++ b/src/Handler/GDPR/Asset/SearchAssets/SearchAssetsResult.php @@ -0,0 +1,25 @@ +id); + if (!$object) { + throw new NotFoundHttpException('Object not found'); + } + if (!$object->isAllowed('view')) { + throw new AccessDeniedHttpException('Export denied'); + } + + return new ExportDataObjectResult($this->dataObjects->doExportData($object), $object->getId()); + } +} diff --git a/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php b/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php new file mode 100644 index 00000000..d5ac5d2a --- /dev/null +++ b/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php @@ -0,0 +1,26 @@ +dataObjects->searchData( + $payload->id, + $payload->firstname, + $payload->lastname, + $payload->email, + $payload->start, + $payload->limit, + $payload->sort, + ); + + return new SearchDataObjectsResult($result); + } +} diff --git a/src/Handler/GDPR/DataObject/SearchDataObjects/SearchDataObjectsResult.php b/src/Handler/GDPR/DataObject/SearchDataObjects/SearchDataObjectsResult.php new file mode 100644 index 00000000..e4fa424a --- /dev/null +++ b/src/Handler/GDPR/DataObject/SearchDataObjects/SearchDataObjectsResult.php @@ -0,0 +1,25 @@ +manager->getServices() as $service) { + $providers[] = [ + 'name' => $service->getName(), + 'jsClass' => $service->getJsClassName(), + ]; + } + + return new GetDataProvidersResult($providers); + } +} diff --git a/src/Handler/GDPR/GetDataProviders/GetDataProvidersResult.php b/src/Handler/GDPR/GetDataProviders/GetDataProvidersResult.php new file mode 100644 index 00000000..6dc89bae --- /dev/null +++ b/src/Handler/GDPR/GetDataProviders/GetDataProvidersResult.php @@ -0,0 +1,25 @@ +openDxpUsers->getExportData($payload->id)); + } +} diff --git a/src/Handler/GDPR/OpenDxpUsers/ExportUserData/ExportUserDataResult.php b/src/Handler/GDPR/OpenDxpUsers/ExportUserData/ExportUserDataResult.php new file mode 100644 index 00000000..f711b567 --- /dev/null +++ b/src/Handler/GDPR/OpenDxpUsers/ExportUserData/ExportUserDataResult.php @@ -0,0 +1,25 @@ +openDxpUsers->searchData( + $payload->id, + $payload->firstname, + $payload->lastname, + $payload->email, + $payload->start, + $payload->limit, + $payload->sort, + ); + + return new SearchUsersResult($result); + } +} diff --git a/src/Handler/GDPR/OpenDxpUsers/SearchUsers/SearchUsersResult.php b/src/Handler/GDPR/OpenDxpUsers/SearchUsers/SearchUsersResult.php new file mode 100644 index 00000000..a523e011 --- /dev/null +++ b/src/Handler/GDPR/OpenDxpUsers/SearchUsers/SearchUsersResult.php @@ -0,0 +1,25 @@ +query->all(); + + return new static( + id: (int)$allParams['id'], + firstname: strip_tags($allParams['firstname']), + lastname: strip_tags($allParams['lastname']), + email: strip_tags($allParams['email']), + start: (int)$allParams['start'], + limit: (int)$allParams['limit'], + sort: $allParams['sort'] ?? null, + ); + } +} diff --git a/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailHandler.php b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailHandler.php new file mode 100644 index 00000000..a89185e8 --- /dev/null +++ b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailHandler.php @@ -0,0 +1,39 @@ +id); + if (!$sentMail) { + throw new NotFoundHttpException(); + } + + $sentMailArray = (array)$sentMail; + $sentMailArray['htmlBody'] = $sentMail->getHtmlLog(); + $sentMailArray['textBody'] = $sentMail->getTextLog(); + + return new ExportSentMailResult($sentMailArray, $sentMail->getId()); + } +} diff --git a/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php new file mode 100644 index 00000000..41e720e8 --- /dev/null +++ b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php @@ -0,0 +1,26 @@ +db); + $viewParams['headless'] = $payload->headless; + + return new CheckSystemResult(viewParams: $viewParams); + } +} diff --git a/src/Handler/Install/CheckSystem/CheckSystemPayload.php b/src/Handler/Install/CheckSystem/CheckSystemPayload.php new file mode 100644 index 00000000..0474af0b --- /dev/null +++ b/src/Handler/Install/CheckSystem/CheckSystemPayload.php @@ -0,0 +1,22 @@ +query->getBoolean('headless') || $request->request->getBoolean('headless'), + ); + } +} diff --git a/src/Handler/Install/CheckSystem/CheckSystemResult.php b/src/Handler/Install/CheckSystem/CheckSystemResult.php new file mode 100644 index 00000000..a767e96d --- /dev/null +++ b/src/Handler/Install/CheckSystem/CheckSystemResult.php @@ -0,0 +1,12 @@ +deeplink) { + throw new NotFoundHttpException(); + } + + // Has token → redirect with deeplink + if (str_contains($payload->queryString, 'token')) { + $event = new LoginRedirectEvent('opendxp_admin_login', [ + 'deeplink' => $payload->deeplink, + 'perspective' => $payload->perspective, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_REDIRECT); + + $url = $this->urlGenerator->generate($event->getRouteName(), $event->getRouteParams()); + + return new DeeplinkResult(redirectUrl: $url . '&' . $payload->queryString); + } + + // Has query string → render deeplink page + if ($payload->queryString) { + $event = new LoginRedirectEvent('opendxp_admin_login', [ + 'deeplink' => 'true', + 'perspective' => $payload->perspective, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_REDIRECT); + + $redirect = $this->urlGenerator->generate($event->getRouteName(), $event->getRouteParams()); + + return new DeeplinkResult( + template: '@OpenDxpAdmin/admin/login/deeplink.html.twig', + params: ['tab' => $payload->deeplink, 'redirect' => $redirect], + ); + } + + // No query string → not found + throw new NotFoundHttpException(); + } +} diff --git a/src/Handler/Login/Deeplink/DeeplinkPayload.php b/src/Handler/Login/Deeplink/DeeplinkPayload.php new file mode 100644 index 00000000..0b0acc2c --- /dev/null +++ b/src/Handler/Login/Deeplink/DeeplinkPayload.php @@ -0,0 +1,35 @@ +server->get('QUERY_STRING') ?? ''; + $perspective = (string) $request->query->get('perspective', ''); + $perspective = strip_tags($perspective); + + $deeplink = null; + if (preg_match('/(document|asset|object)_(\d+)_([a-z]+)/', $queryString, $matches)) { + $deeplink = $matches[0]; + } + + return new static( + queryString: $queryString, + perspective: $perspective, + deeplink: $deeplink, + ); + } +} diff --git a/src/Handler/Login/Deeplink/DeeplinkResult.php b/src/Handler/Login/Deeplink/DeeplinkResult.php new file mode 100644 index 00000000..cda14174 --- /dev/null +++ b/src/Handler/Login/Deeplink/DeeplinkResult.php @@ -0,0 +1,14 @@ +userContext->getAdminUser(); + $proxyUser = $this->userContext->getAdminUserProxy(); + + $secret = $this->twoFactor->generateSecret(); + + $user->setTwoFactorAuthentication('enabled', true); + $user->setTwoFactorAuthentication('type', 'google'); + $user->setTwoFactorAuthentication('secret', $secret); + + $url = $this->twoFactor->getQRContent($proxyUser); + + $qrResult = Builder::create() + ->writer(new PngWriter()) + ->data($url) + ->size(200) + ->build(); + + return new GenerateTwoFactorSetupResult( + secret: $secret, + qrDataUri: $qrResult->getDataUri(), + ); + } +} diff --git a/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupPayload.php b/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupPayload.php new file mode 100644 index 00000000..fbfac6d1 --- /dev/null +++ b/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupPayload.php @@ -0,0 +1,22 @@ +query->getString('error') ?: null, + ); + } +} diff --git a/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupResult.php b/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupResult.php new file mode 100644 index 00000000..4fe12421 --- /dev/null +++ b/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupResult.php @@ -0,0 +1,13 @@ +query->getString('perspective') ?: null; + + return new static( + perspective: $perspective !== null ? strip_tags($perspective) : null, + ); + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordHandler.php b/src/Handler/Login/LostPassword/LostPasswordHandler.php new file mode 100644 index 00000000..770e9892 --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordHandler.php @@ -0,0 +1,96 @@ +isPost) { + return new LostPasswordResult(error: null); + } + + if (!$payload->username) { + return new LostPasswordResult(error: 'user_unknown'); + } + + $user = User::getByName($payload->username); + if (!$user instanceof User) { + return new LostPasswordResult(error: 'user_unknown'); + } + + $limiter = $this->resetPasswordLimiter->create($payload->clientIp); + if (false === $limiter->consume(1)->isAccepted()) { + return new LostPasswordResult(error: 'user_reset_password_too_many_attempts'); + } + + if (!$user->isActive()) { + return new LostPasswordResult(error: 'user_inactive'); + } + + if (!$user->getEmail()) { + return new LostPasswordResult(error: 'user_no_email_address'); + } + + if (!$user->getPassword()) { + return new LostPasswordResult(error: 'user_no_password'); + } + + $token = Authentication::generateTokenByUser($user); + + try { + $domain = $this->hostResolver->resolve($payload->resolverContext ?? []) ?? ''; + if (!$domain) { + throw new \Exception('No main domain set in system settings, unable to generate reset password link'); + } + + $context = $this->router->getContext(); + $context->setHost($domain); + + $loginUrl = $this->router->generate('opendxp_admin_login_check', [ + 'token' => $token, + 'reset' => 'true', + ], UrlGeneratorInterface::ABSOLUTE_URL); + + $event = new LostPasswordEvent($user, $loginUrl); + $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_LOSTPASSWORD); + + if ($event->getSendMail()) { + $mail = Tool::getMail([$user->getEmail()], 'OpenDXP lost password service'); + $mail->setIgnoreDebugMode(true); + $mail->text("Login to OpenDXP and change your password using the following link. This temporary login link will expire in 24 hours: \r\n\r\n" . $loginUrl); + $mail->send(); + } + + return new LostPasswordResult( + error: null, + eventResponse: $event->hasResponse() ? $event->getResponse() : null, + ); + } catch (\Exception $e) { + Logger::error('Error sending password recovery email: ' . $e->getMessage()); + + return new LostPasswordResult(error: 'lost_password_email_error'); + } + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordPayload.php b/src/Handler/Login/LostPassword/LostPasswordPayload.php new file mode 100644 index 00000000..453f13e7 --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordPayload.php @@ -0,0 +1,28 @@ +request->get('username'), + clientIp: (string) $request->getClientIp(), + isPost: $request->isMethod('POST') && $request->request->has('username'), + resolverContext: ['source' => $request], + ); + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordResult.php b/src/Handler/Login/LostPassword/LostPasswordResult.php new file mode 100644 index 00000000..8845e6d5 --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordResult.php @@ -0,0 +1,15 @@ +secret) { + throw new Exception('2fa secret not found'); + } + + $user = $this->userContext->getAdminUser(); + $proxyUser = $this->userContext->getAdminUserProxy(); + + $user->setTwoFactorAuthentication('enabled', true); + $user->setTwoFactorAuthentication('type', 'google'); + $user->setTwoFactorAuthentication('secret', $payload->secret); + + if (!$this->twoFactor->checkCode($proxyUser, $payload->authCode)) { + throw new Exception('2fa_wrong'); + } + + $user->save(); + } +} diff --git a/src/Handler/Login/SaveTwoFactorSetup/SaveTwoFactorSetupPayload.php b/src/Handler/Login/SaveTwoFactorSetup/SaveTwoFactorSetupPayload.php new file mode 100644 index 00000000..e5fa82cc --- /dev/null +++ b/src/Handler/Login/SaveTwoFactorSetup/SaveTwoFactorSetupPayload.php @@ -0,0 +1,24 @@ +getSession()->get('2fa_secret'), + authCode: (string) $request->request->get('_auth_code'), + ); + } +} diff --git a/src/Handler/Misc/AdminCss/AdminCssHandler.php b/src/Handler/Misc/AdminCss/AdminCssHandler.php new file mode 100644 index 00000000..04ae08cf --- /dev/null +++ b/src/Handler/Misc/AdminCss/AdminCssHandler.php @@ -0,0 +1,42 @@ +provider->getControllerReferences(); + + $data = array_map(static fn($controller) => [ + 'name' => $controller, + ], $controllerReferences); + + return new GetAvailableControllerReferencesResult( + data: $data, + total: count($data), + ); + } +} diff --git a/src/Handler/Misc/GetAvailableControllerReferences/GetAvailableControllerReferencesResult.php b/src/Handler/Misc/GetAvailableControllerReferences/GetAvailableControllerReferencesResult.php new file mode 100644 index 00000000..7d1c536c --- /dev/null +++ b/src/Handler/Misc/GetAvailableControllerReferences/GetAvailableControllerReferencesResult.php @@ -0,0 +1,26 @@ +provider->getTemplates(); + + sort($templates, SORT_NATURAL | SORT_FLAG_CASE); + + $data = array_map(static fn ($template) => [ + 'path' => $template, + ], $templates); + + return new GetAvailableTemplatesResult(data: $data); + } +} diff --git a/src/Handler/Misc/GetAvailableTemplates/GetAvailableTemplatesResult.php b/src/Handler/Misc/GetAvailableTemplates/GetAvailableTemplatesResult.php new file mode 100644 index 00000000..848861f9 --- /dev/null +++ b/src/Handler/Misc/GetAvailableTemplates/GetAvailableTemplatesResult.php @@ -0,0 +1,25 @@ +localeService->getDisplayRegions(); + asort($countries); + + $data = []; + foreach ($countries as $short => $translation) { + if (strlen($short) === 2) { + $data[] = [ + 'name' => $translation, + 'code' => $short, + ]; + } + } + + return new GetCountryListResult(data: $data); + } +} diff --git a/src/Handler/Misc/GetCountryList/GetCountryListResult.php b/src/Handler/Misc/GetCountryList/GetCountryListResult.php new file mode 100644 index 00000000..04f659a8 --- /dev/null +++ b/src/Handler/Misc/GetCountryList/GetCountryListResult.php @@ -0,0 +1,25 @@ +type) { + 'color' => FileSystemHelper::scanDirectory($iconDir . '/flat-color-icons/'), + 'white' => FileSystemHelper::scanDirectory($iconDir . '/flat-white-icons/'), + 'twemoji' => FileSystemHelper::scanDirectory($iconDir . '/twemoji/'), + 'flags' => $this->getFlags(), + default => [] + }; + + $source = match ($payload->type) { + 'color', 'white' => + 'based on the ' . + 'Material Design Icons', + 'twemoji' => + 'based on the ' . + 'Twemoji icons', + default => '' + }; + + $extraInfo = null; + if ($payload->type === 'twemoji') { + $extraInfo = 'ℹ Click on icon with green border to display all its related variants. Click on the letter to display flags with the clicked initial'; + } + + $iconsCss = file_get_contents($publicDir . '/css/icons.css'); + + return new GetIconListResult( + icons: $icons, + iconsCss: $iconsCss !== false ? $iconsCss : '', + type: $payload->type, + extraInfo: $extraInfo, + source: $source, + ); + } + + private function getFlags(): array + { + $locales = Tool::getSupportedLocales(); + $languageOptions = []; + foreach (array_keys($locales) as $short) { + if (!empty($short)) { + $flag = AdminTool::getLanguageFlagFile($short, true, false); + if ($flag) { + $languageOptions[] = $flag; + } + } + } + + $languageOptions = array_unique($languageOptions); + sort($languageOptions); + + return $languageOptions; + } +} diff --git a/src/Handler/Misc/GetIconList/GetIconListPayload.php b/src/Handler/Misc/GetIconList/GetIconListPayload.php new file mode 100644 index 00000000..faf916dc --- /dev/null +++ b/src/Handler/Misc/GetIconList/GetIconListPayload.php @@ -0,0 +1,35 @@ +query->has('type') ? $request->query->getString('type') : null, + ); + } +} diff --git a/src/Handler/Misc/GetIconList/GetIconListResult.php b/src/Handler/Misc/GetIconList/GetIconListResult.php new file mode 100644 index 00000000..3c6ffc96 --- /dev/null +++ b/src/Handler/Misc/GetIconList/GetIconListResult.php @@ -0,0 +1,29 @@ +translator->lazyInitialize('admin', $payload->language); + + $translations = []; + + $fallbackLanguages = []; + if (null !== Locale::getRegion($payload->language)) { + $fallbackLanguages[] = Locale::getPrimaryLanguage($payload->language); + } + if ($payload->language !== 'en') { + $fallbackLanguages[] = 'en'; + } + + foreach (['admin', 'admin_ext'] as $domain) { + $translations = array_replace($translations, $this->translator->getCatalogue($payload->language)->all($domain)); + + foreach ($fallbackLanguages as $fallbackLanguage) { + $this->translator->lazyInitialize($domain, $fallbackLanguage); + foreach ($this->translator->getCatalogue($fallbackLanguage)->all($domain) as $key => $value) { + if (empty($translations[$key])) { + $translations[$key] = $value; + } + } + } + } + + return new GetJsonTranslationsResult(translations: $translations); + } +} diff --git a/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsPayload.php b/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsPayload.php new file mode 100644 index 00000000..e68b36ed --- /dev/null +++ b/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsPayload.php @@ -0,0 +1,35 @@ +query->has('language') ? $request->query->getString('language') : null, + ); + } +} diff --git a/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsResult.php b/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsResult.php new file mode 100644 index 00000000..749dc2e7 --- /dev/null +++ b/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsResult.php @@ -0,0 +1,25 @@ +language), + ); + } +} diff --git a/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagPayload.php b/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagPayload.php new file mode 100644 index 00000000..50d24d14 --- /dev/null +++ b/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagPayload.php @@ -0,0 +1,35 @@ +query->has('language') ? $request->query->getString('language') : null, + ); + } +} diff --git a/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagResult.php b/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagResult.php new file mode 100644 index 00000000..7d53a343 --- /dev/null +++ b/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagResult.php @@ -0,0 +1,25 @@ + $translation) { + $data[] = [ + 'name' => $translation, + 'code' => $short, + ]; + } + + return new GetLanguageListResult(data: $data); + } +} diff --git a/src/Handler/Misc/GetLanguageList/GetLanguageListResult.php b/src/Handler/Misc/GetLanguageList/GetLanguageListResult.php new file mode 100644 index 00000000..46c5b065 --- /dev/null +++ b/src/Handler/Misc/GetLanguageList/GetLanguageListResult.php @@ -0,0 +1,25 @@ +value, $payload->type), + ); + } +} diff --git a/src/Handler/Misc/GetValidFilename/GetValidFilenamePayload.php b/src/Handler/Misc/GetValidFilename/GetValidFilenamePayload.php new file mode 100644 index 00000000..54a634b2 --- /dev/null +++ b/src/Handler/Misc/GetValidFilename/GetValidFilenamePayload.php @@ -0,0 +1,37 @@ +query->has('value') ? $request->query->getString('value') : null, + type: $request->query->has('type') ? $request->query->getString('type') : null, + ); + } +} diff --git a/src/Handler/Misc/GetValidFilename/GetValidFilenameResult.php b/src/Handler/Misc/GetValidFilename/GetValidFilenameResult.php new file mode 100644 index 00000000..7e5a28bc --- /dev/null +++ b/src/Handler/Misc/GetValidFilename/GetValidFilenameResult.php @@ -0,0 +1,25 @@ +activate) { + $this->maintenanceModeHelper->activate($payload->sessionId); + } + + if ($payload->deactivate) { + $this->maintenanceModeHelper->deactivate(); + } + } +} diff --git a/src/Handler/Misc/Maintenance/MaintenancePayload.php b/src/Handler/Misc/Maintenance/MaintenancePayload.php new file mode 100644 index 00000000..09107bd3 --- /dev/null +++ b/src/Handler/Misc/Maintenance/MaintenancePayload.php @@ -0,0 +1,39 @@ +query->has('activate') ? $request->query->getString('activate') : null, + deactivate: $request->query->has('deactivate') ? $request->query->getString('deactivate') : null, + sessionId: $request->getSession()->getId(), + ); + } +} diff --git a/src/Handler/Misc/ScriptProxy/ScriptProxyHandler.php b/src/Handler/Misc/ScriptProxy/ScriptProxyHandler.php new file mode 100644 index 00000000..d2c89e42 --- /dev/null +++ b/src/Handler/Misc/ScriptProxy/ScriptProxyHandler.php @@ -0,0 +1,47 @@ +storageFile) { + throw new InvalidArgumentException('The parameter storageFile is required'); + } + + $fileExtension = pathinfo($payload->storageFile, PATHINFO_EXTENSION); + $storage = Storage::get('admin'); + $scriptsContent = $storage->read($payload->storageFile); + + if (empty($scriptsContent)) { + throw new NotFoundHttpException('Scripts not found'); + } + + $contentType = $fileExtension === 'css' ? 'text/css' : 'text/javascript'; + + return new ScriptProxyResult( + content: $scriptsContent, + contentType: $contentType, + ); + } +} diff --git a/src/Handler/Misc/ScriptProxy/ScriptProxyPayload.php b/src/Handler/Misc/ScriptProxy/ScriptProxyPayload.php new file mode 100644 index 00000000..ea8ce901 --- /dev/null +++ b/src/Handler/Misc/ScriptProxy/ScriptProxyPayload.php @@ -0,0 +1,35 @@ +query->has('storageFile') ? $request->query->getString('storageFile') : null, + ); + } +} diff --git a/src/Handler/Misc/ScriptProxy/ScriptProxyResult.php b/src/Handler/Misc/ScriptProxy/ScriptProxyResult.php new file mode 100644 index 00000000..4d92c72e --- /dev/null +++ b/src/Handler/Misc/ScriptProxy/ScriptProxyResult.php @@ -0,0 +1,26 @@ +notificationService->deleteAll((int) $this->userContext->getAdminUser()?->getId()); + } +} diff --git a/src/Handler/Notification/DeleteNotification/DeleteNotificationHandler.php b/src/Handler/Notification/DeleteNotification/DeleteNotificationHandler.php new file mode 100644 index 00000000..d29a6df0 --- /dev/null +++ b/src/Handler/Notification/DeleteNotification/DeleteNotificationHandler.php @@ -0,0 +1,35 @@ +notificationService->delete($payload->id, (int) $this->userContext->getAdminUser()?->getId()); + } +} diff --git a/src/Handler/Notification/FindAllNotifications/FindAllNotificationsHandler.php b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsHandler.php new file mode 100644 index 00000000..56fa56c6 --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsHandler.php @@ -0,0 +1,54 @@ + (int) $this->userContext->getAdminUser()?->getId()]; + + $syntheticRequest = new Request(request: [NotificationServiceFilterParser::KEY_FILTER => $payload->filter]); + $parser = new NotificationServiceFilterParser($syntheticRequest); + foreach ($parser->parse() as $key => $val) { + $filter[$key] = $val; + } + + $result = $this->notificationService->findAll($filter, [ + 'offset' => $payload->offset, + 'limit' => $payload->limit, + ]); + + $data = []; + foreach ($result['data'] as $notification) { + $data[] = $this->notificationService->format($notification); + } + + return new FindAllNotificationsResult(data: $data, total: (int) $result['total']); + } +} diff --git a/src/Handler/Notification/FindAllNotifications/FindAllNotificationsPayload.php b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsPayload.php new file mode 100644 index 00000000..c0e72c4d --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsPayload.php @@ -0,0 +1,38 @@ +request->getInt('start'), + limit: $request->request->getInt('limit', 40), + filter: $request->request->getString('filter', '[]'), + ); + } +} diff --git a/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php new file mode 100644 index 00000000..9bddc82f --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $result = $this->notificationService->findLastUnread($userId, $payload->lastUpdate ?? time()); + $unread = $this->notificationService->countAllUnread($userId); + + $data = []; + foreach ($result['data'] as $notification) { + $data[] = $this->notificationService->format($notification); + } + + return new FindLastUnreadNotificationsResult( + data: $data, + total: (int) $result['total'], + unread: $unread, + ); + } +} diff --git a/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsPayload.php b/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsPayload.php new file mode 100644 index 00000000..2ec4cb3d --- /dev/null +++ b/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsPayload.php @@ -0,0 +1,36 @@ +query->get('lastUpdate'); + + return new static( + lastUpdate: $raw !== null ? (int) $raw : null, + ); + } +} diff --git a/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsResult.php b/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsResult.php new file mode 100644 index 00000000..1929b906 --- /dev/null +++ b/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsResult.php @@ -0,0 +1,27 @@ +userContext->getAdminUser()?->getId() ?? 0; + try { + $notification = $this->notificationService->findAndMarkAsRead($payload->id, $userId); + } catch (UnexpectedValueException $e) { + throw new NotFoundHttpException(sprintf('Notification with id %d not found', $payload->id), $e); + } + + $data = $this->notificationService->format($notification); + + return new FindNotificationResult(data: $data); + } +} diff --git a/src/Handler/Notification/FindNotification/FindNotificationResult.php b/src/Handler/Notification/FindNotification/FindNotificationResult.php new file mode 100644 index 00000000..237578c1 --- /dev/null +++ b/src/Handler/Notification/FindNotification/FindNotificationResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $data = []; + $group = $this->translator->trans('group', [], 'admin'); + + foreach ($this->userService->findAll($adminUser) as $recipient) { + $prefix = $recipient->getType() === 'role' ? $group . ' - ' : ''; + + $data[] = [ + 'id' => $recipient->getId(), + 'text' => $prefix . $recipient->getName(), + ]; + } + + return new GetRecipientsResult(data: $data); + } +} diff --git a/src/Handler/Notification/GetRecipients/GetRecipientsResult.php b/src/Handler/Notification/GetRecipients/GetRecipientsResult.php new file mode 100644 index 00000000..f50a1519 --- /dev/null +++ b/src/Handler/Notification/GetRecipients/GetRecipientsResult.php @@ -0,0 +1,25 @@ +notificationService->findAndMarkAsRead($payload->id, (int) $this->userContext->getAdminUser()?->getId()); + } +} diff --git a/src/Handler/Notification/SendNotification/SendNotificationHandler.php b/src/Handler/Notification/SendNotification/SendNotificationHandler.php new file mode 100644 index 00000000..65e88a6f --- /dev/null +++ b/src/Handler/Notification/SendNotification/SendNotificationHandler.php @@ -0,0 +1,47 @@ +userContext->getAdminUser()?->getId(); + + $element = null; + if ($payload->elementId && $payload->elementType) { + $element = Service::getElementById($payload->elementType, $payload->elementId); + } + + if (User::getById($payload->recipientId) instanceof User) { + $this->notificationService->sendToUser($payload->recipientId, $fromUserId, $payload->title, $payload->message, $element); + } else { + $this->notificationService->sendToGroup($payload->recipientId, $fromUserId, $payload->title, $payload->message, $element); + } + } +} diff --git a/src/Handler/Notification/SendNotification/SendNotificationPayload.php b/src/Handler/Notification/SendNotification/SendNotificationPayload.php new file mode 100644 index 00000000..c2f75d6a --- /dev/null +++ b/src/Handler/Notification/SendNotification/SendNotificationPayload.php @@ -0,0 +1,44 @@ +request->getString('recipientId'), + title: $request->request->getString('title'), + message: $request->request->getString('message'), + elementId: (int) $request->request->getString('elementId'), + elementType: $request->request->has('elementType') + ? $request->request->getString('elementType') + : null, + ); + } +} diff --git a/src/Handler/Portal/AddWidget/AddWidgetHandler.php b/src/Handler/Portal/AddWidget/AddWidgetHandler.php new file mode 100644 index 00000000..df829f78 --- /dev/null +++ b/src/Handler/Portal/AddWidget/AddWidgetHandler.php @@ -0,0 +1,51 @@ +dashboardFactory->create(); + + $config = $dashboard->getDashboard($payload->dashboardId); + + $nextId = 0; + foreach ($config['positions'] as $col) { + foreach ($col as $row) { + $nextId = ($row['id'] > $nextId ? $row['id'] : $nextId); + } + } + + $nextId += 1; + $config['positions'][0][] = [ + 'id' => $nextId, + 'type' => $payload->type, + 'config' => null, + ]; + + $dashboard->saveDashboard($payload->dashboardId, $config); + + return new AddWidgetResult(id: $nextId); + } +} diff --git a/src/Handler/Portal/AddWidget/AddWidgetPayload.php b/src/Handler/Portal/AddWidget/AddWidgetPayload.php new file mode 100644 index 00000000..c9947ff7 --- /dev/null +++ b/src/Handler/Portal/AddWidget/AddWidgetPayload.php @@ -0,0 +1,37 @@ +request->get('key'), + type: (string) $request->request->get('type'), + ); + } +} diff --git a/src/Handler/Portal/AddWidget/AddWidgetResult.php b/src/Handler/Portal/AddWidget/AddWidgetResult.php new file mode 100644 index 00000000..6361bff7 --- /dev/null +++ b/src/Handler/Portal/AddWidget/AddWidgetResult.php @@ -0,0 +1,24 @@ +key)) { + throw new \InvalidArgumentException('empty'); + } + + $dashboard = $this->dashboardFactory->create(); + + $dashboards = $dashboard->getAllDashboards(); + if (isset($dashboards[$payload->key])) { + throw new \InvalidArgumentException('name_already_in_use'); + } + + $dashboard->saveDashboard($payload->key); + } +} diff --git a/src/Handler/Portal/CreateDashboard/CreateDashboardPayload.php b/src/Handler/Portal/CreateDashboard/CreateDashboardPayload.php new file mode 100644 index 00000000..054b0ab1 --- /dev/null +++ b/src/Handler/Portal/CreateDashboard/CreateDashboardPayload.php @@ -0,0 +1,32 @@ +request->get('key', ''))); + } +} diff --git a/src/Handler/Portal/DeleteDashboard/DeleteDashboardHandler.php b/src/Handler/Portal/DeleteDashboard/DeleteDashboardHandler.php new file mode 100644 index 00000000..ae354be3 --- /dev/null +++ b/src/Handler/Portal/DeleteDashboard/DeleteDashboardHandler.php @@ -0,0 +1,32 @@ +dashboardFactory->create(); + $dashboard->deleteDashboard($payload->key); + } +} diff --git a/src/Handler/Portal/DeleteDashboard/DeleteDashboardPayload.php b/src/Handler/Portal/DeleteDashboard/DeleteDashboardPayload.php new file mode 100644 index 00000000..3a71d456 --- /dev/null +++ b/src/Handler/Portal/DeleteDashboard/DeleteDashboardPayload.php @@ -0,0 +1,32 @@ +request->get('key')); + } +} diff --git a/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationHandler.php b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationHandler.php new file mode 100644 index 00000000..a9b88109 --- /dev/null +++ b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationHandler.php @@ -0,0 +1,33 @@ +dashboardFactory->create(); + + return new GetDashboardConfigurationResult(config: $dashboard->getDashboard($payload->key ?? 'welcome')); + } +} diff --git a/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationPayload.php b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationPayload.php new file mode 100644 index 00000000..6bf7951f --- /dev/null +++ b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationPayload.php @@ -0,0 +1,34 @@ +query->has('key') ? (string) $request->query->get('key') : null, + ); + } +} diff --git a/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationResult.php b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationResult.php new file mode 100644 index 00000000..24a6ea6d --- /dev/null +++ b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationResult.php @@ -0,0 +1,24 @@ +dashboardFactory->create(); + + $dashboards = []; + foreach (array_keys($dashboard->getAllDashboards()) as $key) { + if ($key !== 'welcome') { + $dashboards[] = $key; + } + } + + return new GetDashboardListResult(dashboards: $dashboards); + } +} diff --git a/src/Handler/Portal/GetDashboardList/GetDashboardListResult.php b/src/Handler/Portal/GetDashboardList/GetDashboardListResult.php new file mode 100644 index 00000000..dfbb5740 --- /dev/null +++ b/src/Handler/Portal/GetDashboardList/GetDashboardListResult.php @@ -0,0 +1,24 @@ +fetchOne( + 'SELECT COUNT(*) AS count FROM objects WHERE modificationDate > ? AND modificationDate < ?', + [$start, $end] + ); + $a = $db->fetchOne( + 'SELECT COUNT(*) AS count FROM assets WHERE modificationDate > ? AND modificationDate < ?', + [$start, $end] + ); + $d = $db->fetchOne( + 'SELECT COUNT(*) AS count FROM documents WHERE modificationDate > ? AND modificationDate < ?', + [$start, $end] + ); + + $date = new DateTime(); + $date->setTimestamp($start); + + $data[] = [ + 'timestamp' => $start, + 'datetext' => $date->format('Y-m-d'), + 'objects' => (int) $o, + 'documents' => (int) $d, + 'assets' => (int) $a, + ]; + } + + return new GetModificationStatisticsResult(data: array_reverse($data)); + } +} diff --git a/src/Handler/Portal/GetModificationStatistics/GetModificationStatisticsResult.php b/src/Handler/Portal/GetModificationStatistics/GetModificationStatisticsResult.php new file mode 100644 index 00000000..ce97ac70 --- /dev/null +++ b/src/Handler/Portal/GetModificationStatistics/GetModificationStatisticsResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $list = Asset::getList([ + 'limit' => 10, + 'order' => 'DESC', + 'orderKey' => 'modificationDate', + 'condition' => 'userModification = ' . $userId, + ]); + + $assets = []; + foreach ($list as $doc) { + /** @var Asset $doc */ + if ($doc->isAllowed('view')) { + $assets[] = [ + 'id' => $doc->getId(), + 'type' => $doc->getType(), + 'path' => $doc->getRealFullPath(), + 'date' => $doc->getModificationDate(), + ]; + } + } + + return new GetModifiedAssetsResult(assets: $assets); + } +} diff --git a/src/Handler/Portal/GetModifiedAssets/GetModifiedAssetsResult.php b/src/Handler/Portal/GetModifiedAssets/GetModifiedAssetsResult.php new file mode 100644 index 00000000..d99a45a0 --- /dev/null +++ b/src/Handler/Portal/GetModifiedAssets/GetModifiedAssetsResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $list = Document::getList([ + 'limit' => 10, + 'order' => 'DESC', + 'orderKey' => 'modificationDate', + 'condition' => 'userModification = ' . $userId, + ]); + + $documents = []; + foreach ($list as $doc) { + if ($doc->isAllowed('view')) { + $documents[] = [ + 'id' => $doc->getId(), + 'type' => $doc->getType(), + 'path' => $doc->getRealFullPath(), + 'date' => $doc->getModificationDate(), + ]; + } + } + + return new GetModifiedDocumentsResult(documents: $documents); + } +} diff --git a/src/Handler/Portal/GetModifiedDocuments/GetModifiedDocumentsResult.php b/src/Handler/Portal/GetModifiedDocuments/GetModifiedDocumentsResult.php new file mode 100644 index 00000000..32ca1725 --- /dev/null +++ b/src/Handler/Portal/GetModifiedDocuments/GetModifiedDocumentsResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->getId() ?? 0; + $list = DataObject::getList([ + 'limit' => 10, + 'order' => 'DESC', + 'orderKey' => 'modificationDate', + 'condition' => 'userModification = ' . $userId, + ]); + + $objects = []; + foreach ($list as $object) { + if ($object->isAllowed('view')) { + $objects[] = [ + 'id' => $object->getId(), + 'type' => $object->getType(), + 'path' => $object->getRealFullPath(), + 'date' => $object->getModificationDate(), + ]; + } + } + + return new GetModifiedObjectsResult(objects: $objects); + } +} diff --git a/src/Handler/Portal/GetModifiedObjects/GetModifiedObjectsResult.php b/src/Handler/Portal/GetModifiedObjects/GetModifiedObjectsResult.php new file mode 100644 index 00000000..4c9c9516 --- /dev/null +++ b/src/Handler/Portal/GetModifiedObjects/GetModifiedObjectsResult.php @@ -0,0 +1,26 @@ +dashboardFactory->create(); + + $config = $dashboard->getDashboard($payload->dashboardId); + $newConfig = [[], []]; + $colCount = 0; + + foreach ($config['positions'] as $col) { + foreach ($col as $row) { + if ($row['id'] !== $payload->widgetId) { + $newConfig[$colCount][] = $row; + } + } + $colCount++; + } + + $config['positions'] = $newConfig; + $dashboard->saveDashboard($payload->dashboardId, $config); + } +} diff --git a/src/Handler/Portal/RemoveWidget/RemoveWidgetPayload.php b/src/Handler/Portal/RemoveWidget/RemoveWidgetPayload.php new file mode 100644 index 00000000..dac73169 --- /dev/null +++ b/src/Handler/Portal/RemoveWidget/RemoveWidgetPayload.php @@ -0,0 +1,37 @@ +request->get('key'), + widgetId: $request->request->has('id') ? (int) $request->request->get('id') : null, + ); + } +} diff --git a/src/Handler/Portal/ReorderWidget/ReorderWidgetHandler.php b/src/Handler/Portal/ReorderWidget/ReorderWidgetHandler.php new file mode 100644 index 00000000..f3059316 --- /dev/null +++ b/src/Handler/Portal/ReorderWidget/ReorderWidgetHandler.php @@ -0,0 +1,52 @@ +dashboardFactory->create(); + + $config = $dashboard->getDashboard($payload->dashboardId); + $newConfig = [[], []]; + $colCount = 0; + $toMove = null; + + foreach ($config['positions'] as $col) { + foreach ($col as $item) { + if ($item['id'] !== $payload->widgetId) { + $newConfig[$colCount][] = $item; + } else { + $toMove = $item; + } + } + $colCount++; + } + + array_splice($newConfig[$payload->column], $payload->row, 0, [$toMove]); + + $config['positions'] = $newConfig; + $dashboard->saveDashboard($payload->dashboardId, $config); + } +} diff --git a/src/Handler/Portal/ReorderWidget/ReorderWidgetPayload.php b/src/Handler/Portal/ReorderWidget/ReorderWidgetPayload.php new file mode 100644 index 00000000..e30d53e6 --- /dev/null +++ b/src/Handler/Portal/ReorderWidget/ReorderWidgetPayload.php @@ -0,0 +1,41 @@ +request->get('key'), + widgetId: $request->request->has('id') ? (int) $request->request->get('id') : null, + column: $request->request->getInt('column'), + row: $request->request->getInt('row'), + ); + } +} diff --git a/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigHandler.php b/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigHandler.php new file mode 100644 index 00000000..ce8bced0 --- /dev/null +++ b/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigHandler.php @@ -0,0 +1,43 @@ +dashboardFactory->create(); + + $config = $dashboard->getDashboard($payload->dashboardKey); + foreach ($config['positions'] as &$col) { + foreach ($col as &$portlet) { + if ($portlet['id'] === $payload->portletId) { + $portlet['config'] = $payload->configuration; + break; + } + } + } + + $dashboard->saveDashboard($payload->dashboardKey, $config); + } +} diff --git a/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigPayload.php b/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigPayload.php new file mode 100644 index 00000000..11760aca --- /dev/null +++ b/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigPayload.php @@ -0,0 +1,39 @@ +request->get('key'), + portletId: $request->request->has('id') ? (int) $request->request->get('id') : null, + configuration: $request->request->get('config'), + ); + } +} diff --git a/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinHandler.php b/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinHandler.php new file mode 100644 index 00000000..6057916c --- /dev/null +++ b/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinHandler.php @@ -0,0 +1,47 @@ +userContext->getAdminUser(); + $element = Service::getElementById($payload->type, $payload->id); + + if (!$element) { + return; + } + + $list = $element::getList(['unpublished' => true]); + $list->setCondition('`path` LIKE ' . $list->quote($list->escapeLike($element->getRealFullPath()) . '/%')); + $children = $list->getTotalCount(); + + if ($children <= 100) { + Recyclebin\Item::create($element, $adminUser); + } + } +} diff --git a/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinPayload.php b/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinPayload.php new file mode 100644 index 00000000..22fd1c6a --- /dev/null +++ b/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinPayload.php @@ -0,0 +1,36 @@ +request->get('type'), + id: $request->request->getInt('id'), + ); + } +} diff --git a/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemHandler.php b/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemHandler.php new file mode 100644 index 00000000..ffe07c07 --- /dev/null +++ b/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemHandler.php @@ -0,0 +1,32 @@ +id); + if ($item) { + $item->delete(); + } + } +} \ No newline at end of file diff --git a/src/Handler/Recyclebin/FlushRecyclebin/FlushRecyclebinHandler.php b/src/Handler/Recyclebin/FlushRecyclebin/FlushRecyclebinHandler.php new file mode 100644 index 00000000..df6b997e --- /dev/null +++ b/src/Handler/Recyclebin/FlushRecyclebin/FlushRecyclebinHandler.php @@ -0,0 +1,29 @@ +flush(); + } +} diff --git a/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinHandler.php b/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinHandler.php new file mode 100644 index 00000000..19a5c6b0 --- /dev/null +++ b/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinHandler.php @@ -0,0 +1,104 @@ +setLimit($payload->limit); + $list->setOffset($payload->offset); + $list->setOrderKey($payload->orderKey); + $list->setOrder($payload->order); + + $conditionFilters = []; + + if ($payload->filterFullText) { + $conditionFilters[] = '`path` LIKE ' . $list->quote('%' . $list->escapeLike($payload->filterFullText) . '%'); + } + + foreach ($payload->filters as $filter) { + $operator = '='; + + $filterField = $filter['property']; + $filterOperator = $filter['operator']; + + if ($filter['type'] === 'string') { + $operator = 'LIKE'; + } elseif ($filter['type'] === 'numeric') { + if ($filterOperator === 'lt') { + $operator = '<'; + } elseif ($filterOperator === 'gt') { + $operator = '>'; + } elseif ($filterOperator === 'eq') { + $operator = '='; + } + } elseif ($filter['type'] === 'date') { + if ($filterOperator === 'lt') { + $operator = '<'; + } elseif ($filterOperator === 'gt') { + $operator = '>'; + } elseif ($filterOperator === 'eq') { + $operator = '='; + } + $filter['value'] = strtotime($filter['value']); + } elseif ($filter['type'] === 'list') { + $operator = '='; + } elseif ($filter['type'] === 'boolean') { + $operator = '='; + $filter['value'] = (int) $filter['value']; + } + + $value = ($filter['value'] ?? ''); + if ($operator === 'LIKE') { + $value = '%' . $value . '%'; + } + + $field = $db->quoteIdentifier($filterField); + if (($filter['field'] ?? false) === 'fullpath') { + $field = 'CONCAT(`path`,filename)'; + } + + if ($filter['type'] === 'date' && $operator === '=') { + $maxTime = $value + (86400 - 1); + $condition = $field . ' BETWEEN ' . $db->quote($value) . ' AND ' . $db->quote($maxTime); + $conditionFilters[] = $condition; + } else { + $conditionFilters[] = $field . $operator . ' ' . $db->quote($value); + } + } + + if ($conditionFilters !== []) { + $list->setCondition(implode(' AND ', $conditionFilters)); + } + + $items = $list->load(); + $data = []; + foreach ($items as $item) { + $data[] = $item->getObjectVars(); + } + + return new ListRecyclebinResult(data: $data, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinResult.php b/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinResult.php new file mode 100644 index 00000000..f335b425 --- /dev/null +++ b/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinResult.php @@ -0,0 +1,26 @@ +request->has('data')) { + return new static( + hasData: true, + id: QueryParams::getRecordIdForGridRequest($request->request->getString('data')), + ); + } + + $sortingSettings = QueryParams::extractSortingSettings($request->request->all()); + $orderKey = $sortingSettings['orderKey'] ?: 'date'; + $order = $sortingSettings['orderKey'] ? $sortingSettings['order'] : 'DESC'; + + return new static( + hasData: false, + limit: $request->request->getInt('limit', 50), + offset: $request->request->getInt('start', 0), + orderKey: $orderKey, + order: $order, + filterFullText: $request->request->get('filterFullText') ?: null, + filters: json_decode($request->request->getString('filter', '[]'), true) ?? [], + ); + } +} diff --git a/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemHandler.php b/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemHandler.php new file mode 100644 index 00000000..09928779 --- /dev/null +++ b/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemHandler.php @@ -0,0 +1,34 @@ +id); + if (!$item) { + throw new NotFoundHttpException(sprintf('Recyclebin item with id %d not found', $payload->id)); + } + + $item->restore(); + } +} diff --git a/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemPayload.php b/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemPayload.php new file mode 100644 index 00000000..2c1542f5 --- /dev/null +++ b/src/Handler/Recyclebin/RestoreRecyclebinItem/RestoreRecyclebinItemPayload.php @@ -0,0 +1,30 @@ +request->get('id')); + } +} diff --git a/src/Handler/Settings/AddThumbnail/AddThumbnailHandler.php b/src/Handler/Settings/AddThumbnail/AddThumbnailHandler.php new file mode 100644 index 00000000..1b5d7e13 --- /dev/null +++ b/src/Handler/Settings/AddThumbnail/AddThumbnailHandler.php @@ -0,0 +1,47 @@ +name); + + if (!$pipe) { + $pipe = new Asset\Image\Thumbnail\Config(); + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + $pipe->setName($payload->name); + $pipe->save(); + + return new AddThumbnailResult(id: $pipe->getName(), created: true); + } + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + return new AddThumbnailResult(id: $pipe->getName(), created: false); + } +} diff --git a/src/Handler/Settings/AddThumbnail/AddThumbnailPayload.php b/src/Handler/Settings/AddThumbnail/AddThumbnailPayload.php new file mode 100644 index 00000000..47062c07 --- /dev/null +++ b/src/Handler/Settings/AddThumbnail/AddThumbnailPayload.php @@ -0,0 +1,34 @@ +request->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/AddThumbnail/AddThumbnailResult.php b/src/Handler/Settings/AddThumbnail/AddThumbnailResult.php new file mode 100644 index 00000000..f424164e --- /dev/null +++ b/src/Handler/Settings/AddThumbnail/AddThumbnailResult.php @@ -0,0 +1,26 @@ +name); + + if (!$pipe) { + $pipe = new Asset\Video\Thumbnail\Config(); + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + $pipe->setName($payload->name); + $pipe->save(); + + return new AddVideoThumbnailResult(id: $pipe->getName(), created: true); + } + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + return new AddVideoThumbnailResult(id: $pipe->getName(), created: false); + } +} diff --git a/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailPayload.php b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailPayload.php new file mode 100644 index 00000000..4f4a57ec --- /dev/null +++ b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailPayload.php @@ -0,0 +1,34 @@ +request->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailResult.php b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailResult.php new file mode 100644 index 00000000..64213f1c --- /dev/null +++ b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailResult.php @@ -0,0 +1,26 @@ +env ?: $this->kernel->getEnvironment(); + + if (!$payload->onlySymfonyCache) { + $this->openDxpCache->clear(); + } + + if (!$payload->onlyOpendxpCache) { + $this->symfonyCache->clear($env); + } + } +} diff --git a/src/Handler/Settings/ClearCache/ClearCachePayload.php b/src/Handler/Settings/ClearCache/ClearCachePayload.php new file mode 100644 index 00000000..b37c287b --- /dev/null +++ b/src/Handler/Settings/ClearCache/ClearCachePayload.php @@ -0,0 +1,38 @@ +request->getString('only_symfony_cache'), + onlyOpendxpCache: (bool) $request->request->getString('only_opendxp_cache'), + env: $request->request->getString('env'), + ); + } +} diff --git a/src/Handler/Settings/ClearOutputCache/ClearOutputCacheHandler.php b/src/Handler/Settings/ClearOutputCache/ClearOutputCacheHandler.php new file mode 100644 index 00000000..47cf6ac1 --- /dev/null +++ b/src/Handler/Settings/ClearOutputCache/ClearOutputCacheHandler.php @@ -0,0 +1,35 @@ +eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_FULLPAGE_CACHE); + } +} diff --git a/src/Handler/Settings/ClearTemporaryFiles/ClearTemporaryFilesHandler.php b/src/Handler/Settings/ClearTemporaryFiles/ClearTemporaryFilesHandler.php new file mode 100644 index 00000000..59808b23 --- /dev/null +++ b/src/Handler/Settings/ClearTemporaryFiles/ClearTemporaryFilesHandler.php @@ -0,0 +1,44 @@ +deleteDirectory('/'); + Db::get()->executeStatement('TRUNCATE TABLE assets_image_thumbnail_cache'); + + Tool\Storage::get('asset_cache')->deleteDirectory('/'); + + // system files + FileSystemHelper::recursiveDelete(OPENDXP_SYSTEM_TEMP_DIRECTORY, false); + + $this->eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_TEMPORARY_FILES); + } +} diff --git a/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataHandler.php b/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataHandler.php new file mode 100644 index 00000000..2fddb509 --- /dev/null +++ b/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataHandler.php @@ -0,0 +1,56 @@ +data; + unset($data['id']); + + if (!(new Metadata\Predefined())->isWriteable()) { + throw new ConfigWriteException(); + } + + $metadata = Metadata\Predefined::create(); + $metadata->setValues($data); + + $existingItem = Metadata\Predefined\Listing::getByKeyAndLanguage( + $metadata->getName(), + $metadata->getLanguage(), + $metadata->getTargetSubtype() + ); + + if ($existingItem) { + throw new BadRequestHttpException('rule_violation'); + } + + $metadata->save(); + + $responseData = $metadata->getObjectVars(); + $responseData['writeable'] = $metadata->isWriteable(); + + return new CreatePredefinedMetadataResult(data: $responseData); + } +} diff --git a/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataResult.php b/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataResult.php new file mode 100644 index 00000000..90a87383 --- /dev/null +++ b/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataResult.php @@ -0,0 +1,25 @@ +data; + unset($data['id']); + + if (!(new Property\Predefined())->isWriteable()) { + throw new ConfigWriteException(); + } + + $property = Property\Predefined::create(); + $property->setValues($data); + $property->save(); + + $responseData = $property->getObjectVars(); + $responseData['writeable'] = $property->isWriteable(); + + return new CreatePredefinedPropertyResult(data: $responseData); + } +} diff --git a/src/Handler/Settings/CreatePredefinedProperty/CreatePredefinedPropertyResult.php b/src/Handler/Settings/CreatePredefinedProperty/CreatePredefinedPropertyResult.php new file mode 100644 index 00000000..77b03e61 --- /dev/null +++ b/src/Handler/Settings/CreatePredefinedProperty/CreatePredefinedPropertyResult.php @@ -0,0 +1,25 @@ +data; + unset($data['id']); + + $setting = new WebsiteSetting(); + $setting->setValues($data); + $setting->save(); + + return new CreateWebsiteSettingResult(data: $setting->getObjectVars()); + } +} diff --git a/src/Handler/Settings/CreateWebsiteSetting/CreateWebsiteSettingResult.php b/src/Handler/Settings/CreateWebsiteSetting/CreateWebsiteSettingResult.php new file mode 100644 index 00000000..fa63c719 --- /dev/null +++ b/src/Handler/Settings/CreateWebsiteSetting/CreateWebsiteSettingResult.php @@ -0,0 +1,25 @@ +fileExists(self::LOGO_PATH)) { + $storage->delete(self::LOGO_PATH); + } + } +} diff --git a/src/Handler/Settings/DeletePredefinedMetadata/DeletePredefinedMetadataHandler.php b/src/Handler/Settings/DeletePredefinedMetadata/DeletePredefinedMetadataHandler.php new file mode 100644 index 00000000..b23ef57d --- /dev/null +++ b/src/Handler/Settings/DeletePredefinedMetadata/DeletePredefinedMetadataHandler.php @@ -0,0 +1,36 @@ +data['id']); + + if (!$metadata->isWriteable()) { + throw new ConfigWriteException(); + } + + $metadata->delete(); + } +} diff --git a/src/Handler/Settings/DeletePredefinedProperty/DeletePredefinedPropertyHandler.php b/src/Handler/Settings/DeletePredefinedProperty/DeletePredefinedPropertyHandler.php new file mode 100644 index 00000000..67d12acb --- /dev/null +++ b/src/Handler/Settings/DeletePredefinedProperty/DeletePredefinedPropertyHandler.php @@ -0,0 +1,36 @@ +data['id']); + + if (!$property->isWriteable()) { + throw new ConfigWriteException(); + } + + $property->delete(); + } +} diff --git a/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailHandler.php b/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailHandler.php new file mode 100644 index 00000000..6bfe0b54 --- /dev/null +++ b/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailHandler.php @@ -0,0 +1,36 @@ +name); + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + $pipe->delete(); + } +} diff --git a/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailPayload.php b/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailPayload.php new file mode 100644 index 00000000..05d05b4c --- /dev/null +++ b/src/Handler/Settings/DeleteThumbnail/DeleteThumbnailPayload.php @@ -0,0 +1,34 @@ +request->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailHandler.php b/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailHandler.php new file mode 100644 index 00000000..9258f8c9 --- /dev/null +++ b/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailHandler.php @@ -0,0 +1,36 @@ +name); + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + $pipe->delete(); + } +} diff --git a/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailPayload.php b/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailPayload.php new file mode 100644 index 00000000..4801aa47 --- /dev/null +++ b/src/Handler/Settings/DeleteVideoThumbnail/DeleteVideoThumbnailPayload.php @@ -0,0 +1,34 @@ +request->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/DeleteWebsiteSetting/DeleteWebsiteSettingHandler.php b/src/Handler/Settings/DeleteWebsiteSetting/DeleteWebsiteSettingHandler.php new file mode 100644 index 00000000..4e80f046 --- /dev/null +++ b/src/Handler/Settings/DeleteWebsiteSetting/DeleteWebsiteSettingHandler.php @@ -0,0 +1,35 @@ +data['id']); + + if (!$setting instanceof WebsiteSetting) { + return; + } + + $setting->delete(); + } +} diff --git a/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoHandler.php b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoHandler.php new file mode 100644 index 00000000..78dd3c77 --- /dev/null +++ b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoHandler.php @@ -0,0 +1,45 @@ +white ? 'logo-claim-white.svg' : 'logo-claim-gray.svg'); + $stream = fopen($logoFile, 'rb'); + + $storage = Tool\Storage::get('admin'); + if ($storage->fileExists(self::LOGO_PATH)) { + try { + $mime = $storage->mimeType(self::LOGO_PATH); + $stream = $storage->readStream(self::LOGO_PATH); + } catch (Exception) { + // keep default stream and mime on storage error + } + } + + return new DisplayCustomLogoResult(mime: $mime, stream: $stream); + } +} diff --git a/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoPayload.php b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoPayload.php new file mode 100644 index 00000000..0013da89 --- /dev/null +++ b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoPayload.php @@ -0,0 +1,35 @@ +query->has('white'), + ); + } +} diff --git a/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoResult.php b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoResult.php new file mode 100644 index 00000000..438021e2 --- /dev/null +++ b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoResult.php @@ -0,0 +1,26 @@ +config->getAdminSystemSettingsConfig()); + } +} diff --git a/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php b/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php new file mode 100644 index 00000000..a762b823 --- /dev/null +++ b/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php @@ -0,0 +1,23 @@ + $lang, 'display' => $locales[$lang]]; + } + } + + usort($langs, static fn ($a, $b) => strcmp($a['display'], $b['display'])); + + return new GetAvailableAdminLanguagesResult(langs: $langs); + } +} diff --git a/src/Handler/Settings/GetAvailableAdminLanguages/GetAvailableAdminLanguagesResult.php b/src/Handler/Settings/GetAvailableAdminLanguages/GetAvailableAdminLanguagesResult.php new file mode 100644 index 00000000..c986ea43 --- /dev/null +++ b/src/Handler/Settings/GetAvailableAdminLanguages/GetAvailableAdminLanguagesResult.php @@ -0,0 +1,23 @@ + 'password_hash', 'value' => 'password_hash']]; + + foreach (hash_algos() as $algorithm) { + $options[] = [ + 'key' => $algorithm . ' (' . $this->translator->trans('deprecated', [], 'admin') . ')', + 'value' => $algorithm, + ]; + } + + return new GetAvailableAlgorithmsResult(options: $options); + } +} diff --git a/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php b/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php new file mode 100644 index 00000000..849e6f10 --- /dev/null +++ b/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php @@ -0,0 +1,23 @@ +localeService->getDisplayRegions(); + asort($countries); + + $options = []; + foreach ($countries as $short => $translation) { + if (strlen((string) $short) === 2) { + $options[] = ['key' => $translation . ' (' . $short . ')', 'value' => $short]; + } + } + + return new GetAvailableCountriesResult(options: $options); + } +} diff --git a/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php b/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php new file mode 100644 index 00000000..f086832d --- /dev/null +++ b/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php @@ -0,0 +1,23 @@ +load(); + $sites = []; + + if (!$excludeMainSite) { + $sites[] = [ + 'id' => 0, + 'rootId' => 1, + 'domains' => '', + 'rootPath' => '/', + 'domain' => $this->translator->trans('main_site', [], 'admin'), + ]; + } + + foreach ($sitesObjects as $site) { + if ($site->getRootDocument()) { + if ($site->getMainDomain()) { + $sites[] = [ + 'id' => $site->getId(), + 'rootId' => $site->getRootId(), + 'domains' => implode(',', $site->getDomains()), + 'rootPath' => $site->getRootPath(), + 'domain' => $site->getMainDomain(), + ]; + } + } else { + $site->delete(); + } + } + + return new GetAvailableSitesResult(sites: $sites); + } +} diff --git a/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php new file mode 100644 index 00000000..cb01b61c --- /dev/null +++ b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php @@ -0,0 +1,23 @@ +setFilter(fn (Asset\Image\Thumbnail\Config $config) => $config->isDownloadable()); + + foreach ($list->getThumbnails() as $item) { + $thumbnails[] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + ]; + } + + return new GetDownloadableThumbnailsResult(thumbnails: $thumbnails); + } +} diff --git a/src/Handler/Settings/GetDownloadableThumbnails/GetDownloadableThumbnailsResult.php b/src/Handler/Settings/GetDownloadableThumbnails/GetDownloadableThumbnailsResult.php new file mode 100644 index 00000000..596fc7c0 --- /dev/null +++ b/src/Handler/Settings/GetDownloadableThumbnails/GetDownloadableThumbnailsResult.php @@ -0,0 +1,25 @@ +getGroup() ?? ''; + if ($group === 'default' || $group === $itemGroup) { + $item->expand(); + $data = $item->getObjectVars(); + $data['writeable'] = $item->isWriteable(); + $result[] = $data; + } + } + + return new GetFilteredPredefinedMetadataResult(data: $result); + } +} diff --git a/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php new file mode 100644 index 00000000..b2bf13ff --- /dev/null +++ b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php @@ -0,0 +1,23 @@ +filter) { + $list->setFilter(function (Metadata\Predefined $predefined) use ($payload) { + foreach ($predefined->getObjectVars() as $value) { + if (stripos((string) $value, $payload->filter) !== false) { + return true; + } + } + + return false; + }); + } + + $properties = []; + foreach ($list->getDefinitions() as $metadata) { + $metadata->expand(); + $data = $metadata->getObjectVars(); + $data['writeable'] = $metadata->isWriteable(); + $properties[] = $data; + } + + return new GetPredefinedMetadataListResult(data: $properties, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Settings/GetPredefinedMetadataList/GetPredefinedMetadataListResult.php b/src/Handler/Settings/GetPredefinedMetadataList/GetPredefinedMetadataListResult.php new file mode 100644 index 00000000..21563615 --- /dev/null +++ b/src/Handler/Settings/GetPredefinedMetadataList/GetPredefinedMetadataListResult.php @@ -0,0 +1,26 @@ +filter) { + $list->setFilter(function (Property\Predefined $predefined) use ($payload) { + foreach ($predefined->getObjectVars() as $value) { + if ($value) { + $cellValues = is_array($value) ? $value : [$value]; + + foreach ($cellValues as $cellValue) { + if (stripos((string) $cellValue, $payload->filter) !== false) { + return true; + } + } + } + } + + return false; + }); + } + + $properties = []; + foreach ($list->getProperties() as $property) { + $data = $property->getObjectVars(); + $data['writeable'] = $property->isWriteable(); + $properties[] = $data; + } + + return new GetPredefinedPropertiesListResult(data: $properties, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/Settings/GetPredefinedPropertiesList/GetPredefinedPropertiesListResult.php b/src/Handler/Settings/GetPredefinedPropertiesList/GetPredefinedPropertiesListResult.php new file mode 100644 index 00000000..d04f2024 --- /dev/null +++ b/src/Handler/Settings/GetPredefinedPropertiesList/GetPredefinedPropertiesListResult.php @@ -0,0 +1,26 @@ +config->getSystemSettingsConfig(); + + // If required languages is empty it's the same as if all languages are required. Therefore, we + // need to overwrite the value with the valid languages value to have all languages required + if (empty($config['general']['required_languages'])) { + $config['general']['required_languages'] = $config['general']['valid_languages']; + } + + $values = [ + 'general' => $config['general'], + 'documents' => $config['documents'], + 'assets' => $config['assets'], + 'objects' => $config['objects'], + 'email' => $config['email'], + 'writeable' => $config['writeable'], + ]; + + $locales = Tool::getSupportedLocales(); + $languageOptions = []; + $validLanguages = []; + + foreach ($locales as $short => $translation) { + if (!empty($short)) { + $languageOptions[] = ['language' => $short, 'display' => $translation . " ($short)"]; + $validLanguages[] = $short; + } + } + + foreach ($values['general']['valid_languages'] as $existingValue) { + if (!in_array($existingValue, $validLanguages, true)) { + $languageOptions[] = ['language' => $existingValue, 'display' => $existingValue]; + } + } + + return new GetSystemSettingsResult(values: $values, languages: $languageOptions); + } +} diff --git a/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php b/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php new file mode 100644 index 00000000..aa0370ba --- /dev/null +++ b/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php @@ -0,0 +1,26 @@ +getObjectVars(); + $data['writeable'] = $pipe->isWriteable(); + + return new GetThumbnailResult(data: $data); + } +} diff --git a/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php b/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php new file mode 100644 index 00000000..ee00700f --- /dev/null +++ b/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php @@ -0,0 +1,25 @@ +getThumbnails() as $item) { + if ($item->getGroup()) { + if (empty($groups[$item->getGroup()])) { + $groups[$item->getGroup()] = [ + 'id' => 'group_' . $item->getName(), + 'text' => htmlspecialchars($item->getGroup()), + 'expandable' => true, + 'leaf' => false, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'group' => $item->getGroup(), + 'children' => [], + ]; + } + $groups[$item->getGroup()]['children'][] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_thumbnails', + 'cls' => 'opendxp_treenode_disabled', + 'writeable' => $item->isWriteable(), + ]; + } else { + $thumbnails[] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_thumbnails', + 'cls' => 'opendxp_treenode_disabled', + 'writeable' => $item->isWriteable(), + ]; + } + } + + foreach ($groups as $group) { + $thumbnails[] = $group; + } + + return new GetThumbnailTreeResult(nodes: $thumbnails); + } +} diff --git a/src/Handler/Settings/GetThumbnailTree/GetThumbnailTreeResult.php b/src/Handler/Settings/GetThumbnailTree/GetThumbnailTreeResult.php new file mode 100644 index 00000000..2939197c --- /dev/null +++ b/src/Handler/Settings/GetThumbnailTree/GetThumbnailTreeResult.php @@ -0,0 +1,25 @@ +getObjectVars(); + $data['writeable'] = $pipe->isWriteable(); + + return new GetVideoThumbnailResult(data: $data); + } +} diff --git a/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php new file mode 100644 index 00000000..8e43dec1 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php @@ -0,0 +1,25 @@ + 'opendxp-system-treepreview', 'text' => 'original'], + ]; + $list = new Asset\Video\Thumbnail\Config\Listing(); + + foreach ($list->getThumbnails() as $item) { + $thumbnails[] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + ]; + } + + return new GetVideoThumbnailListResult(thumbnails: $thumbnails); + } +} diff --git a/src/Handler/Settings/GetVideoThumbnailList/GetVideoThumbnailListResult.php b/src/Handler/Settings/GetVideoThumbnailList/GetVideoThumbnailListResult.php new file mode 100644 index 00000000..27bbe5e6 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnailList/GetVideoThumbnailListResult.php @@ -0,0 +1,25 @@ +getThumbnails() as $item) { + if ($item->getGroup()) { + if (empty($groups[$item->getGroup()])) { + $groups[$item->getGroup()] = [ + 'id' => 'group_' . $item->getName(), + 'text' => htmlspecialchars($item->getGroup()), + 'expandable' => true, + 'leaf' => false, + 'allowChildren' => true, + 'iconCls' => 'opendxp_icon_folder', + 'group' => $item->getGroup(), + 'children' => [], + ]; + } + $groups[$item->getGroup()]['children'][] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_videothumbnails', + 'cls' => 'opendxp_treenode_disabled', + 'writeable' => $item->isWriteable(), + ]; + } else { + $thumbnails[] = [ + 'id' => $item->getName(), + 'text' => $item->getName(), + 'leaf' => true, + 'iconCls' => 'opendxp_icon_videothumbnails', + 'cls' => 'opendxp_treenode_disabled', + 'writeable' => $item->isWriteable(), + ]; + } + } + + foreach ($groups as $group) { + $thumbnails[] = $group; + } + + return new GetVideoThumbnailTreeResult(nodes: $thumbnails); + } +} diff --git a/src/Handler/Settings/GetVideoThumbnailTree/GetVideoThumbnailTreeResult.php b/src/Handler/Settings/GetVideoThumbnailTree/GetVideoThumbnailTreeResult.php new file mode 100644 index 00000000..aac52476 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnailTree/GetVideoThumbnailTreeResult.php @@ -0,0 +1,25 @@ +setLimit($payload->limit); + $list->setOffset($payload->offset); + + if ($payload->orderKey) { + $list->setOrderKey($payload->orderKey); + $list->setOrder($payload->order); + } else { + $list->setOrderKey('name'); + $list->setOrder('asc'); + } + + if ($payload->filter) { + $list->setCondition('`name` LIKE ' . $list->quote('%' . $payload->filter . '%')); + } + + $totalCount = $list->getTotalCount(); + $items = $list->load(); + + $settings = []; + foreach ($items as $item) { + $settings[] = $this->buildEditModeData($item); + } + + return new GetWebsiteSettingsListResult(data: $settings, total: $totalCount); + } + + /** + * @return array{id: ?int, name: string, language: string, type: string, data: mixed, siteId: ?int, creationDate: ?int, modificationDate: ?int} + */ + private function buildEditModeData(WebsiteSetting $item): array + { + $resultItem = [ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'language' => $item->getLanguage(), + 'type' => $item->getType(), + 'data' => null, + 'siteId' => $item->getSiteId(), + 'creationDate' => $item->getCreationDate(), + 'modificationDate' => $item->getModificationDate(), + ]; + + switch ($item->getType()) { + case 'document': + case 'asset': + case 'object': + $element = $item->getData(); + if ($element) { + $resultItem['data'] = $element->getRealFullPath(); + } + + break; + default: + $resultItem['data'] = $item->getData(); + + break; + } + + return $resultItem; + } +} diff --git a/src/Handler/Settings/GetWebsiteSettingsList/GetWebsiteSettingsListResult.php b/src/Handler/Settings/GetWebsiteSettingsList/GetWebsiteSettingsListResult.php new file mode 100644 index 00000000..35fe1c88 --- /dev/null +++ b/src/Handler/Settings/GetWebsiteSettingsList/GetWebsiteSettingsListResult.php @@ -0,0 +1,26 @@ +request->has('data')) { + return new static( + hasData: true, + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } + + return new static( + hasData: false, + filter: $request->request->get('filter'), + ); + } +} diff --git a/src/Handler/Settings/PredefinedPropertyPayload.php b/src/Handler/Settings/PredefinedPropertyPayload.php new file mode 100644 index 00000000..eeba4688 --- /dev/null +++ b/src/Handler/Settings/PredefinedPropertyPayload.php @@ -0,0 +1,44 @@ +request->has('data')) { + return new static( + hasData: true, + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } + + return new static( + hasData: false, + filter: $request->request->get('filter'), + ); + } +} diff --git a/src/Handler/Settings/SaveAppearanceSettings/SaveAppearanceSettingsHandler.php b/src/Handler/Settings/SaveAppearanceSettings/SaveAppearanceSettingsHandler.php new file mode 100644 index 00000000..db0ce3c2 --- /dev/null +++ b/src/Handler/Settings/SaveAppearanceSettings/SaveAppearanceSettingsHandler.php @@ -0,0 +1,57 @@ +env ?: $this->kernel->getEnvironment(); + + $this->config->save($payload->values); + $this->symfonyCache->clear($env); + $this->stopMessengerWorkers(); + + $openDxpCache = $this->openDxpCache; + $this->eventDispatcher->addListener(KernelEvents::TERMINATE, static function (TerminateEvent $event) use ($openDxpCache): void { + // delay to ensure messenger:stop-workers signal has been processed before cache is cleared + sleep(2); + $openDxpCache->clear(); + }); + } +} diff --git a/src/Handler/Settings/SaveSettingsPayload.php b/src/Handler/Settings/SaveSettingsPayload.php new file mode 100644 index 00000000..62fdb50e --- /dev/null +++ b/src/Handler/Settings/SaveSettingsPayload.php @@ -0,0 +1,36 @@ +request->getString('data'), true), + env: $request->request->getString('env'), + ); + } +} diff --git a/src/Handler/Settings/SaveSystemSettings/SaveSystemSettingsHandler.php b/src/Handler/Settings/SaveSystemSettings/SaveSystemSettingsHandler.php new file mode 100644 index 00000000..6d650984 --- /dev/null +++ b/src/Handler/Settings/SaveSystemSettings/SaveSystemSettingsHandler.php @@ -0,0 +1,57 @@ +env ?: $this->kernel->getEnvironment(); + + $this->config->save($payload->values); + $this->symfonyCache->clear($env); + $this->stopMessengerWorkers(); + + $openDxpCache = $this->openDxpCache; + $this->eventDispatcher->addListener(KernelEvents::TERMINATE, static function (TerminateEvent $event) use ($openDxpCache): void { + // delay to ensure messenger:stop-workers signal has been processed before cache is cleared + sleep(2); + $openDxpCache->clear(); + }); + } +} diff --git a/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckHandler.php b/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckHandler.php new file mode 100644 index 00000000..ae7c769b --- /dev/null +++ b/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckHandler.php @@ -0,0 +1,42 @@ +' . + $this->translator->trans('important_use_imagick_pecl_extensions_for_best_results_gd_is_just_a_fallback_with_less_quality', [], 'admin') . + ''; + } + + return new ThumbnailAdapterCheckResult(content: $content); + } +} diff --git a/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckResult.php b/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckResult.php new file mode 100644 index 00000000..42b55b3b --- /dev/null +++ b/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckResult.php @@ -0,0 +1,23 @@ +data; + $metadata = Metadata\Predefined::getById($data['id']); + + if (!$metadata->isWriteable()) { + throw new ConfigWriteException(); + } + + $metadata->setValues($data); + + $existingItem = Metadata\Predefined\Listing::getByKeyAndLanguage( + $metadata->getName(), + $metadata->getLanguage(), + $metadata->getTargetSubtype() + ); + + if ($existingItem && $existingItem->getId() !== $metadata->getId()) { + throw new BadRequestHttpException('predefined_metadata_definitions_error_name_exists_msg'); + } + + $metadata->minimize(); + $metadata->save(); + $metadata->expand(); + + $responseData = $metadata->getObjectVars(); + $responseData['writeable'] = $metadata->isWriteable(); + + return new UpdatePredefinedMetadataResult(data: $responseData); + } +} diff --git a/src/Handler/Settings/UpdatePredefinedMetadata/UpdatePredefinedMetadataResult.php b/src/Handler/Settings/UpdatePredefinedMetadata/UpdatePredefinedMetadataResult.php new file mode 100644 index 00000000..3389653a --- /dev/null +++ b/src/Handler/Settings/UpdatePredefinedMetadata/UpdatePredefinedMetadataResult.php @@ -0,0 +1,25 @@ +data; + $property = Property\Predefined::getById($data['id']); + + if (!$property->isWriteable()) { + throw new ConfigWriteException(); + } + + if (is_array($data['ctype'])) { + $data['ctype'] = implode(',', $data['ctype']); + } + + $property->setValues($data); + $property->save(); + + $responseData = $property->getObjectVars(); + $responseData['writeable'] = $property->isWriteable(); + + return new UpdatePredefinedPropertyResult(data: $responseData); + } +} diff --git a/src/Handler/Settings/UpdatePredefinedProperty/UpdatePredefinedPropertyResult.php b/src/Handler/Settings/UpdatePredefinedProperty/UpdatePredefinedPropertyResult.php new file mode 100644 index 00000000..6307598e --- /dev/null +++ b/src/Handler/Settings/UpdatePredefinedProperty/UpdatePredefinedPropertyResult.php @@ -0,0 +1,25 @@ +name); + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + foreach ($payload->settingsData as $key => $value) { + $setter = 'set' . ucfirst($key); + if (method_exists($pipe, $setter)) { + $pipe->$setter($value); + } + } + + $pipe->resetItems(); + + $mediaData = $payload->mediaData; + $mediaOrder = $payload->mediaOrder; + + uksort($mediaData, static function ($a, $b) use ($mediaOrder) { + if ($a === 'default') { + return -1; + } + + return ($mediaOrder[$a] < $mediaOrder[$b]) ? -1 : 1; + }); + + foreach ($mediaData as $mediaName => $items) { + if (preg_match('/["<>]/', $mediaName)) { + throw new Exception('Invalid media query name'); + } + + foreach ($items as $item) { + $type = $item['type']; + unset($item['type']); + + $pipe->addItem($type, $item, $mediaName); + } + } + + $pipe->save(); + } +} diff --git a/src/Handler/Settings/UpdateThumbnail/UpdateThumbnailPayload.php b/src/Handler/Settings/UpdateThumbnail/UpdateThumbnailPayload.php new file mode 100644 index 00000000..5f93a53d --- /dev/null +++ b/src/Handler/Settings/UpdateThumbnail/UpdateThumbnailPayload.php @@ -0,0 +1,40 @@ +request->getString('name'), + settingsData: json_decode($request->request->getString('settings'), true), + mediaData: json_decode($request->request->getString('medias'), true), + mediaOrder: json_decode($request->request->getString('mediaOrder'), true), + ); + } +} diff --git a/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailHandler.php b/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailHandler.php new file mode 100644 index 00000000..84fe5f56 --- /dev/null +++ b/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailHandler.php @@ -0,0 +1,65 @@ +name); + + if (!$pipe->isWriteable()) { + throw new ConfigWriteException(); + } + + foreach ($payload->settingsData as $key => $value) { + $setter = 'set' . ucfirst($key); + if (method_exists($pipe, $setter)) { + $pipe->$setter($value); + } + } + + $pipe->resetItems(); + + $mediaData = $payload->mediaData; + $mediaOrder = $payload->mediaOrder; + + uksort($mediaData, static function ($a, $b) use ($mediaOrder) { + if ($a === 'default') { + return -1; + } + + return ($mediaOrder[$a] < $mediaOrder[$b]) ? -1 : 1; + }); + + foreach ($mediaData as $mediaName => $items) { + foreach ($items as $item) { + $type = $item['type']; + unset($item['type']); + + $pipe->addItem($type, $item, htmlspecialchars($mediaName)); + } + } + + $pipe->save(); + } +} diff --git a/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailPayload.php b/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailPayload.php new file mode 100644 index 00000000..77192452 --- /dev/null +++ b/src/Handler/Settings/UpdateVideoThumbnail/UpdateVideoThumbnailPayload.php @@ -0,0 +1,40 @@ +request->getString('name'), + settingsData: json_decode($request->request->getString('settings'), true), + mediaData: json_decode($request->request->getString('medias'), true), + mediaOrder: json_decode($request->request->getString('mediaOrder'), true), + ); + } +} diff --git a/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingHandler.php b/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingHandler.php new file mode 100644 index 00000000..5ad28d72 --- /dev/null +++ b/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingHandler.php @@ -0,0 +1,88 @@ +data; + $setting = WebsiteSetting::getById($data['id']); + + if (!$setting instanceof WebsiteSetting) { + throw new NotFoundHttpException(sprintf('WebsiteSetting with id %d not found', $data['id'])); + } + + switch ($setting->getType()) { + case 'document': + case 'asset': + case 'object': + if (isset($data['data'])) { + $element = Element\Service::getElementByPath($setting->getType(), $data['data']); + $data['data'] = $element; + } + + break; + } + + $setting->setValues($data); + $setting->save(); + + return new UpdateWebsiteSettingResult(data: $this->buildEditModeData($setting)); + } + + /** + * @return array{id: ?int, name: string, language: string, type: string, data: mixed, siteId: ?int, creationDate: ?int, modificationDate: ?int} + */ + private function buildEditModeData(WebsiteSetting $item): array + { + $resultItem = [ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'language' => $item->getLanguage(), + 'type' => $item->getType(), + 'data' => null, + 'siteId' => $item->getSiteId(), + 'creationDate' => $item->getCreationDate(), + 'modificationDate' => $item->getModificationDate(), + ]; + + switch ($item->getType()) { + case 'document': + case 'asset': + case 'object': + $element = $item->getData(); + if ($element) { + $resultItem['data'] = $element->getRealFullPath(); + } + + break; + default: + $resultItem['data'] = $item->getData(); + + break; + } + + return $resultItem; + } +} diff --git a/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingResult.php b/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingResult.php new file mode 100644 index 00000000..accbad6c --- /dev/null +++ b/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingResult.php @@ -0,0 +1,25 @@ +writeStream(self::LOGO_PATH, fopen($pathname, 'rb')); + } +} diff --git a/src/Handler/Settings/WebsiteSettingPayload.php b/src/Handler/Settings/WebsiteSettingPayload.php new file mode 100644 index 00000000..86bcb333 --- /dev/null +++ b/src/Handler/Settings/WebsiteSettingPayload.php @@ -0,0 +1,63 @@ +request->has('data')) { + $data = json_decode($request->request->getString('data'), true) ?? []; + + if (is_array($data)) { + foreach ($data as &$value) { + if (is_string($value)) { + $value = trim($value); + } + } + unset($value); + } + + return new static(hasData: true, data: $data); + } + + $sortingSettings = QueryParams::extractSortingSettings([...$request->request->all(), ...$request->query->all()]); + + return new static( + hasData: false, + limit: $request->request->getInt('limit', 50), + offset: $request->request->getInt('start', 0), + orderKey: $sortingSettings['orderKey'] ?: null, + order: $sortingSettings['order'] ?? null, + filter: $request->request->has('filter') ? $request->request->getString('filter') : null, + ); + } +} diff --git a/src/Handler/Tags/AddTag/AddTagHandler.php b/src/Handler/Tags/AddTag/AddTagHandler.php new file mode 100644 index 00000000..384fcca8 --- /dev/null +++ b/src/Handler/Tags/AddTag/AddTagHandler.php @@ -0,0 +1,33 @@ +setName($payload->text); + $tag->setParentId($payload->parentId); + $tag->save(); + + return new AddTagResult(id: $tag->getId()); + } +} diff --git a/src/Handler/Tags/AddTag/AddTagPayload.php b/src/Handler/Tags/AddTag/AddTagPayload.php new file mode 100644 index 00000000..2d2498a1 --- /dev/null +++ b/src/Handler/Tags/AddTag/AddTagPayload.php @@ -0,0 +1,37 @@ +request->get('text', '')), + parentId: (int) $request->request->get('parentId'), + ); + } +} diff --git a/src/Handler/Tags/AddTag/AddTagResult.php b/src/Handler/Tags/AddTag/AddTagResult.php new file mode 100644 index 00000000..74db5166 --- /dev/null +++ b/src/Handler/Tags/AddTag/AddTagResult.php @@ -0,0 +1,25 @@ +tagId); + if (!$tag) { + throw new NotFoundHttpException('Tag with ID ' . $payload->tagId . ' not found.'); + } + + Tag::addTagToElement($payload->elementType, $payload->elementId, $tag); + + return new AddTagToElementResult(id: $tag->getId()); + } +} diff --git a/src/Handler/Tags/AddTagToElement/AddTagToElementPayload.php b/src/Handler/Tags/AddTagToElement/AddTagToElementPayload.php new file mode 100644 index 00000000..115cff05 --- /dev/null +++ b/src/Handler/Tags/AddTagToElement/AddTagToElementPayload.php @@ -0,0 +1,39 @@ +request->get('tagId'), + elementType: strip_tags($request->request->get('assignmentElementType', '')), + elementId: (int) $request->request->get('assignmentElementId'), + ); + } +} diff --git a/src/Handler/Tags/AddTagToElement/AddTagToElementResult.php b/src/Handler/Tags/AddTagToElement/AddTagToElementResult.php new file mode 100644 index 00000000..10075c1d --- /dev/null +++ b/src/Handler/Tags/AddTagToElement/AddTagToElementResult.php @@ -0,0 +1,25 @@ +id); + if (!$tag) { + throw new NotFoundHttpException('Tag with ID ' . $payload->id . ' not found.'); + } + + $tag->delete(); + } +} diff --git a/src/Handler/Tags/DeleteTag/DeleteTagPayload.php b/src/Handler/Tags/DeleteTag/DeleteTagPayload.php new file mode 100644 index 00000000..76845219 --- /dev/null +++ b/src/Handler/Tags/DeleteTag/DeleteTagPayload.php @@ -0,0 +1,35 @@ +request->get('id'), + ); + } +} diff --git a/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentHandler.php b/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentHandler.php new file mode 100644 index 00000000..1eae25a0 --- /dev/null +++ b/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentHandler.php @@ -0,0 +1,28 @@ +elementType, $payload->childrenIds, $payload->assignedTags, $payload->doCleanupTags); + } +} diff --git a/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentPayload.php b/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentPayload.php new file mode 100644 index 00000000..e0cd54e0 --- /dev/null +++ b/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentPayload.php @@ -0,0 +1,41 @@ +request->get('elementType', '')), + childrenIds: json_decode($request->request->get('childrenIds'), true) ?? [], + assignedTags: json_decode($request->request->get('assignedTags'), true) ?? [], + doCleanupTags: $request->request->get('removeAndApply') === 'true', + ); + } +} diff --git a/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsHandler.php b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsHandler.php new file mode 100644 index 00000000..228143d1 --- /dev/null +++ b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsHandler.php @@ -0,0 +1,170 @@ +elementType; + $elementId = $payload->elementId; + $adminUser = $this->userContext->getAdminUser(); + $userIds = null; + if (!$adminUser?->isAdmin()) { + $userIds = $adminUser?->getRoles() ?? []; + $userIds[] = $adminUser?->getId(); + } + $idList = []; + + switch ($elementType) { + case 'object': + $object = DataObject::getById($elementId); + if ($object) { + $idList = $this->getSubObjectIds($object, $userIds); + } + break; + + case 'asset': + $asset = Asset::getById($elementId); + if ($asset) { + $idList = $this->getSubAssetIds($asset, $userIds); + } + break; + + case 'document': + $document = Document::getById($elementId); + if ($document) { + $idList = $this->getSubDocumentIds($document, $userIds); + } + break; + } + + $size = 2; + $offset = 0; + $idListParts = []; + while ($offset < count($idList)) { + $idListParts[] = array_slice($idList, $offset, $size); + $offset += $size; + } + + return new GetBatchAssignmentJobsResult(idListParts: $idListParts, totalCount: count($idList)); + } + + /** + * @param int[]|null $userIds + * + * @return int[] + */ + private function getSubObjectIds(DataObject\AbstractObject $object, ?array $userIds): array + { + $childrenList = new DataObject\Listing(); + $condition = '`path` LIKE ?'; + if ($userIds !== null) { + $condition .= ' AND ( + (SELECT `view` FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,`key`),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + OR + (SELECT `view` FROM users_workspaces_object WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,`key`))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + )'; + } + + $childrenList->setCondition($condition, $childrenList->escapeLike($object->getRealFullPath()) . '/%'); + + $beforeListLoadEvent = new GenericEvent(null, [ + 'list' => $childrenList, + 'context' => [], + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); + /** @var DataObject\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + return $childrenList->loadIdList(); + } + + /** + * @param int[]|null $userIds + * + * @return int[] + */ + private function getSubAssetIds(Asset $asset, ?array $userIds): array + { + $childrenList = new Asset\Listing(); + $condition = '`path` LIKE ?'; + if ($userIds !== null) { + $condition .= ' AND ( + (SELECT `view` FROM users_workspaces_asset WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,filename),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + OR + (SELECT `view` FROM users_workspaces_asset WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,filename))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + )'; + } + + $childrenList->setCondition($condition, $childrenList->escapeLike($asset->getRealFullPath()) . '/%'); + + $beforeListLoadEvent = new GenericEvent(null, [ + 'list' => $childrenList, + 'context' => [], + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); + /** @var Asset\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + return $childrenList->loadIdList(); + } + + /** + * @param int[]|null $userIds + * + * @return int[] + */ + private function getSubDocumentIds(Document $document, ?array $userIds): array + { + $childrenList = new Document\Listing(); + $condition = '`path` LIKE ?'; + if ($userIds !== null) { + $condition .= ' AND ( + (SELECT `view` FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(CONCAT(`path`,`key`),cpath)=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + OR + (SELECT `view` FROM users_workspaces_document WHERE userId IN (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(`path`,`key`))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1 + )'; + } + + $childrenList->setCondition($condition, $childrenList->escapeLike($document->getRealFullPath()) . '/%'); + + $beforeListLoadEvent = new GenericEvent(null, [ + 'list' => $childrenList, + 'context' => [], + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::DOCUMENT_LIST_BEFORE_LIST_LOAD); + /** @var Document\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + return $childrenList->loadIdList(); + } +} diff --git a/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsPayload.php b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsPayload.php new file mode 100644 index 00000000..0ff602cf --- /dev/null +++ b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsPayload.php @@ -0,0 +1,37 @@ +query->get('elementType', ''), + elementId: $request->query->has('elementId') ? (int) $request->query->get('elementId') : 0, + ); + } +} diff --git a/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsResult.php b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsResult.php new file mode 100644 index 00000000..76a61602 --- /dev/null +++ b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsResult.php @@ -0,0 +1,26 @@ +assignmentCId && $payload->assignmentCType) { + $assignedTags = Tag::getTagsForElement($payload->assignmentCType, $payload->assignmentCId); + foreach ($assignedTags as $assignedTag) { + $assignedTagIds[$assignedTag->getId()] = $assignedTag; + } + } + + $tagList = new Tag\Listing(); + if ($payload->node) { + $tagList->setCondition('parentId = ?', $payload->node); + } else { + $tagList->setCondition('ISNULL(parentId) OR parentId = 0'); + } + $tagList->setOrderKey('name'); + + $recursiveChildren = false; + if (!empty($payload->filter)) { + $filterIds = [0]; + $filterTagList = new Tag\Listing(); + $filterTagList->setCondition('LOWER(`name`) LIKE ?', ['%' . $filterTagList->escapeLike(mb_strtolower($payload->filter)) . '%']); + foreach ($filterTagList->load() as $filterTag) { + if ($filterTag->getParentId() === 0) { + $filterIds[] = $filterTag->getId(); + } else { + $ids = explode('/', $filterTag->getIdPath()); + if (isset($ids[1])) { + $filterIds[] = (int) $ids[1]; + } + } + } + + $filterIds = array_unique($filterIds); + $tagList->setCondition('id IN(' . implode(',', $filterIds) . ')'); + $recursiveChildren = true; + } + + $tags = []; + foreach ($tagList->load() as $tag) { + $tags[] = $this->convertTagToArray($tag, $payload->showSelection, $assignedTagIds, true, $recursiveChildren); + } + + return new GetTagTreeChildrenResult(tags: $tags); + } + + private function convertTagToArray(Tag $tag, bool $showSelection, array $assignedTagIds, bool $loadChildren = false, bool $recursiveChildren = false): array + { + $hasChildren = $tag->hasChildren(); + + $tagArray = [ + 'id' => $tag->getId(), + 'text' => $tag->getName(), + 'path' => $tag->getNamePath(), + 'expandable' => $hasChildren, + 'leaf' => !$hasChildren, + 'iconCls' => 'opendxp_icon_element_tags', + 'qtipCfg' => [ + 'title' => 'ID: ' . $tag->getId(), + ], + ]; + + if ($showSelection) { + $tagArray['checked'] = isset($assignedTagIds[$tag->getId()]); + } + + if ($hasChildren && $loadChildren) { + $children = $tag->getChildren(); + $loadChildren = $recursiveChildren; + foreach ($children as $child) { + $tagArray['children'][] = $this->convertTagToArray($child, $showSelection, $assignedTagIds, $loadChildren, $recursiveChildren); + } + } + + return $tagArray; + } +} diff --git a/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenPayload.php b/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenPayload.php new file mode 100644 index 00000000..721d20cb --- /dev/null +++ b/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenPayload.php @@ -0,0 +1,43 @@ +query->get('showSelection') === 'true', + assignmentCId: $request->query->has('assignmentCId') ? (int) $request->query->get('assignmentCId') : null, + assignmentCType: $request->query->get('assignmentCType', ''), + node: $request->query->get('node'), + filter: $request->query->get('filter'), + ); + } +} diff --git a/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenResult.php b/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenResult.php new file mode 100644 index 00000000..6d5d232a --- /dev/null +++ b/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenResult.php @@ -0,0 +1,25 @@ +assignmentCType, $payload->assignmentCId); + + foreach ($assignedTags as $assignedTag) { + $assignedTagArray[] = $this->convertTagToArray($assignedTag); + } + + return new GetTagsForElementResult(tags: $assignedTagArray); + } + + private function convertTagToArray(Tag $tag): array + { + $hasChildren = $tag->hasChildren(); + + return [ + 'id' => $tag->getId(), + 'text' => $tag->getName(), + 'path' => $tag->getNamePath(), + 'expandable' => $hasChildren, + 'leaf' => !$hasChildren, + 'iconCls' => 'opendxp_icon_element_tags', + 'qtipCfg' => [ + 'title' => 'ID: ' . $tag->getId(), + ], + ]; + } +} diff --git a/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementResult.php b/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementResult.php new file mode 100644 index 00000000..9d249129 --- /dev/null +++ b/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementResult.php @@ -0,0 +1,25 @@ +query->has('assignmentCId') ? (int) $request->query->get('assignmentCId') : null, + assignmentCType: $request->query->get('assignmentCType', ''), + ); + } +} diff --git a/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementHandler.php b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementHandler.php new file mode 100644 index 00000000..f4742bf7 --- /dev/null +++ b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementHandler.php @@ -0,0 +1,36 @@ +tagId); + if (!$tag) { + throw new NotFoundHttpException('Tag with ID ' . $payload->tagId . ' not found.'); + } + + Tag::removeTagFromElement($payload->elementType, $payload->elementId, $tag); + + return new RemoveTagFromElementResult(id: $tag->getId()); + } +} diff --git a/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementPayload.php b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementPayload.php new file mode 100644 index 00000000..314549dc --- /dev/null +++ b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementPayload.php @@ -0,0 +1,39 @@ +request->get('tagId'), + elementType: strip_tags($request->request->get('assignmentElementType', '')), + elementId: (int) $request->request->get('assignmentElementId'), + ); + } +} diff --git a/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementResult.php b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementResult.php new file mode 100644 index 00000000..6ebc4592 --- /dev/null +++ b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementResult.php @@ -0,0 +1,25 @@ +id); + if (!$tag) { + throw new NotFoundHttpException('Tag with ID ' . $payload->id . ' not found.'); + } + + if ($payload->parentId !== null) { + $tag->setParentId($payload->parentId); + } + + if ($payload->name !== null) { + $tag->setName($payload->name); + } + + $tag->save(); + } +} diff --git a/src/Handler/Tags/UpdateTag/UpdateTagPayload.php b/src/Handler/Tags/UpdateTag/UpdateTagPayload.php new file mode 100644 index 00000000..3e93a576 --- /dev/null +++ b/src/Handler/Tags/UpdateTag/UpdateTagPayload.php @@ -0,0 +1,41 @@ +request->get('parentId'); + + return new static( + id: (int) $request->request->get('id'), + parentId: ($parentId || $parentId === '0') ? (int) $parentId : null, + name: $request->request->has('text') ? strip_tags($request->request->get('text', '')) : null, + ); + } +} diff --git a/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysHandler.php b/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysHandler.php new file mode 100644 index 00000000..998f575a --- /dev/null +++ b/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysHandler.php @@ -0,0 +1,59 @@ +keys as $translationData) { + $t = null; + + try { + $t = Translation::getByKey($translationData, Translation::DOMAIN_ADMIN); + } catch (Exception $e) { + Logger::log((string) $e); + } + + if (!$t instanceof Translation) { + $t = new Translation(); + $t->setDomain(Translation::DOMAIN_ADMIN); + $t->setKey($translationData); + $t->setCreationDate(time()); + $t->setModificationDate(time()); + + foreach ($availableLanguages as $lang) { + $t->addTranslation($lang, ''); + } + + try { + $t->save(); + } catch (Exception $e) { + Logger::log((string) $e); + } + } + } + } +} diff --git a/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysPayload.php b/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysPayload.php new file mode 100644 index 00000000..79b320a9 --- /dev/null +++ b/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysPayload.php @@ -0,0 +1,36 @@ +request->get('keys'); + + return new static( + keys: $keys ? json_decode($keys, true) : [], + ); + } +} diff --git a/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsHandler.php b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsHandler.php new file mode 100644 index 00000000..5a13f90d --- /dev/null +++ b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsHandler.php @@ -0,0 +1,111 @@ +data !== []) { + foreach ($payload->data as $element) { + $elements[$element['type'] . '_' . $element['id']] = [ + 'id' => $element['id'], + 'type' => $element['type'], + ]; + + $el = null; + + if ($element['children']) { + $el = Element\Service::getElementById($element['type'], (int) $element['id']); + $baseClass = Element\Service::getBaseClassNameForElement($element['type']); + $listClass = '\\OpenDxp\\Model\\' . $baseClass . '\\Listing'; + $list = new $listClass(); + $list->setUnpublished(true); + if ($el instanceof AbstractObject) { + $list->setObjectTypes( + [DataObject::OBJECT_TYPE_VARIANT, + DataObject::OBJECT_TYPE_OBJECT, + DataObject::OBJECT_TYPE_FOLDER, ] + ); + } + $list->setCondition( + 'path LIKE ?', + [$list->escapeLike($el->getRealFullPath() . ($el->getRealFullPath() !== '/' ? '/' : '')) . '%'] + ); + $children = $list->load(); + + foreach ($children as $child) { + $childId = $child->getId(); + $elements[$element['type'] . '_' . $childId] = [ + 'id' => $childId, + 'type' => $element['type'], + ]; + + if (isset($element['relations']) && $element['relations']) { + $childDependencies = $child->getDependencies()->getRequires(); + foreach ($childDependencies as $cd) { + if ($cd['type'] === 'object' || $cd['type'] === 'document') { + $elements[$cd['type'] . '_' . $cd['id']] = $cd; + } + } + } + } + } + + if (isset($element['relations']) && $element['relations']) { + if (!$el instanceof Element\ElementInterface) { + $el = Element\Service::getElementById($element['type'], (int) $element['id']); + } + + $dependencies = $el->getDependencies()->getRequires(); + foreach ($dependencies as $dependency) { + if ($dependency['type'] === 'object' || $dependency['type'] === 'document') { + $elements[$dependency['type'] . '_' . $dependency['id']] = $dependency; + } + } + } + } + } + + $elements = array_values($elements); + + $elements = array_chunk($elements, $payload->elementsPerJob); + foreach ($elements as $chunk) { + $jobs[] = [[ + 'url' => $payload->jobUrl, + 'method' => 'POST', + 'params' => [ + 'id' => $exportId, + 'source' => $payload->source, + 'target' => $payload->target, + 'data' => json_encode($chunk, JSON_THROW_ON_ERROR), + ], + ]]; + } + + return new BuildContentExportJobsResult(jobs: $jobs, exportId: $exportId); + } +} diff --git a/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsPayload.php b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsPayload.php new file mode 100644 index 00000000..170681d4 --- /dev/null +++ b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsPayload.php @@ -0,0 +1,46 @@ +request->get('type'); + $jobUrl = $request->request->get('job_url', $request->getBaseUrl() . '/admin/translation/' . $type . '-export'); + $data = json_decode($request->request->get('data'), true); + + return new static( + data: $data && is_array($data) ? $data : [], + source: str_replace('_', '-', $request->request->get('source', '')), + target: str_replace('_', '-', $request->request->get('target', '')), + jobUrl: $jobUrl, + elementsPerJob: max(1, (int) $request->request->get('elements_per_job', 10)), + ); + } +} diff --git a/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsResult.php b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsResult.php new file mode 100644 index 00000000..5ab20af1 --- /dev/null +++ b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsResult.php @@ -0,0 +1,26 @@ +setDomain($payload->domain); + $list->cleanup(); + + Cache::clearTags(['translator', 'translate']); + } +} diff --git a/src/Handler/Translation/CleanupTranslations/CleanupTranslationsPayload.php b/src/Handler/Translation/CleanupTranslations/CleanupTranslationsPayload.php new file mode 100644 index 00000000..35bd4f5b --- /dev/null +++ b/src/Handler/Translation/CleanupTranslations/CleanupTranslationsPayload.php @@ -0,0 +1,35 @@ +request->get('domain', Translation::DOMAIN_DEFAULT), + ); + } +} diff --git a/src/Handler/Translation/CreateTranslation/CreateTranslationHandler.php b/src/Handler/Translation/CreateTranslation/CreateTranslationHandler.php new file mode 100644 index 00000000..849e514d --- /dev/null +++ b/src/Handler/Translation/CreateTranslation/CreateTranslationHandler.php @@ -0,0 +1,62 @@ +data; + $admin = $payload->domain === Translation::DOMAIN_ADMIN; + $validLanguages = $admin + ? Tool\Admin::getLanguages() + : $this->userContext->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); + if (Translation::getByKey($data['key'], $payload->domain)) { + throw new BadRequestHttpException('identifier_already_exists'); + } + + $t = new Translation(); + $t->setDomain($payload->domain); + $t->setKey($data['key']); + $t->setCreationDate(time()); + $t->setModificationDate(time()); + $t->setType($data['type'] ?? null); + + foreach ($validLanguages as $lang) { + $t->addTranslation($lang, ''); + } + + $t->save(); + + return new CreateTranslationResult( + key: $t->getKey(), + creationDate: $t->getCreationDate(), + modificationDate: $t->getModificationDate(), + type: $t->getType(), + translations: $t->getTranslations(), + ); + } +} diff --git a/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php b/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php new file mode 100644 index 00000000..8300d479 --- /dev/null +++ b/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php @@ -0,0 +1,29 @@ +data['key'], $payload->domain); + if ($t instanceof Translation) { + $t->delete(); + } + } +} diff --git a/src/Handler/Translation/ExportTranslations/ExportTranslationsHandler.php b/src/Handler/Translation/ExportTranslations/ExportTranslationsHandler.php new file mode 100644 index 00000000..27221417 --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsHandler.php @@ -0,0 +1,147 @@ +domain === Translation::DOMAIN_ADMIN; + $allowedLanguages = $admin + ? Tool\Admin::getLanguages() + : $this->userContext->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); + $translation = new Translation(); + $translation->setDomain($payload->domain); + $tableName = $translation->getDao()->getDatabaseTableName(); + + $list = new Translation\Listing(); + $list->setDomain($payload->domain); + + $joins = []; + + $list->setOrder('asc'); + $list->setOrderKey($tableName . '.key', false); + + $filterParameters = [ + 'filter' => $payload->filter, + 'searchString' => $payload->searchString, + ]; + + $conditions = $this->getGridFilterCondition($filterParameters, $tableName, false, $allowedLanguages); + if ($conditions !== []) { + $list->setCondition($conditions['condition'], $conditions['params']); + } + + $filters = $this->getGridFilterCondition($filterParameters, $tableName, true, $allowedLanguages); + + if ($filters) { + $joins = [...$joins, ...$filters['joins']]; + } + + $this->extendTranslationQuery($joins, $list, $tableName, $filters); + + try { + $list->load(); + } catch (SyntaxErrorException) { + throw new InvalidArgumentException('Check your arguments.'); + } + + $translations = []; + $translationObjects = $list->getTranslations(); + + if ($translationObjects === []) { + if ($admin) { + $t = new Translation(); + $t->setDomain(Translation::DOMAIN_ADMIN); + $languages = Tool\Admin::getLanguages(); + } else { + $t = new Translation(); + $languages = $allowedLanguages; + } + + foreach ($languages as $language) { + $t->addTranslation($language, ''); + } + + $translationObjects[] = $t; + } + + foreach ($translationObjects as $t) { + $row = $t->getTranslations(); + $row = Element\Service::escapeCsvRecord($row); + $translations[] = ['key' => $t->getKey(), 'creationDate' => $t->getCreationDate(), 'modificationDate' => $t->getModificationDate(), ...$row]; + } + + $columns = array_keys($translations[0]); + + if ($admin) { + $languages = Tool\Admin::getLanguages(); + } else { + $languages = $allowedLanguages; + } + + foreach ($languages as $l) { + if (!in_array($l, $columns)) { + $columns[] = $l; + } + } + + foreach ($columns as $key => $column) { + if (strtolower(trim($column)) !== 'key' && !in_array($column, $languages)) { + unset($columns[$key]); + } + } + $columns = array_values($columns); + + $headerRow = []; + foreach ($columns as $value) { + $headerRow[] = '"' . $value . '"'; + } + $csv = implode(';', $headerRow) . "\r\n"; + + foreach ($translations as $t) { + $tempRow = []; + foreach ($columns as $key) { + $value = $t[$key] ?? null; + if (is_string($value)) { + $value = Text::removeLineBreaks($value); + $value = str_replace('"', '"', $value); + $tempRow[$key] = '"' . $value . '"'; + } else { + $tempRow[$key] = $value; + } + } + $csv .= implode(';', $tempRow) . "\r\n"; + } + + return new ExportTranslationsResult(csv: $csv, domain: $payload->domain ?? ''); + } +} diff --git a/src/Handler/Translation/ExportTranslations/ExportTranslationsPayload.php b/src/Handler/Translation/ExportTranslations/ExportTranslationsPayload.php new file mode 100644 index 00000000..979badcd --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsPayload.php @@ -0,0 +1,38 @@ +query->get('domain'), + filter: $request->query->get('filter'), + searchString: $request->query->get('searchString'), + ); + } +} diff --git a/src/Handler/Translation/ExportTranslations/ExportTranslationsResult.php b/src/Handler/Translation/ExportTranslations/ExportTranslationsResult.php new file mode 100644 index 00000000..0fcfbd2d --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsResult.php @@ -0,0 +1,26 @@ + ['name' => $domain], + $translation->getDao()->getAvailableDomains(), + ); + + return new GetTranslationDomainsResult(domains: $domains); + } +} diff --git a/src/Handler/Translation/GetTranslationDomains/GetTranslationDomainsResult.php b/src/Handler/Translation/GetTranslationDomains/GetTranslationDomainsResult.php new file mode 100644 index 00000000..641d6605 --- /dev/null +++ b/src/Handler/Translation/GetTranslationDomains/GetTranslationDomainsResult.php @@ -0,0 +1,25 @@ +domain === Translation::DOMAIN_ADMIN; + $validLanguages = $admin + ? Tool\Admin::getLanguages() + : $this->userContext->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(); + $translation = new Translation(); + $translation->setDomain($payload->domain); + $tableName = $translation->getDao()->getDatabaseTableName(); + + $list = new Translation\Listing(); + $list->setDomain($payload->domain); + $list->setOrder('asc'); + $list->setOrderKey($tableName . '.key', false); + $list->setLanguages($validLanguages); + + $sortingSettings = QueryParams::extractSortingSettings($payload->requestParams); + + $joins = []; + + if ($orderKey = $sortingSettings['orderKey']) { + if (in_array(trim($orderKey, '_'), $validLanguages)) { + $orderKey = trim($orderKey, '_'); + $joins[] = [ + 'language' => $orderKey, + ]; + $list->setOrderKey($orderKey); + } elseif ($list->isValidOrderKey($sortingSettings['orderKey'])) { + $list->setOrderKey($tableName . '.' . $sortingSettings['orderKey'], false); + } + } + if ($sortingSettings['order']) { + $list->setOrder($sortingSettings['order']); + } + + $list->setLimit($payload->limit); + $list->setOffset($payload->offset); + + $filterParameters = [ + 'filter' => $payload->filter, + 'searchString' => $payload->searchString, + ]; + + $conditions = $this->getGridFilterCondition($filterParameters, $tableName, false, $validLanguages); + $filters = $this->getGridFilterCondition($filterParameters, $tableName, true, $validLanguages); + + if ($filters) { + $joins = [...$joins, ...$filters['joins']]; + } + + if ($conditions !== []) { + $list->setCondition($conditions['condition'], $conditions['params']); + } + + $this->extendTranslationQuery($joins, $list, $tableName, $filters); + + $translations = []; + foreach ($list->getTranslations() as $t) { + if ($payload->searchString && !strpos($payload->searchString, (string) $t->getKey()) && !$t = Translation::getByKey($t->getKey(), $payload->domain)) { + continue; + } + + $prefixed = []; + foreach ($t->getTranslations() as $lang => $trans) { + $prefixed['_' . $lang] = $trans; + } + + $translations[] = [ + ...$prefixed, + 'key' => $t->getKey(), + 'creationDate' => $t->getCreationDate(), + 'modificationDate' => $t->getModificationDate(), + 'type' => $t->getType(), + ]; + } + + return new GetTranslationsResult( + translations: $translations, + total: $list->getTotalCount(), + ); + } +} diff --git a/src/Handler/Translation/GetTranslations/GetTranslationsResult.php b/src/Handler/Translation/GetTranslations/GetTranslationsResult.php new file mode 100644 index 00000000..194657e4 --- /dev/null +++ b/src/Handler/Translation/GetTranslations/GetTranslationsResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser(); + + return new GetWebsiteTranslationLanguagesResult( + view: $user->getAllowedLanguagesForViewingWebsiteTranslations(), + edit: $user->getAllowedLanguagesForEditingWebsiteTranslations(), + ); + } +} diff --git a/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesResult.php b/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesResult.php new file mode 100644 index 00000000..cfc868bd --- /dev/null +++ b/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesResult.php @@ -0,0 +1,25 @@ +domain === Translation::DOMAIN_ADMIN; + $allowedLanguages = $admin + ? Tool\Admin::getLanguages() + : $this->userContext->getAdminUser()->getAllowedLanguagesForEditingWebsiteTranslations(); + $delta = Translation::importTranslationsFromFile( + $payload->tmpFile, + $payload->domain, + $payload->overwrite, + $allowedLanguages, + $payload->dialect + ); + + if (is_file($payload->tmpFile)) { + @unlink($payload->tmpFile); + } + + if ($payload->enrichDelta) { + $flagUrlTemplate = $this->router->generate('opendxp_admin_misc_getlanguageflag', ['language' => '{language}']); + $flagUrlTemplate = str_replace('%7Blanguage%7D', '{language}', $flagUrlTemplate); + $enrichedDelta = []; + foreach ($delta as $item) { + $lg = $item['lg']; + $currentLocale = $this->localeService->findLocale(); + $item['lgname'] = Locale::getDisplayLanguage($lg, $currentLocale); + $item['icon'] = str_replace('{language}', $lg, $flagUrlTemplate); + $item['current'] = $item['text']; + $enrichedDelta[] = $item; + } + + return new ImportTranslationsResult(delta: $enrichedDelta); + } + + return new ImportTranslationsResult(delta: []); + } +} diff --git a/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php b/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php new file mode 100644 index 00000000..09f150a9 --- /dev/null +++ b/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php @@ -0,0 +1,53 @@ +request->get('domain', Translation::DOMAIN_DEFAULT); + $merge = $request->query->get('merge'); + $dialect = $request->request->get('csvSettings'); + if ($dialect) { + $dialect = json_decode($dialect); + } + $session = Session::getSessionBag($request->getSession(), 'opendxp_importconfig'); + $tmpFile = $session->get('translation_import_file'); + + return new static( + domain: $domain, + tmpFile: $tmpFile, + overwrite: !$merge, + dialect: $dialect, + enrichDelta: (bool) $merge, + ); + } +} diff --git a/src/Handler/Translation/ImportTranslations/ImportTranslationsResult.php b/src/Handler/Translation/ImportTranslations/ImportTranslationsResult.php new file mode 100644 index 00000000..18ae4be7 --- /dev/null +++ b/src/Handler/Translation/ImportTranslations/ImportTranslationsResult.php @@ -0,0 +1,25 @@ +dataList as $data) { + $t = Translation::getByKey($data['key'], $payload->domain, true); + $newValue = htmlspecialchars_decode($data['current']); + $t->addTranslation($data['lg'], $newValue); + $t->setModificationDate(time()); + $t->save(); + } + } +} diff --git a/src/Handler/Translation/MergeTranslationItems/MergeTranslationItemsPayload.php b/src/Handler/Translation/MergeTranslationItems/MergeTranslationItemsPayload.php new file mode 100644 index 00000000..7f876ba4 --- /dev/null +++ b/src/Handler/Translation/MergeTranslationItems/MergeTranslationItemsPayload.php @@ -0,0 +1,37 @@ +request->get('data'), true), + domain: $request->request->get('domain', Translation::DOMAIN_DEFAULT), + ); + } +} diff --git a/src/Handler/Translation/TranslationPayload.php b/src/Handler/Translation/TranslationPayload.php new file mode 100644 index 00000000..dd9271aa --- /dev/null +++ b/src/Handler/Translation/TranslationPayload.php @@ -0,0 +1,59 @@ +request->getString('domain', Translation::DOMAIN_DEFAULT); + $hasData = $request->request->has('data'); + + if ($hasData) { + return new static( + domain: $domain, + hasData: true, + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } + + return new static( + domain: $domain, + hasData: false, + requestParams: [...$request->request->all(), ...$request->query->all()], + limit: $request->request->getInt('limit', 50), + offset: $request->request->getInt('start', 0), + filter: $request->request->has('filter') ? $request->request->getString('filter') : null, + searchString: $request->request->has('searchString') ? $request->request->getString('searchString') : null, + ); + } +} diff --git a/src/Handler/Translation/TranslationQueryTrait.php b/src/Handler/Translation/TranslationQueryTrait.php new file mode 100644 index 00000000..3d54b2f4 --- /dev/null +++ b/src/Handler/Translation/TranslationQueryTrait.php @@ -0,0 +1,175 @@ +onCreateQueryBuilder( + function (DoctrineQueryBuilder $select) use ($joins, $tableName, $filters): void { + $db = \OpenDxp\Db::get(); + + $alreadyJoined = []; + + foreach ($joins as $join) { + $fieldname = $join['language']; + + if (isset($alreadyJoined[$fieldname])) { + continue; + } + $alreadyJoined[$fieldname] = 1; + + $select->addSelect($fieldname . '.text AS ' . $fieldname); + $select->leftJoin( + $tableName, + $tableName, + $fieldname, + '(' + . $fieldname . '.key = ' . $tableName . '.key' + . ' and ' . $fieldname . '.language = ' . $db->quote($fieldname) + . ')' + ); + } + + $havings = $filters['conditions']; + if ($havings) { + $havings = implode(' AND ', $havings); + $select->having($havings); + } + } + ); + } + } + + protected function getGridFilterCondition(array $filterParameters, string $tableName, bool $languageMode, array $validLanguages): array + { + $placeHolderCount = 0; + $joins = []; + $conditions = []; + + $db = \OpenDxp\Db::get(); + $conditionFilters = []; + + $filterJson = $filterParameters['filter']; + if ($filterJson) { + $propertyField = 'property'; + $operatorField = 'operator'; + + $filters = json_decode($filterJson, true); + foreach ($filters as $filter) { + $operator = '='; + $field = null; + $value = null; + + $fieldname = $filter[$propertyField]; + if (in_array(ltrim($fieldname, '_'), $validLanguages)) { + $fieldname = ltrim($fieldname, '_'); + } + $fieldname = str_replace('--', '', $fieldname); + if (!$languageMode && in_array($fieldname, $validLanguages)) { + continue; + } + if ($languageMode && !in_array($fieldname, $validLanguages)) { + continue; + } + + if (!$languageMode) { + $fieldname = $tableName . '.' . $fieldname; + } + + if (!empty($filter['value'])) { + if ($filter['type'] === 'string') { + $operator = 'LIKE'; + $field = $fieldname; + $value = '%' . $filter['value'] . '%'; + } elseif ($filter['type'] === 'date' || + (in_array($fieldname, ['modificationDate', 'creationDate']))) { + if ($filter[$operatorField] === 'lt') { + $operator = '<'; + } elseif ($filter[$operatorField] === 'gt') { + $operator = '>'; + } elseif ($filter[$operatorField] === 'eq') { + $operator = '='; + $fieldname = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))"; + } + $filter['value'] = strtotime($filter['value']); + $field = $fieldname; + $value = $filter['value']; + } + } + + if ($field && $value) { + $condition = $db->quoteIdentifier($field) . ' ' . $operator . ' ' . $db->quote($value); + + if ($languageMode) { + $conditions[$fieldname] = $condition; + $joins[] = [ + 'language' => $fieldname, + ]; + } else { + $placeHolderName = self::FILTER_PLACEHOLDER_NAME . $placeHolderCount; + $placeHolderCount++; + $conditionFilters[] = [ + 'condition' => $field . ' ' . $operator . ' :' . $placeHolderName, + 'field' => $placeHolderName, + 'value' => $value, + ]; + } + } + } + } + + if (!empty($filterParameters['searchString'])) { + $conditionFilters[] = [ + 'condition' => '(lower(' . $tableName . '.key) LIKE :filterTerm OR lower(' . $tableName . '.text) LIKE :filterTerm)', + 'field' => 'filterTerm', + 'value' => '%' . mb_strtolower($filterParameters['searchString']) . '%', + ]; + } + + if ($languageMode) { + return [ + 'joins' => $joins, + 'conditions' => $conditions, + ]; + } + + if ($conditionFilters !== []) { + $conditions = []; + $params = []; + foreach ($conditionFilters as $conditionFilter) { + $conditions[] = $conditionFilter['condition']; + $params[$conditionFilter['field']] = $conditionFilter['value']; + } + + $conditionFilters = [ + 'condition' => implode(' AND ', $conditions), + 'params' => $params, + ]; + } + + return $conditionFilters; + } +} diff --git a/src/Handler/Translation/UpdateTranslation/UpdateTranslationHandler.php b/src/Handler/Translation/UpdateTranslation/UpdateTranslationHandler.php new file mode 100644 index 00000000..e1195aa6 --- /dev/null +++ b/src/Handler/Translation/UpdateTranslation/UpdateTranslationHandler.php @@ -0,0 +1,61 @@ +data; + + $t = Translation::getByKey($data['key'], $payload->domain); + if (!$t instanceof Translation) { + throw new NotFoundHttpException(sprintf('Translation with key "%s" not found.', $data['key'])); + } + + foreach ($data as $key => $value) { + $key = preg_replace('/^_/', '', $key, 1); + if (!in_array($key, ['key', 'type'])) { + $t->addTranslation($key, $value); + } + } + + if ($data['key']) { + $t->setKey($data['key']); + } + + if ($data['type']) { + $t->setType($data['type']); + } + + $t->setModificationDate(time()); + $t->save(); + + return new UpdateTranslationResult( + key: $t->getKey(), + creationDate: $t->getCreationDate(), + modificationDate: $t->getModificationDate(), + type: $t->getType(), + translations: $t->getTranslations(), + ); + } +} diff --git a/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php b/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php new file mode 100644 index 00000000..45133aa8 --- /dev/null +++ b/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php @@ -0,0 +1,29 @@ +file->getPathname()); + + $filename = uniqid('import_translations-', false); + $importFile = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . $filename; + $this->filesystem->dumpFile($importFile, $tmpData); + + $dialect = AdminTool::determineCsvDialect($importFile); + + if (!empty($dialect->lineterminator) && empty(preg_match('/[a-f0-9]{2}/i', $dialect->lineterminator))) { + $dialect->lineterminator = bin2hex($dialect->lineterminator); + } + + return new UploadTranslationImportFileResult( + importFile: $importFile, + dialect: $dialect, + ); + } +} diff --git a/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFilePayload.php b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFilePayload.php new file mode 100644 index 00000000..b57fd86e --- /dev/null +++ b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFilePayload.php @@ -0,0 +1,35 @@ +files->get('Filedata'), + ); + } +} diff --git a/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileResult.php b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileResult.php new file mode 100644 index 00000000..63b3715f --- /dev/null +++ b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser()?->isAdmin() ?? false; + $className = User\Service::getClassNameForType($payload->type); + $user = $className::create([ + 'parentId' => $payload->parentId, + 'name' => $payload->name, + 'password' => '', + 'active' => $payload->active, + ]); + + if ($payload->referenceId !== null) { + $rObject = $className::getById($payload->referenceId); + if ($rObject && ($payload->type === 'user' || $payload->type === 'role')) { + $user->setParentId($rObject->getParentId()); + if ($rObject->getClasses()) { + $user->setClasses(implode(',', $rObject->getClasses())); + } + if ($rObject->getDocTypes()) { + $user->setDocTypes(implode(',', $rObject->getDocTypes())); + } + $keys = ['asset', 'document', 'object']; + foreach ($keys as $key) { + $getter = 'getWorkspaces' . ucfirst($key); + $setter = 'setWorkspaces' . ucfirst($key); + $workspaces = $rObject->$getter(); + $clonedWorkspaces = []; + if (is_array($workspaces)) { + /** @var User\Workspace\AbstractWorkspace $workspace */ + foreach ($workspaces as $workspace) { + $vars = $workspace->getObjectVars(); + if ($key === 'object') { + $workspaceClass = \OpenDxp\Model\User\Workspace\DataObject::class; + } else { + $workspaceClass = '\\OpenDxp\\Model\\User\\Workspace\\' . ucfirst($key); + } + $newWorkspace = new $workspaceClass(); + foreach ($vars as $varKey => $varValue) { + $newWorkspace->setObjectVar($varKey, $varValue); + } + $newWorkspace->setUserId($user->getId()); + $clonedWorkspaces[] = $newWorkspace; + } + } + + $user->$setter($clonedWorkspaces); + } + $user->setPerspectives($rObject->getPerspectives()); + $user->setPermissions($rObject->getPermissions()); + if ($payload->type === 'user') { + $user->setAdmin(false); + if ($currentUserIsAdmin) { + $user->setAdmin($rObject->getAdmin()); + } + $user->setActive($rObject->getActive()); + $user->setRoles($rObject->getRoles()); + $user->setWelcomeScreen($rObject->getWelcomescreen()); + $user->setMemorizeTabs($rObject->getMemorizeTabs()); + $user->setCloseWarning($rObject->getCloseWarning()); + } + $user->setWebsiteTranslationLanguagesView($rObject->getWebsiteTranslationLanguagesView()); + $user->setWebsiteTranslationLanguagesEdit($rObject->getWebsiteTranslationLanguagesEdit()); + $user->save(); + } + } + + return new AddUserResult(id: $user->getId()); + } +} diff --git a/src/Handler/User/AddUser/AddUserPayload.php b/src/Handler/User/AddUser/AddUserPayload.php new file mode 100644 index 00000000..eee78a86 --- /dev/null +++ b/src/Handler/User/AddUser/AddUserPayload.php @@ -0,0 +1,46 @@ +request->get('name', ''); + $referenceId = $request->request->has('rid') ? (int) $request->request->get('rid') : null; + + return new static( + type: $request->request->get('type'), + parentId: $request->request->getInt('parentId'), + name: trim((string) $name), + active: $request->request->getBoolean('active'), + referenceId: $referenceId, + ); + } +} diff --git a/src/Handler/User/AddUser/AddUserResult.php b/src/Handler/User/AddUser/AddUserResult.php new file mode 100644 index 00000000..f0f4b5cf --- /dev/null +++ b/src/Handler/User/AddUser/AddUserResult.php @@ -0,0 +1,25 @@ +userContext->getAdminUser(); + $currentUserId = (int) $adminUser?->getId(); + + $user = User\AbstractUser::getById($payload->id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + if (($user instanceof User\Folder && !$adminUser?->isAdmin()) + || ($user instanceof User && $user->isAdmin() && !$adminUser?->isAdmin()) + ) { + throw new AccessDeniedHttpException('You are not allowed to delete this user'); + } + + if ($user instanceof User\Role\Folder) { + $list = [$user]; + $this->populateChildNodes($user, $list, true, $currentUserId); + $listCount = count($list); + for ($i = $listCount - 1; $i >= 0; $i--) { + $list[$i]->delete(); + } + } elseif ($user->getId()) { + $user->delete(); + } + } + + /** + * @throws Exception + */ + private function populateChildNodes(User\AbstractUser $node, array &$currentList, bool $roleMode, int $currentUserId): void + { + $list = $roleMode ? new User\Role\Listing() : new User\Listing(); + $list->setCondition('parentId = ?', $node->getId()); + $list->setOrder('ASC'); + $list->setOrderKey('name'); + $list->load(); + + $childList = $roleMode ? $list->getRoles() : $list->getUsers(); + + foreach ($childList as $child) { + if ($child->getId() === $currentUserId) { + throw new Exception('Cannot delete current user'); + } + if ($child->getId() && $currentUserId && $child->getName() !== 'system') { + $currentList[] = $child; + $this->populateChildNodes($child, $currentList, $roleMode, $currentUserId); + } + } + } +} diff --git a/src/Handler/User/DeleteUser/DeleteUserPayload.php b/src/Handler/User/DeleteUser/DeleteUserPayload.php new file mode 100644 index 00000000..88fb6e64 --- /dev/null +++ b/src/Handler/User/DeleteUser/DeleteUserPayload.php @@ -0,0 +1,35 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/User/DeleteUserImage/DeleteUserImageHandler.php b/src/Handler/User/DeleteUserImage/DeleteUserImageHandler.php new file mode 100644 index 00000000..4402c177 --- /dev/null +++ b/src/Handler/User/DeleteUserImage/DeleteUserImageHandler.php @@ -0,0 +1,50 @@ +userContext->getAdminUser(); + $targetUserId = $payload->targetUserId ?? (int) $adminUser?->getId(); + + $userObj = User::getById($targetUserId); + if (!$userObj) { + throw new NotFoundHttpException('User not found'); + } + + if (!$adminUser?->isAdmin()) { + if ($userObj->isAdmin()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify admin users'); + } + if ($adminUser?->getId() !== $userObj->getId()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify users other than themselves'); + } + } + + $userObj->setImage(null); + } +} diff --git a/src/Handler/User/DeleteUserImage/DeleteUserImagePayload.php b/src/Handler/User/DeleteUserImage/DeleteUserImagePayload.php new file mode 100644 index 00000000..c17c1206 --- /dev/null +++ b/src/Handler/User/DeleteUserImage/DeleteUserImagePayload.php @@ -0,0 +1,35 @@ +query->has('id') ? $request->query->getInt('id') : null, + ); + } +} diff --git a/src/Handler/User/Disable2Fa/Disable2FaHandler.php b/src/Handler/User/Disable2Fa/Disable2FaHandler.php new file mode 100644 index 00000000..d4eb72aa --- /dev/null +++ b/src/Handler/User/Disable2Fa/Disable2FaHandler.php @@ -0,0 +1,42 @@ +userContext->getAdminUser(); + if (!$user instanceof User) { + throw new BadRequestHttpException('No user found'); + } + + if ($user->getTwoFactorAuthentication('required')) { + throw new BadRequestHttpException('Two-factor authentication is required and cannot be disabled.'); + } + + $user->setTwoFactorAuthentication([]); + $user->save(); + } +} diff --git a/src/Handler/User/Disable2Fa/Disable2FaPayload.php b/src/Handler/User/Disable2Fa/Disable2FaPayload.php new file mode 100644 index 00000000..3512c66e --- /dev/null +++ b/src/Handler/User/Disable2Fa/Disable2FaPayload.php @@ -0,0 +1,31 @@ +userContext->getAdminUser(); + $list = new User\Permission\Definition\Listing(); + $definitions = $list->load(); + + foreach ($definitions as $definition) { + $user->setPermission($definition->getKey(), $user->isAllowed($definition->getKey())); + } + + $userData = $user->getObjectVars(); + $contentLanguages = Tool\Admin::reorderWebsiteLanguages($user, Tool::getValidLanguages()); + $userData['contentLanguages'] = $contentLanguages; + $userData['keyBindings'] = UserHelper::getDefaultKeyBindings($user); + + unset($userData['password'], $userData['passwordRecoveryToken']); + $userData['twoFactorAuthentication'] = $user->getTwoFactorAuthentication(); + unset($userData['twoFactorAuthentication']['secret']); + $userData['twoFactorAuthentication']['isActive'] = $user->getTwoFactorAuthentication('enabled') && $user->getTwoFactorAuthentication('secret'); + $userData['hasImage'] = $user->hasImage(); + $userData['isPasswordReset'] = $payload->isPasswordReset; + $userData['validLocales'] = Tool::getSupportedJSLocales(); + + return new GetCurrentUserResult(userData: $userData); + } +} diff --git a/src/Handler/User/GetCurrentUser/GetCurrentUserPayload.php b/src/Handler/User/GetCurrentUser/GetCurrentUserPayload.php new file mode 100644 index 00000000..e9f7aa83 --- /dev/null +++ b/src/Handler/User/GetCurrentUser/GetCurrentUserPayload.php @@ -0,0 +1,33 @@ +getSession(), fn (AttributeBagInterface $adminSession) => $adminSession->get('password_reset')), + ); + } +} diff --git a/src/Handler/User/GetCurrentUser/GetCurrentUserResult.php b/src/Handler/User/GetCurrentUser/GetCurrentUserResult.php new file mode 100644 index 00000000..655f22aa --- /dev/null +++ b/src/Handler/User/GetCurrentUser/GetCurrentUserResult.php @@ -0,0 +1,25 @@ +id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + return new GetMinimalUserResult( + id: $user->getId(), + admin: $user->isAdmin(), + active: $user->isActive(), + permissionInfo: [ + 'assets' => $user->isAllowed('assets'), + 'documents' => $user->isAllowed('documents'), + 'objects' => $user->isAllowed('objects'), + ], + ); + } +} diff --git a/src/Handler/User/GetMinimalUser/GetMinimalUserPayload.php b/src/Handler/User/GetMinimalUser/GetMinimalUserPayload.php new file mode 100644 index 00000000..6d059025 --- /dev/null +++ b/src/Handler/User/GetMinimalUser/GetMinimalUserPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/User/GetMinimalUser/GetMinimalUserResult.php b/src/Handler/User/GetMinimalUser/GetMinimalUserResult.php new file mode 100644 index 00000000..8ca6cef9 --- /dev/null +++ b/src/Handler/User/GetMinimalUser/GetMinimalUserResult.php @@ -0,0 +1,28 @@ +id); + if (!$role) { + throw new NotFoundHttpException('Role not found'); + } + + // workspaces + $types = ['asset', 'document', 'object']; + foreach ($types as $type) { + /** @var Workspace\Document[]|Workspace\Asset[]|Workspace\DataObject[] $workspaces */ + $workspaces = $role->{'getWorkspaces' . ucfirst($type)}(); + foreach ($workspaces as $wKey => $workspace) { + $el = Element\Service::getElementById($type, $workspace->getCid()); + if ($el) { + $workspaceVars = $workspace->getObjectVars(); + $workspaceVars['path'] = $el->getRealFullPath(); + $workspaces[$wKey] = $workspaceVars; + } + } + $role->{'setWorkspaces' . ucfirst($type)}($workspaces); + } + + $replaceFn = static fn ($value) => $value->getObjectVars(); + + // get available permissions + $availableUserPermissionsList = new User\Permission\Definition\Listing(); + $availableUserPermissionsList->setOrderKey('category'); + $availableUserPermissions = $availableUserPermissionsList->load(); + $availableUserPermissions = array_map($replaceFn, $availableUserPermissions); + + $availablePerspectives = Config::getAvailablePerspectives(null); + + return new GetRoleResult( + role: $role->getObjectVars(), + permissions: $role->generatePermissionList(), + classes: $role->getClasses(), + docTypes: $role->getDocTypes(), + availablePermissions: $availableUserPermissions, + availablePerspectives: $availablePerspectives, + validLanguages: Tool::getValidLanguages(), + ); + } +} diff --git a/src/Handler/User/GetRole/GetRolePayload.php b/src/Handler/User/GetRole/GetRolePayload.php new file mode 100644 index 00000000..93fcc124 --- /dev/null +++ b/src/Handler/User/GetRole/GetRolePayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/User/GetRole/GetRoleResult.php b/src/Handler/User/GetRole/GetRoleResult.php new file mode 100644 index 00000000..54c94589 --- /dev/null +++ b/src/Handler/User/GetRole/GetRoleResult.php @@ -0,0 +1,31 @@ +setCondition('parentId = ?', $payload->node); + $list->load(); + + $roles = []; + foreach ($list->getItems() as $role) { + $roles[] = $this->buildRoleTreeNodeConfig($role); + } + + return $roles; + } + + private function buildRoleTreeNodeConfig(User\Role|User\Role\Folder $role): array + { + $tmpUser = [ + 'id' => $role->getId(), + 'text' => $role->getName(), + 'elementType' => 'role', + 'qtipCfg' => [ + 'title' => 'ID: ' . $role->getId(), + ], + ]; + + if ($role instanceof User\Role\Folder) { + $tmpUser['leaf'] = false; + $tmpUser['iconCls'] = 'opendxp_icon_folder'; + $tmpUser['expanded'] = true; + $tmpUser['allowChildren'] = true; + + if ($role->hasChildren()) { + $tmpUser['expanded'] = false; + } else { + $tmpUser['loaded'] = true; + } + } else { + $tmpUser['leaf'] = true; + $tmpUser['iconCls'] = 'opendxp_icon_roles'; + $tmpUser['allowChildren'] = false; + } + + return $tmpUser; + } +} diff --git a/src/Handler/User/GetRoleTreeChildren/GetRoleTreeChildrenPayload.php b/src/Handler/User/GetRoleTreeChildren/GetRoleTreeChildrenPayload.php new file mode 100644 index 00000000..3127b7e3 --- /dev/null +++ b/src/Handler/User/GetRoleTreeChildren/GetRoleTreeChildrenPayload.php @@ -0,0 +1,30 @@ +query->getInt('node')); + } +} diff --git a/src/Handler/User/GetRoles/GetRolesHandler.php b/src/Handler/User/GetRoles/GetRolesHandler.php new file mode 100644 index 00000000..fa42595b --- /dev/null +++ b/src/Handler/User/GetRoles/GetRolesHandler.php @@ -0,0 +1,42 @@ +setCondition('`type` = "role"'); + $list->load(); + + $roles = []; + foreach ($list->getRoles() as $role) { + if ($payload->permission === null || in_array($payload->permission, $role->getPermissions())) { + $roles[] = [ + 'id' => $role->getId(), + 'label' => $role->getName(), + ]; + } + } + + return $roles; + } +} diff --git a/src/Handler/User/GetRoles/GetRolesPayload.php b/src/Handler/User/GetRoles/GetRolesPayload.php new file mode 100644 index 00000000..7120087e --- /dev/null +++ b/src/Handler/User/GetRoles/GetRolesPayload.php @@ -0,0 +1,30 @@ +query->get('permission')); + } +} diff --git a/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkHandler.php b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkHandler.php new file mode 100644 index 00000000..63226fb4 --- /dev/null +++ b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkHandler.php @@ -0,0 +1,57 @@ +id); + if (!$user) { + throw new NotFoundHttpException($this->translator->trans('login_token_invalid_user_error', [], 'admin')); + } + + $adminUser = $this->userContext->getAdminUser(); + if ($user->isAdmin() && !$adminUser?->isAdmin()) { + throw new AccessDeniedHttpException($this->translator->trans('login_token_as_admin_non_admin_user_error', [], 'admin')); + } + + if (empty($user->getPassword())) { + throw new AccessDeniedHttpException($this->translator->trans('login_token_no_password_error', [], 'admin')); + } + + $token = Tool\Authentication::generateTokenByUser($user); + $link = $this->loginUrlGenerator->generate(['token' => $token]); + + return new GetTokenLoginLinkResult(link: $link); + } +} diff --git a/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkPayload.php b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkPayload.php new file mode 100644 index 00000000..f13fe924 --- /dev/null +++ b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkResult.php b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkResult.php new file mode 100644 index 00000000..6f040b7d --- /dev/null +++ b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkResult.php @@ -0,0 +1,25 @@ +id < 1) { + throw new NotFoundHttpException('User not found'); + } + + $user = User::getById($payload->id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + $adminUser = $this->userContext->getAdminUser(); + if ($user->isAdmin() && !$adminUser?->isAdmin()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify admin users'); + } + // workspaces + $types = ['asset', 'document', 'object']; + foreach ($types as $type) { + /** @var Workspace\Document[]|Workspace\Asset[]|Workspace\DataObject[] $workspaces */ + $workspaces = $user->{'getWorkspaces' . ucfirst($type)}(); + foreach ($workspaces as $wKey => $workspace) { + $el = Element\Service::getElementById($type, $workspace->getCid()); + if ($el) { + $workspaceVars = $workspace->getObjectVars(); + $workspaceVars['path'] = $el->getRealFullPath(); + $workspaces[$wKey] = $workspaceVars; + } + } + $user->{'setWorkspaces' . ucfirst($type)}($workspaces); + } + + // object <=> user dependencies + $userObjects = DataObject\Service::getObjectsReferencingUser($user->getId()); + $userObjectData = []; + $hasHidden = false; + + foreach ($userObjects as $o) { + if ($o->isAllowed('list')) { + $userObjectData[] = [ + 'path' => $o->getRealFullPath(), + 'id' => $o->getId(), + 'subtype' => $o->getClass()->getName(), + ]; + } else { + $hasHidden = true; + } + } + + // get available permissions + $availableUserPermissionsList = new User\Permission\Definition\Listing(); + $availableUserPermissionsList->setOrderKey('category'); + $availableUserPermissions = $availableUserPermissionsList->load(); + + $availableUserPermissionsData = []; + foreach ($availableUserPermissions as $availableUserPermission) { + $availableUserPermissionsData[] = $availableUserPermission->getObjectVars(); + } + + // get available roles + $list = new User\Role\Listing(); + $list->setCondition('`type` = ?', ['role']); + $list->load(); + + $roles = []; + foreach ($list->getItems() as $role) { + $roles[] = [$role->getId(), $role->getName()]; + } + + // unset confidential information + $userData = $user->getObjectVars(); + $userData['roles'] = $user->getRoles(); + $userData['docTypes'] = $user->getDocTypes(); + $contentLanguages = Tool\Admin::reorderWebsiteLanguages($user, Tool::getValidLanguages()); + $userData['contentLanguages'] = $contentLanguages; + $userData['twoFactorAuthentication']['isActive'] = ($user->getTwoFactorAuthentication('enabled') || $user->getTwoFactorAuthentication('secret')); + unset($userData['password'], $userData['passwordRecoveryToken'], $userData['twoFactorAuthentication']['secret']); + $userData['hasImage'] = $user->hasImage(); + + $availablePerspectives = Config::getAvailablePerspectives(null); + + return new GetUserResult( + userData: $userData, + roles: $roles, + permissions: $user->generatePermissionList(), + availablePermissions: $availableUserPermissionsData, + availablePerspectives: $availablePerspectives, + validLanguages: Tool::getValidLanguages(), + validLocales: Tool::getSupportedJSLocales(), + objectDependencies: [ + 'hasHidden' => $hasHidden, + 'dependencies' => $userObjectData, + ], + ); + } +} diff --git a/src/Handler/User/GetUser/GetUserPayload.php b/src/Handler/User/GetUser/GetUserPayload.php new file mode 100644 index 00000000..1cc8e0cc --- /dev/null +++ b/src/Handler/User/GetUser/GetUserPayload.php @@ -0,0 +1,35 @@ +query->getInt('id'), + ); + } +} diff --git a/src/Handler/User/GetUser/GetUserResult.php b/src/Handler/User/GetUser/GetUserResult.php new file mode 100644 index 00000000..4903aa06 --- /dev/null +++ b/src/Handler/User/GetUser/GetUserResult.php @@ -0,0 +1,32 @@ +targetUserId ?? (int) $this->userContext->getAdminUser()?->getId(); + + $userObj = User::getById($targetUserId); + + if (!$userObj) { + throw new NotFoundHttpException('User not found'); + } + + return new GetUserImageResult(image: $userObj->getImage()); + } +} diff --git a/src/Handler/User/GetUserImage/GetUserImagePayload.php b/src/Handler/User/GetUserImage/GetUserImagePayload.php new file mode 100644 index 00000000..1a482473 --- /dev/null +++ b/src/Handler/User/GetUserImage/GetUserImagePayload.php @@ -0,0 +1,35 @@ +query->has('id') ? $request->query->getInt('id') : null, + ); + } +} diff --git a/src/Handler/User/GetUserImage/GetUserImageResult.php b/src/Handler/User/GetUserImage/GetUserImageResult.php new file mode 100644 index 00000000..d40ccf25 --- /dev/null +++ b/src/Handler/User/GetUserImage/GetUserImageResult.php @@ -0,0 +1,27 @@ +setCondition('parentId = ?', $payload->node); + $list->setOrder('ASC'); + $list->setOrderKey('name'); + $list->load(); + + $users = []; + foreach ($list->getUsers() as $user) { + if ($user->getId() && $user->getName() !== 'system') { + $users[] = $this->buildTreeNodeConfig($user); + } + } + + return $users; + } + + private function buildTreeNodeConfig(User|User\Folder $user): array + { + $tmpUser = [ + 'id' => $user->getId(), + 'text' => $user->getName(), + 'elementType' => 'user', + 'type' => $user->getType(), + 'qtipCfg' => [ + 'title' => 'ID: ' . $user->getId(), + ], + ]; + + if ($user instanceof User\Folder) { + $tmpUser['leaf'] = false; + $tmpUser['iconCls'] = 'opendxp_icon_folder'; + $tmpUser['expanded'] = true; + $tmpUser['allowChildren'] = true; + + if ($user->hasChildren()) { + $tmpUser['expanded'] = false; + } else { + $tmpUser['loaded'] = true; + } + } else { + $tmpUser['leaf'] = true; + $tmpUser['iconCls'] = 'opendxp_icon_user'; + if (!$user->getActive()) { + $tmpUser['cls'] = ' opendxp_unpublished'; + } + $tmpUser['allowChildren'] = false; + $tmpUser['admin'] = $user->isAdmin(); + } + + return $tmpUser; + } +} diff --git a/src/Handler/User/GetUserTreeChildren/GetUserTreeChildrenPayload.php b/src/Handler/User/GetUserTreeChildren/GetUserTreeChildrenPayload.php new file mode 100644 index 00000000..202a7dc4 --- /dev/null +++ b/src/Handler/User/GetUserTreeChildren/GetUserTreeChildrenPayload.php @@ -0,0 +1,35 @@ +query->getInt('node'), + ); + } +} diff --git a/src/Handler/User/GetUsers/GetUsersHandler.php b/src/Handler/User/GetUsers/GetUsersHandler.php new file mode 100644 index 00000000..fbdd4c63 --- /dev/null +++ b/src/Handler/User/GetUsers/GetUsersHandler.php @@ -0,0 +1,53 @@ +userContext->getAdminUser()?->getId(); + $list = new User\Listing(); + + $conditions = ['type = "user"']; + + if (!$payload->includeCurrentUser) { + $conditions[] = 'id != ' . $currentUserId; + } + + $list->setCondition(implode(' AND ', $conditions)); + $list->load(); + + $users = []; + foreach ($list->getUsers() as $user) { + if (!$payload->permission || $user->isAllowed($payload->permission)) { + $users[] = [ + 'id' => $user->getId(), + 'label' => $user->getUsername(), + ]; + } + } + + return $users; + } +} diff --git a/src/Handler/User/GetUsers/GetUsersPayload.php b/src/Handler/User/GetUsers/GetUsersPayload.php new file mode 100644 index 00000000..24b3d558 --- /dev/null +++ b/src/Handler/User/GetUsers/GetUsersPayload.php @@ -0,0 +1,39 @@ +query->has('include_current_user') + ? $request->query->get('include_current_user') === '1' + : false, + permission: $request->query->get('permission'), + ); + } +} diff --git a/src/Handler/User/Reset2FaSecret/Reset2FaSecretHandler.php b/src/Handler/User/Reset2FaSecret/Reset2FaSecretHandler.php new file mode 100644 index 00000000..af03b01d --- /dev/null +++ b/src/Handler/User/Reset2FaSecret/Reset2FaSecretHandler.php @@ -0,0 +1,36 @@ +id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + $user->setTwoFactorAuthentication('enabled', false); + $user->setTwoFactorAuthentication('secret', ''); + $user->save(); + } +} diff --git a/src/Handler/User/Reset2FaSecret/Reset2FaSecretPayload.php b/src/Handler/User/Reset2FaSecret/Reset2FaSecretPayload.php new file mode 100644 index 00000000..7f047f9d --- /dev/null +++ b/src/Handler/User/Reset2FaSecret/Reset2FaSecretPayload.php @@ -0,0 +1,35 @@ +request->getInt('id'), + ); + } +} diff --git a/src/Handler/User/ResetMy2FaSecret/ResetMy2FaSecretHandler.php b/src/Handler/User/ResetMy2FaSecret/ResetMy2FaSecretHandler.php new file mode 100644 index 00000000..5d4b26d9 --- /dev/null +++ b/src/Handler/User/ResetMy2FaSecret/ResetMy2FaSecretHandler.php @@ -0,0 +1,34 @@ +userContext->getAdminUser(); + $user->setTwoFactorAuthentication('required', true); + $user->setTwoFactorAuthentication('enabled', false); + $user->setTwoFactorAuthentication('secret', ''); + $user->save(); + } +} diff --git a/src/Handler/User/SearchUsers/SearchUsersHandler.php b/src/Handler/User/SearchUsers/SearchUsersHandler.php new file mode 100644 index 00000000..d1da687f --- /dev/null +++ b/src/Handler/User/SearchUsers/SearchUsersHandler.php @@ -0,0 +1,49 @@ +query . '%'; + + $list = new User\Listing(); + $list->setCondition('name LIKE ? OR firstname LIKE ? OR lastname LIKE ? OR email LIKE ? OR id = ?', [$q, $q, $q, $q, $payload->query]); + $list->setOrder('ASC'); + $list->setOrderKey('name'); + + $users = []; + foreach ($list->getUsers() as $user) { + /** @phpstan-ignore instanceof.alwaysTrue */ + if ($user instanceof User && $user->getName() !== 'system') { + $users[] = [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'email' => $user->getEmail(), + 'firstname' => $user->getFirstname(), + 'lastname' => $user->getLastname(), + ]; + } + } + + return $users; + } +} diff --git a/src/Handler/User/SearchUsers/SearchUsersPayload.php b/src/Handler/User/SearchUsers/SearchUsersPayload.php new file mode 100644 index 00000000..9e13e361 --- /dev/null +++ b/src/Handler/User/SearchUsers/SearchUsersPayload.php @@ -0,0 +1,35 @@ +query->get('query'), + ); + } +} diff --git a/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php b/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php new file mode 100644 index 00000000..9a4e79b5 --- /dev/null +++ b/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php @@ -0,0 +1,97 @@ +username) { + return new SendInvitationLinkResult(success: false, message: $message); + } + + $user = User::getByName($payload->username); + if (!$user instanceof User) { + return new SendInvitationLinkResult(success: false, message: 'User unknown
'); + } + + if (!$user->getActive()) { + $message .= 'User is not active
'; + } + + if (!$user->getEmail()) { + $message .= 'User has no email address
'; + } + + if (empty($message)) { + $domain = $this->generalHostResolver->resolve(['source' => $this->requestStack->getCurrentRequest()]) ?? ''; + + if (!$domain) { + return new SendInvitationLinkResult(success: false, message: 'No main domain set in system settings, unable to generate login invitation link'); + } + + if (!$user->getPassword()) { + $user->setPassword(bin2hex(random_bytes(16))); + $user->save(); + } + + $token = Tool\Authentication::generateTokenByUser($user); + + $context = $this->router->getContext(); + $context->setHost($domain); + + $loginUrl = $this->loginUrlGenerator->generate(['token' => $token, 'reset' => true]); + + try { + $mail = Tool::getMail([$user->getEmail()], 'OpenDXP login invitation for ' . Tool::getHostname()); + $mail->setIgnoreDebugMode(true); + $mail->text("Login to OpenDXP and change your password using the following link. This temporary login link will expire in 24 hours: \r\n\r\n" . $loginUrl); + $mail->send(); + + $success = true; + $message = sprintf($this->translator->trans('invitation_link_sent', [], 'admin_ext'), $user->getEmail()); + } catch (\Symfony\Component\HttpKernel\Exception\BadRequestHttpException $e) { + $message .= $e->getMessage() . '
'; + } catch (Exception) { + $message .= 'could not send email'; + } + } + + return new SendInvitationLinkResult(success: $success, message: $message); + } +} diff --git a/src/Handler/User/SendInvitationLink/SendInvitationLinkPayload.php b/src/Handler/User/SendInvitationLink/SendInvitationLinkPayload.php new file mode 100644 index 00000000..86fea4e2 --- /dev/null +++ b/src/Handler/User/SendInvitationLink/SendInvitationLinkPayload.php @@ -0,0 +1,35 @@ +request->get('username', ''), + ); + } +} diff --git a/src/Handler/User/SendInvitationLink/SendInvitationLinkResult.php b/src/Handler/User/SendInvitationLink/SendInvitationLinkResult.php new file mode 100644 index 00000000..17c2c7fa --- /dev/null +++ b/src/Handler/User/SendInvitationLink/SendInvitationLinkResult.php @@ -0,0 +1,26 @@ +userContext->getAdminUser(); + if ($user === null || $user->getId() !== $payload->requestedUserId) { + throw new BadRequestHttpException('User ID mismatch'); + } + $values = $payload->values; + unset($values['name'], $values['id'], $values['admin'], $values['permissions'], $values['roles'], $values['active']); + + if (!empty($values['new_password'])) { + $oldPasswordCheck = false; + + if ($payload->isPasswordReset) { + $oldPasswordCheck = true; + } elseif (!empty($values['old_password'])) { + $errors = $this->validator->validate($values['old_password'], [new UserPassword()]); + + if (count($errors) === 0) { + $oldPasswordCheck = true; + } + } + + if (strlen($values['new_password']) < 10) { + throw new Exception('Passwords have to be at least 10 characters long'); + } + + if ($oldPasswordCheck && $values['new_password'] == $values['retype_password']) { + if (Tool\Authentication::verifyPassword($user, $values['new_password'])) { + throw new Exception('The new password cannot be the same as the old one'); + } + + $values['password'] = Tool\Authentication::getPasswordHash($user->getName(), $values['new_password']); + } else { + if (!$oldPasswordCheck) { + throw new BadRequestHttpException('incorrect_password'); + } + + throw new BadRequestHttpException('password_cannot_be_changed'); + } + } + + $user->setValues($values); + + if ($payload->keyBindingsJson !== null) { + $keyBindings = json_decode($payload->keyBindingsJson, true); + $tmpArray = []; + foreach ($keyBindings as $item) { + $tmpArray[] = json_decode($item, true); + } + $tmpArray = array_values(array_filter($tmpArray)); + $tmpArray = json_encode($tmpArray); + + $user->setKeyBindings($tmpArray); + } + + $user->save(); + } +} diff --git a/src/Handler/User/UpdateCurrentUser/UpdateCurrentUserPayload.php b/src/Handler/User/UpdateCurrentUser/UpdateCurrentUserPayload.php new file mode 100644 index 00000000..87b29821 --- /dev/null +++ b/src/Handler/User/UpdateCurrentUser/UpdateCurrentUserPayload.php @@ -0,0 +1,41 @@ +request->get('id'), + values: json_decode($request->request->get('data'), true), + isPasswordReset: \OpenDxp\Tool\Session::useBag($request->getSession(), static fn (AttributeBagInterface $adminSession) => (bool) $adminSession->get('password_reset')), + keyBindingsJson: $request->request->has('keyBindings') ? $request->request->get('keyBindings') : null, + ); + } +} diff --git a/src/Handler/User/UpdateUser/UpdateUserHandler.php b/src/Handler/User/UpdateUser/UpdateUserHandler.php new file mode 100644 index 00000000..6fc409d1 --- /dev/null +++ b/src/Handler/User/UpdateUser/UpdateUserHandler.php @@ -0,0 +1,135 @@ +userContext->getAdminUser(); + $currentUserIsAdmin = $adminUser?->isAdmin() ?? false; + + /** @var User\UserRole|null $user */ + $user = User\UserRole::getById($payload->id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + if ($user instanceof User && $user->isAdmin() && !$currentUserIsAdmin) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify admin users'); + } + if ($payload->values !== null) { + $values = $payload->values; + if (!empty($values['password'])) { + if (strlen($values['password']) < 10) { + throw new Exception('Passwords have to be at least 10 characters long'); + } + $values['password'] = Tool\Authentication::getPasswordHash($user->getName(), $values['password']); + } + + // check if there are permissions transmitted, if so reset them all to false (they will be set later) + foreach ($values as $key => $value) { + if (str_starts_with($key, 'permission_')) { + $user->setAllAclToFalse(); + + break; + } + } + + if ($user instanceof User && isset($values['2fa_required'])) { + $user->setTwoFactorAuthentication('required', (bool) $values['2fa_required']); + } + + $user->setValues($values); + + // only admins are allowed to create admin users + if ($user instanceof User && !$currentUserIsAdmin) { + $user->setAdmin(false); + } + + // check for permissions + $availableUserPermissionsList = new User\Permission\Definition\Listing(); + $availableUserPermissions = $availableUserPermissionsList->load(); + + foreach ($availableUserPermissions as $permission) { + if (isset($values['permission_' . $permission->getKey()])) { + $user->setPermission($permission->getKey(), (bool) $values['permission_' . $permission->getKey()]); + } + } + + // check for workspaces + if ($payload->workspaces !== null) { + $processedPaths = ['object' => [], 'asset' => [], 'document' => []]; + foreach ($payload->workspaces as $type => $spaces) { + $newWorkspaces = []; + foreach ($spaces as $space) { + if (in_array($space['path'], $processedPaths[$type])) { + throw new Exception('Error saving workspaces as multiple entries found for path "' . $space['path'] . '" in ' . $this->translator->trans((string) $type, [], 'admin') . 's'); + } + + $element = Element\Service::getElementByPath($type, $space['path']); + if ($element) { + $className = '\\OpenDxp\\Model\\User\\Workspace\\' . Element\Service::getBaseClassNameForElement($type); + $workspace = new $className(); + $workspace->setValues($space); + + $workspace->setCid($element->getId()); + $workspace->setCpath($element->getRealFullPath()); + $workspace->setUserId($user->getId()); + + $newWorkspaces[] = $workspace; + $processedPaths[$type][] = $space['path']; + } + } + $user->{'setWorkspaces' . ucfirst($type)}($newWorkspaces); + } + } + } + + if ($user instanceof User && $payload->keyBindingsJson !== null) { + $keyBindings = json_decode($payload->keyBindingsJson, true); + $tmpArray = []; + foreach ($keyBindings as $item) { + $tmpArray[] = json_decode($item, true); + } + $tmpArray = array_values(array_filter($tmpArray)); + $tmpArray = json_encode($tmpArray); + + $user->setKeyBindings($tmpArray); + } + + $user->save(); + } +} diff --git a/src/Handler/User/UpdateUser/UpdateUserPayload.php b/src/Handler/User/UpdateUser/UpdateUserPayload.php new file mode 100644 index 00000000..382bccd5 --- /dev/null +++ b/src/Handler/User/UpdateUser/UpdateUserPayload.php @@ -0,0 +1,47 @@ +request->getInt('id'), + values: $request->request->has('data') + ? (json_decode($request->request->get('data'), true) ?? null) + : null, + workspaces: $request->request->has('workspaces') + ? (json_decode($request->request->get('workspaces'), true) ?? null) + : null, + keyBindingsJson: $request->request->has('keyBindings') + ? $request->request->get('keyBindings') + : null, + ); + } +} diff --git a/src/Handler/User/UploadUserImage/UploadUserImageHandler.php b/src/Handler/User/UploadUserImage/UploadUserImageHandler.php new file mode 100644 index 00000000..091cee27 --- /dev/null +++ b/src/Handler/User/UploadUserImage/UploadUserImageHandler.php @@ -0,0 +1,60 @@ +userContext->getAdminUser(); + $targetUserId = $payload->targetUserId ?? (int) $adminUser?->getId(); + + $userObj = User::getById($targetUserId); + if (!$userObj) { + throw new NotFoundHttpException('User not found'); + } + + if (!$adminUser?->isAdmin()) { + if ($userObj->isAdmin()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify admin users'); + } + if ($adminUser?->getId() !== $userObj->getId()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify users other than themselves'); + } + } + + $assetType = Asset::getTypeFromMimeMapping($payload->avatarFile->getMimeType(), $payload->avatarFile->getFileName()); + if ($assetType !== 'image') { + throw new BadRequestHttpException('Unsupported file format.'); + } + + $userObj->setImage($payload->avatarFile->getPathname()); + } +} diff --git a/src/Handler/User/UploadUserImage/UploadUserImagePayload.php b/src/Handler/User/UploadUserImage/UploadUserImagePayload.php new file mode 100644 index 00000000..f973b68f --- /dev/null +++ b/src/Handler/User/UploadUserImage/UploadUserImagePayload.php @@ -0,0 +1,37 @@ +query->has('id') ? $request->query->getInt('id') : null, + avatarFile: $request->files->get('Filedata'), + ); + } +} diff --git a/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlHandler.php b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlHandler.php new file mode 100644 index 00000000..6318a7ea --- /dev/null +++ b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlHandler.php @@ -0,0 +1,81 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $workflow = $this->workflowRegistry->get($element, $payload->workflowName); + + if ($payload->isGlobalAction) { + $globalAction = $this->workflowManager->getGlobalAction($workflow->getName(), $payload->transition); + if ($globalAction) { + return new GetModalCustomHtmlResult( + customHtml: $this->buildCustomHtml($globalAction->getCustomHtmlService(), $element), + ); + } + } elseif ($workflow->can($element, $payload->transition)) { + $enabledTransitions = $workflow->getEnabledTransitions($element); + $matchedTransition = null; + foreach ($enabledTransitions as $_transition) { + if ($_transition->getName() === $payload->transition) { + $matchedTransition = $_transition; + } + } + + if ($matchedTransition instanceof Transition) { + return new GetModalCustomHtmlResult( + customHtml: $this->buildCustomHtml($matchedTransition->getCustomHtmlService(), $element), + ); + } + } + + throw new BadRequestHttpException('error validating the action on this element, element cannot perform this action'); + } + + private function buildCustomHtml(?CustomHtmlServiceInterface $customHtmlService, ConcreteObject|Document|Asset $element): array + { + $customHtml = []; + if ($customHtmlService) { + foreach (['top', 'center', 'bottom'] as $position) { + $customHtml[$position] = $customHtmlService->renderHtmlForRequestedPosition($element, $position); + } + } + + return $customHtml; + } +} diff --git a/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlPayload.php b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlPayload.php new file mode 100644 index 00000000..619256b9 --- /dev/null +++ b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlPayload.php @@ -0,0 +1,42 @@ +request->has('ctype') ? $request->request->getString('ctype') : $request->query->getString('ctype'), + cid: (int) ($request->request->has('cid') ? $request->request->getString('cid') : $request->query->getString('cid')), + workflowName: $request->request->getString('workflowName'), + transition: $request->request->getString('transition'), + isGlobalAction: $request->request->getString('isGlobalAction') === 'true', + ); + } +} diff --git a/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlResult.php b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlResult.php new file mode 100644 index 00000000..55a3458a --- /dev/null +++ b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlResult.php @@ -0,0 +1,25 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $data = []; + + foreach ($this->workflowManager->getAllWorkflowsForSubject($element) as $workflow) { + $workflowConfig = $this->workflowManager->getWorkflowConfig($workflow->getName()); + + $svg = null; + $msg = ''; + + try { + $svg = $this->getWorkflowSvg($workflow, $element); + } catch (InvalidArgumentException $e) { + $msg = $e->getMessage(); + } + + $url = $this->router->generate( + 'opendxp_admin_workflow_show_graph', + [ + 'cid' => $payload->cid, + 'ctype' => $payload->ctype, + 'workflow' => $workflow->getName(), + ] + ); + + $allowedTransitions = $this->actionsButtonService->getAllowedTransitions($workflow, $element); + $globalActions = $this->actionsButtonService->getGlobalActions($workflow, $element); + + $data[] = [ + 'workflowName' => $this->translator->trans($workflowConfig->getLabel(), [], 'admin'), + 'placeInfo' => $this->placeStatusInfo->getAllPalacesHtml($element, $workflow->getName()), + 'graph' => $msg ?: '
' . $svg . '
', + 'allowedTransitions' => $allowedTransitions, + 'globalActions' => $globalActions, + ]; + } + + return new GetWorkflowDetailsResult(data: $data); + } + + private function getWorkflowSvg(WorkflowInterface $workflow, ConcreteObject|Document|Asset $element): string + { + $marking = $workflow->getMarking($element); + + $php = Console::getExecutable('php'); + $dot = Console::getExecutable('dot'); + + if (!$php) { + throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['php'], 'admin')); + } + + if (!$dot) { + throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['dot'], 'admin')); + } + + $cmd = $php . ' ' . OPENDXP_PROJECT_ROOT . '/bin/console opendxp:workflow:dump ${WNAME} ${WPLACES} | ${DOT} -Tsvg'; + $params = [ + 'WNAME' => $workflow->getName(), + 'WPLACES' => implode(' ', array_keys($marking->getPlaces())), + 'DOT' => $dot, + ]; + + Console::addLowProcessPriority($cmd); + $process = Process::fromShellCommandline($cmd); + $process->run(null, $params); + + return $process->getOutput(); + } +} diff --git a/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsPayload.php b/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsPayload.php new file mode 100644 index 00000000..5a8a9509 --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsPayload.php @@ -0,0 +1,36 @@ +query->getString('ctype'), + cid: $request->query->getInt('cid'), + ); + } +} diff --git a/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsResult.php b/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsResult.php new file mode 100644 index 00000000..20bbbdd9 --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsResult.php @@ -0,0 +1,25 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $workflow = $this->workflowManager->getWorkflowIfExists($element, $payload->workflowName); + + if (empty($workflow)) { + return new GetWorkflowFormResult( + message: 'workflow not found', + notesEnabled: false, + notesRequired: false, + additionalFields: [], + ); + } + + $enabledTransitions = $workflow->getEnabledTransitions($element); + $transition = null; + foreach ($enabledTransitions as $_transition) { + if ($_transition->getName() === $payload->transitionName) { + $transition = $_transition; + } + } + + if (!$transition instanceof Transition) { + return new GetWorkflowFormResult( + message: sprintf('transition %s currently not allowed', $payload->transitionName), + notesEnabled: false, + notesRequired: false, + additionalFields: [], + ); + } + + return new GetWorkflowFormResult( + message: '', + notesEnabled: false, + notesRequired: $transition->getNotesCommentRequired(), + additionalFields: [], + ); + } +} diff --git a/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormPayload.php b/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormPayload.php new file mode 100644 index 00000000..c4992e57 --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormPayload.php @@ -0,0 +1,40 @@ +request->has('ctype') ? $request->request->getString('ctype') : $request->query->getString('ctype'), + cid: (int) ($request->request->has('cid') ? $request->request->getString('cid') : $request->query->getString('cid')), + workflowName: $request->request->getString('workflowName'), + transitionName: $request->request->getString('transitionName'), + ); + } +} diff --git a/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormResult.php b/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormResult.php new file mode 100644 index 00000000..7dc7e369 --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormResult.php @@ -0,0 +1,28 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $workflow = $this->workflowManager->getWorkflowByName($payload->workflowName); + $marking = $workflow->getMarking($element); + + $php = Console::getExecutable('php'); + $dot = Console::getExecutable('dot'); + + if (!$php) { + throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['php'], 'admin')); + } + + if (!$dot) { + throw new InvalidArgumentException($this->translator->trans('workflow_cmd_not_found', ['dot'], 'admin')); + } + + $cmd = $php . ' ' . OPENDXP_PROJECT_ROOT . '/bin/console opendxp:workflow:dump ${WNAME} ${WPLACES} | ${DOT} -Tsvg'; + $params = [ + 'WNAME' => $workflow->getName(), + 'WPLACES' => implode(' ', array_keys($marking->getPlaces())), + 'DOT' => $dot, + ]; + + Console::addLowProcessPriority($cmd); + $process = Process::fromShellCommandline($cmd); + $process->run(null, $params); + + return $process->getOutput(); + } +} diff --git a/src/Handler/Workflow/ShowGraph/ShowGraphPayload.php b/src/Handler/Workflow/ShowGraph/ShowGraphPayload.php new file mode 100644 index 00000000..21abcf48 --- /dev/null +++ b/src/Handler/Workflow/ShowGraph/ShowGraphPayload.php @@ -0,0 +1,38 @@ +query->getString('ctype'), + cid: $request->query->getInt('cid'), + workflowName: $request->query->has('workflowName') ? $request->query->getString('workflowName') : null, + ); + } +} diff --git a/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php new file mode 100644 index 00000000..b61d602d --- /dev/null +++ b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php @@ -0,0 +1,48 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $workflow = $this->workflowRegistry->get($element, $payload->workflowName); + + $globalAction = $this->workflowManager->getGlobalAction($payload->workflowName, $payload->transition); + $saveSubject = !$globalAction || $globalAction->getSaveSubject(); + + $this->workflowManager->applyGlobalAction($workflow, $element, $payload->transition, $payload->workflowOptions, $saveSubject); + } +} diff --git a/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionPayload.php b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionPayload.php new file mode 100644 index 00000000..b72a1b59 --- /dev/null +++ b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionPayload.php @@ -0,0 +1,42 @@ +request->has('ctype') ? $request->request->getString('ctype') : $request->query->getString('ctype'), + cid: (int) ($request->request->has('cid') ? $request->request->getString('cid') : $request->query->getString('cid')), + workflowName: $request->request->getString('workflowName'), + transition: $request->request->getString('transition'), + workflowOptions: $request->request->all('workflow'), + ); + } +} diff --git a/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionHandler.php b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionHandler.php new file mode 100644 index 00000000..0328aca0 --- /dev/null +++ b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionHandler.php @@ -0,0 +1,57 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + $workflow = $this->workflowRegistry->get($element, $payload->workflowName); + + if (!$workflow->can($element, $payload->transition)) { + $blockTransitionList = $workflow->buildTransitionBlockerList($element, $payload->transition); + $reasons = array_map( + static fn ($item) => $item->getMessage(), + iterator_to_array($blockTransitionList->getIterator(), true) + ); + + return new SubmitWorkflowTransitionResult(blocked: true, blockerReasons: $reasons); + } + + $this->workflowManager->applyWithAdditionalData($workflow, $element, $payload->transition, $payload->workflowOptions, true); + + return new SubmitWorkflowTransitionResult(blocked: false, blockerReasons: []); + } +} diff --git a/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionPayload.php b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionPayload.php new file mode 100644 index 00000000..08c79b3e --- /dev/null +++ b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionPayload.php @@ -0,0 +1,42 @@ +request->has('ctype') ? $request->request->getString('ctype') : $request->query->getString('ctype'), + cid: (int) ($request->request->has('cid') ? $request->request->getString('cid') : $request->query->getString('cid')), + workflowName: $request->request->getString('workflowName'), + transition: $request->request->getString('transition'), + workflowOptions: $request->request->all('workflow'), + ); + } +} diff --git a/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionResult.php b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionResult.php new file mode 100644 index 00000000..d792e285 --- /dev/null +++ b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionResult.php @@ -0,0 +1,27 @@ +getLatestVersion($adminUserId); + if ($latestVersion) { + $latestObj = $latestVersion->loadData(); + if ($latestObj instanceof Concrete) { + $draftVersion = $latestVersion; + + return $latestObj; + } + } + + return $object; + } +} diff --git a/src/Helper/DocumentVersionHelper.php b/src/Helper/DocumentVersionHelper.php new file mode 100644 index 00000000..95f580b0 --- /dev/null +++ b/src/Helper/DocumentVersionHelper.php @@ -0,0 +1,49 @@ +getLatestVersion($userId); + if ($latestVersion) { + $latestDoc = $latestVersion->loadData(); + if ($latestDoc instanceof PageSnippet) { + $draftVersion = $latestVersion; + + return $latestDoc; + } + } + + return $document; + } +} diff --git a/src/Http/ExtJsValueResolver.php b/src/Http/ExtJsValueResolver.php new file mode 100644 index 00000000..4e6e7f39 --- /dev/null +++ b/src/Http/ExtJsValueResolver.php @@ -0,0 +1,35 @@ +getType(); + if (!$type || !is_subclass_of($type, ExtJsPayloadInterface::class, true)) { + return []; + } + + return [$type::fromRequest($request)]; + } +} \ No newline at end of file diff --git a/src/Model/DataObject/DataObjectLoadContext.php b/src/Model/DataObject/DataObjectLoadContext.php new file mode 100644 index 00000000..a180d317 --- /dev/null +++ b/src/Model/DataObject/DataObjectLoadContext.php @@ -0,0 +1,27 @@ +request->getInt('id')); + } +} diff --git a/src/Payload/Common/IdQueryPayload.php b/src/Payload/Common/IdQueryPayload.php new file mode 100644 index 00000000..88b8f352 --- /dev/null +++ b/src/Payload/Common/IdQueryPayload.php @@ -0,0 +1,30 @@ +query->getInt('id')); + } +} diff --git a/src/Payload/Common/StringIdBodyPayload.php b/src/Payload/Common/StringIdBodyPayload.php new file mode 100644 index 00000000..9a30b898 --- /dev/null +++ b/src/Payload/Common/StringIdBodyPayload.php @@ -0,0 +1,30 @@ +request->getString('id')); + } +} diff --git a/src/Payload/ExtJsPayloadInterface.php b/src/Payload/ExtJsPayloadInterface.php new file mode 100644 index 00000000..947eaf64 --- /dev/null +++ b/src/Payload/ExtJsPayloadInterface.php @@ -0,0 +1,24 @@ +tokenResolver->getUser(); + if (!$user) { + return false; + } + + return $user->isAllowed($attribute); + } +} diff --git a/src/Service/AdminUserContext.php b/src/Service/AdminUserContext.php new file mode 100644 index 00000000..7beeab5e --- /dev/null +++ b/src/Service/AdminUserContext.php @@ -0,0 +1,38 @@ +tokenResolver->getUser(); + } + + public function getAdminUserProxy(): ?UserProxy + { + return $this->tokenResolver->getUserProxy(); + } +} diff --git a/src/Service/AdminUserContextInterface.php b/src/Service/AdminUserContextInterface.php new file mode 100644 index 00000000..8939e805 --- /dev/null +++ b/src/Service/AdminUserContextInterface.php @@ -0,0 +1,27 @@ + $data, + 'processed' => false, + ]); + $this->eventDispatcher->dispatch($updateEvent, AdminEvents::ASSET_LIST_BEFORE_UPDATE); + + if ($updateEvent->getArgument('processed')) { + return ['success' => true]; + } + + $data = $updateEvent->getArgument('data'); + + $asset = Asset::getById((int) $data['id']); + if (!$asset) { + throw new NotFoundHttpException('Asset not found'); + } + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException("Permission denied. You don't have the rights to save this asset."); + } + + $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); + $metadata = $asset->getMetadata(null, null, false, true); + $dirty = false; + + unset($data['id']); + $fieldLanguage = $effectiveLanguage; + foreach ($data as $key => $value) { + $fieldDef = explode('~', $key); + $key = $fieldDef[0]; + if (isset($fieldDef[1])) { + $fieldLanguage = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); + } + + foreach ($metadata as &$em) { + if ($em['name'] == $key && $em['language'] == $fieldLanguage) { + try { + $dataImpl = $loader->build($em['type']); + $value = $dataImpl->getDataFromListfolderGrid($value, $em); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $em['type']); + } + $em['data'] = $value; + $dirty = true; + break; + } + } + unset($em); + + if (!$dirty) { + $defaultMetadataFields = ['title', 'alt', 'copyright']; + if (in_array($key, $defaultMetadataFields)) { + $newEm = [ + 'name' => $key, + 'language' => $fieldLanguage, + 'type' => 'input', + 'data' => $value, + ]; + try { + $dataImpl = $loader->build($newEm['type']); + $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $newEm['type']); + } + $metadata[] = $newEm; + $dirty = true; + } else { + $predefined = Metadata\Predefined::getByName($key); + if ($predefined && (empty($predefined->getTargetSubtype()) + || $predefined->getTargetSubtype() === $asset->getType())) { + $newEm = [ + 'name' => $key, + 'language' => $fieldLanguage, + 'type' => $predefined->getType(), + 'data' => $value, + ]; + try { + $dataImpl = $loader->build($newEm['type']); + $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $newEm['type']); + } + $metadata[] = $newEm; + $dirty = true; + } + } + } + } + + if ($dirty) { + $metadataEvent = new GenericEvent(null, [ + 'id' => $asset->getId(), + 'metadata' => $metadata, + ]); + $this->eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); + + $asset->setMetadataRaw($metadataEvent->getArgument('metadata')); + $asset->save(); + + return ['success' => true]; + } + + return ['success' => false, 'message' => 'something went wrong.']; + } catch (NotFoundHttpException|AccessDeniedHttpException $e) { + throw $e; + } catch (Exception $e) { + return ['success' => false, 'message' => $e->getMessage()]; + } + } + } else { + $list = $this->gridHelperService->prepareAssetListingForGrid($allParams, $this->userContext->getAdminUser()); + + $beforeListLoadEvent = new GenericEvent($this->gridHelperService, [ + 'list' => $list, + 'context' => $allParams, + ]); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::ASSET_LIST_BEFORE_LIST_LOAD); + /** @var Asset\Listing $list */ + $list = $beforeListLoadEvent->getArgument('list'); + + $list->load(); + + $assets = []; + foreach ($list->getAssets() as $asset) { + if ($asset->isAllowed('list')) { + $assets[] = GridData\Asset::getData($asset, $allParams['fields'], $allParams['language'] ?? ''); + } + } + + $result = ['success' => true, 'data' => $assets, 'total' => $list->getTotalCount()]; + + $afterListLoadEvent = new GenericEvent($this->gridHelperService, [ + 'list' => $result, + 'context' => $allParams, + ]); + $this->eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::ASSET_LIST_AFTER_LIST_LOAD); + + return $afterListLoadEvent->getArgument('list'); + } + + return ['success' => false]; + } +} diff --git a/src/Service/Asset/AssetPayloadMapper.php b/src/Service/Asset/AssetPayloadMapper.php new file mode 100644 index 00000000..58331ab2 --- /dev/null +++ b/src/Service/Asset/AssetPayloadMapper.php @@ -0,0 +1,125 @@ +metadata !== null) { + $metadataEvent = new GenericEvent(null, [ + 'id' => $asset->getId(), + 'metadata' => $payload->metadata, + ]); + $this->eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); + $this->applyMetadata($metadataEvent->getArgument('metadata'), $asset); + } + + $this->applyProperties($payload->propertiesData, $asset); + $this->applyScheduler($payload->schedulerData, $asset); + $this->applyRawData($payload->rawData, $asset); + $this->applyImageSettings($payload->hasImage, $payload->imageData, $asset); + } + + private function applyMetadata(array $metadata, Asset $asset): void + { + $metadataValues = Asset\Service::minimizeMetadata($metadata['values'], 'editor'); + $asset->setMetadataRaw($metadataValues); + } + + private function applyProperties(?array $propertiesData, Asset $asset): void + { + if ($propertiesData === null) { + return; + } + + $properties = []; + foreach ($propertiesData as $propertyName => $propertyData) { + try { + $property = new Model\Property(); + $property->setType($propertyData['type']); + $property->setName($propertyName); + $property->setCtype('asset'); + $property->setDataFromEditmode($propertyData['data']); + $property->setInheritable($propertyData['inheritable']); + + $properties[$propertyName] = $property; + } catch (Exception) { + Logger::err("Can't add " . $propertyName . ' to asset ' . $asset->getRealFullPath()); + } + } + + $asset->setProperties($properties); + } + + private function applyScheduler(?array $schedulerData, Asset $asset): void + { + if ($schedulerData === null || !$asset->isAllowed('settings')) { + return; + } + + $userId = $this->userContext->getAdminUser()?->getId(); + $tasks = []; + + foreach ($schedulerData as $taskData) { + $taskData['userId'] = $userId; + $tasks[] = new Task($taskData); + } + + $asset->setScheduledTasks($tasks); + } + + private function applyRawData(?string $rawData, Asset $asset): void + { + if ($rawData !== null) { + $asset->setData($rawData); + } + } + + private function applyImageSettings(bool $hasImage, ?array $imageData, Asset $asset): void + { + if (!$asset instanceof Asset\Image) { + return; + } + + if ($hasImage && $imageData !== null) { + if (isset($imageData['focalPoint'])) { + $asset->setCustomSetting('focalPointX', $imageData['focalPoint']['x']); + $asset->setCustomSetting('focalPointY', $imageData['focalPoint']['y']); + } + } else { + $asset->removeCustomSetting('focalPointX'); + $asset->removeCustomSetting('focalPointY'); + } + } +} diff --git a/src/Service/Asset/AssetPersistenceCoordinator.php b/src/Service/Asset/AssetPersistenceCoordinator.php new file mode 100644 index 00000000..9c8f3fa8 --- /dev/null +++ b/src/Service/Asset/AssetPersistenceCoordinator.php @@ -0,0 +1,48 @@ +sessionService->saveAsset($asset); + } else { + $asset->setUserModification($this->userContext->getAdminUser()->getId()); + $asset->save(); + } + + return new SaveAssetResult( + $asset->getModificationDate(), + $asset->getVersionCount(), + $this->elementService->getElementTreeNodeConfig($asset), + ); + } +} diff --git a/src/Service/Asset/AssetUploadService.php b/src/Service/Asset/AssetUploadService.php new file mode 100644 index 00000000..5c9f112a --- /dev/null +++ b/src/Service/Asset/AssetUploadService.php @@ -0,0 +1,234 @@ +config['assets']['default_upload_path'] ?? '/'; + + if ($request->files->has('Filedata')) { + /** @var UploadedFile $file */ + $file = $request->files->get('Filedata'); + $filename = $file->getClientOriginalName(); + $sourcePath = $file->getPathname(); + } elseif ($request->request->get('type') === 'base64') { + $filename = $request->request->get('filename'); + $sourcePath = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/upload-base64' . uniqid('', false) . '.tmp'; + $data = preg_replace('@^data:[^,]+;base64,@', '', $request->request->get('data')); + $filesystem = new Filesystem(); + $filesystem->dumpFile($sourcePath, base64_decode($data)); + } else { + throw new Exception('The filename of the asset is empty'); + } + + $parentId = $request->query->getInt('parentId'); + $parentPath = $request->query->get('parentPath'); + + if ($request->query->has('dir') && $request->query->has('parentId')) { + $parent = Asset::getById((int) $request->query->get('parentId')); + $dir = $request->query->get('dir'); + if (str_contains($dir, '..')) { + throw new Exception('not allowed'); + } + + $newPath = $parent->getRealFullPath() . '/' . trim($dir, '/ '); + + $maxRetries = 5; + $newParent = null; + for ($retries = 0; $retries < $maxRetries; $retries++) { + try { + $newParent = Asset\Service::createFolderByPath($newPath); + + break; + } catch (Exception $e) { + if ($retries < ($maxRetries - 1)) { + $waitTime = random_int(100000, 900000); + usleep($waitTime); + } else { + throw $e; + } + } + } + if ($newParent) { + $parentId = $newParent->getId(); + } + } elseif (!$request->query->get('parentId') && $parentPath) { + $parent = Asset::getByPath($parentPath); + if ($parent instanceof Asset\Folder) { + $parentId = $parent->getId(); + } else { + $parentId = Asset\Service::createFolderByPath($parentPath)->getId(); + } + } + + $filename = Element\Service::getValidKey($filename, 'asset'); + if (empty($filename)) { + throw new Exception('The filename of the asset is empty'); + } + + $context = $request->query->get('context'); + if ($context) { + $context = json_decode($context, true); + $context = $context ?: []; + + $this->validateManyToManyRelationAssetType($context, $filename, $sourcePath); + + $event = new ResolveUploadTargetEvent($parentId, $filename); + $event->setArgument('context', $context); + + OpenDxp::getEventDispatcher()->dispatch($event, AssetEvents::RESOLVE_UPLOAD_TARGET); + $filename = Element\Service::getValidKey($event->getFilename(), 'asset'); + $parentId = $event->getParentId(); + } + + if (!$parentId) { + $parentId = Asset\Service::createFolderByPath($defaultUploadPath)->getId(); + } + + $parentAsset = Asset::getById((int)$parentId); + + if (!$request->query->get('allowOverwrite')) { + $filename = $this->getSafeFilename($parentAsset->getRealFullPath(), $filename); + } + + if (!$parentAsset->isAllowed('create')) { + throw new AccessDeniedHttpException( + 'Missing the permission to create new assets in the folder: ' . $parentAsset->getRealFullPath() + ); + } + if (is_file($sourcePath) && filesize($sourcePath) < 1) { + throw new Exception('File is empty!'); + } + + if (!is_file($sourcePath)) { + throw new Exception('Something went wrong, please check upload_max_filesize and post_max_size in your php.ini as well as the write permissions of your temporary directories.'); + } + + $uploadAssetType = $request->query->get('uploadAssetType'); + if ($uploadAssetType) { + $mimetype = MimeTypes::getDefault()->guessMimeType($sourcePath); + $assetType = Asset::getTypeFromMimeMapping($mimetype, $filename); + + if ($uploadAssetType !== $assetType) { + throw new Exception("Mime type $mimetype does not match with asset type: $uploadAssetType"); + } + } + + $adminUser = $this->userContext->getAdminUser(); + + if ($request->query->get('allowOverwrite') && Asset\Service::pathExists($parentAsset->getRealFullPath().'/'.$filename)) { + $asset = Asset::getByPath($parentAsset->getRealFullPath().'/'.$filename); + $asset->setStream(fopen($sourcePath, 'rb', false, \OpenDxp\File::getContext())); + $asset->save(); + } else { + $asset = Asset::create($parentId, [ + 'filename' => $filename, + 'sourcePath' => $sourcePath, + 'userOwner' => $adminUser->getId(), + 'userModification' => $adminUser->getId(), + ]); + } + + @unlink($sourcePath); + + return [ + 'success' => true, + 'asset' => $asset, + ]; + } + + public function getSafeFilename(string $targetPath, string $filename): string + { + $pathinfo = pathinfo($filename); + $originalFilename = $pathinfo['filename']; + $originalFileextension = empty($pathinfo['extension']) ? '' : '.' . $pathinfo['extension']; + $count = 1; + + if ($targetPath === '/') { + $targetPath = ''; + } + + while (true) { + if (Asset\Service::pathExists($targetPath . '/' . $filename)) { + $filename = $originalFilename . '_' . $count . $originalFileextension; + $count++; + } else { + return $filename; + } + } + } + + /** + * @throws ValidationException + */ + private function validateManyToManyRelationAssetType(array $context, string $filename, string $sourcePath): void + { + if (isset($context['containerType'], $context['objectId'], $context['fieldname']) + && 'object' === $context['containerType'] + && $object = Concrete::getById($context['objectId']) + ) { + $fieldDefinition = $object->getClass()->getFieldDefinition($context['fieldname']); + if (!$fieldDefinition instanceof ManyToManyRelation) { + return; + } + + $mimeType = MimeTypes::getDefault()->guessMimeType($sourcePath); + $type = Asset::getTypeFromMimeMapping($mimeType, $filename); + + $allowedAssetTypes = $fieldDefinition->getAssetTypes(); + $allowedAssetTypes = array_column($allowedAssetTypes, 'assetTypes'); + + if ( + !( + $fieldDefinition->getAssetsAllowed() + && ($allowedAssetTypes === [] || in_array($type, $allowedAssetTypes, true)) + ) + ) { + throw new ValidationException(sprintf('Invalid relation in field `%s` [type: %s]', $context['fieldname'], $type)); + } + } + } +} diff --git a/src/Service/Cache/OpenDxpCacheClearingService.php b/src/Service/Cache/OpenDxpCacheClearingService.php new file mode 100644 index 00000000..8fcd72e2 --- /dev/null +++ b/src/Service/Cache/OpenDxpCacheClearingService.php @@ -0,0 +1,45 @@ +cache->clearAll(); + + if ($this->filesystem->exists(OPENDXP_CACHE_DIRECTORY)) { + $this->filesystem->remove(OPENDXP_CACHE_DIRECTORY); + } + + $this->filesystem->dumpFile(OPENDXP_CACHE_DIRECTORY . '/.gitkeep', ''); + + $this->eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR); + } +} diff --git a/src/Service/Cache/SymfonyCacheClearingService.php b/src/Service/Cache/SymfonyCacheClearingService.php new file mode 100644 index 00000000..a176e944 --- /dev/null +++ b/src/Service/Cache/SymfonyCacheClearingService.php @@ -0,0 +1,46 @@ +kernel->getEnvironment() === $environment) { + foreach ($this->eventDispatcher->getListeners(KernelEvents::TERMINATE) as $listener) { + $this->eventDispatcher->removeListener(KernelEvents::TERMINATE, $listener); + } + + foreach ($this->eventDispatcher->getListeners(KernelEvents::EXCEPTION) as $listener) { + $this->eventDispatcher->removeListener(KernelEvents::EXCEPTION, $listener); + } + } + + $this->cacheClearer->clear($environment); + } +} diff --git a/src/Service/CustomLoginUrlGenerator.php b/src/Service/CustomLoginUrlGenerator.php new file mode 100644 index 00000000..b2b92506 --- /dev/null +++ b/src/Service/CustomLoginUrlGenerator.php @@ -0,0 +1,39 @@ +router->generate($this->customAdminRouteName, $params, UrlGeneratorInterface::ABSOLUTE_URL); + } catch (\Exception) { + return $this->router->generate($fallbackRoute, $params, UrlGeneratorInterface::ABSOLUTE_URL); + } + } +} diff --git a/src/Controller/Admin/DataObject/DataObjectActionsTrait.php b/src/Service/DataObject/DataObjectGridService.php similarity index 82% rename from src/Controller/Admin/DataObject/DataObjectActionsTrait.php rename to src/Service/DataObject/DataObjectGridService.php index 37fc5956..4343d9c0 100644 --- a/src/Controller/Admin/DataObject/DataObjectActionsTrait.php +++ b/src/Service/DataObject/DataObjectGridService.php @@ -1,5 +1,4 @@ setLocale($requestedLanguage); - } - } else { - $requestedLanguage = $request->getLocale(); - } - if ($action === 'update') { try { - $data = $this->decodeJson($allParams['data']); + $data = json_decode($allParams['data'], true, 512, JSON_THROW_ON_ERROR); $object = DataObject::getById((int)$data['id']); if (!$object instanceof DataObject\Concrete) { - throw $this->createNotFoundException('Object not found'); + throw new NotFoundHttpException('Object not found'); } if (!$object->isAllowed('publish')) { - throw $this->createAccessDeniedException("Permission denied. You don't have the rights to save this object."); + throw new AccessDeniedHttpException("Permission denied. You don't have the rights to save this object."); } - $objectData = $this->prepareObjectData($data, $object, $requestedLanguage, $localeService); + $objectData = $this->prepareObjectData($data, $object, $requestedLanguage); $object->setValues($objectData); if ($object->getPublished() === false) { @@ -96,6 +89,8 @@ protected function gridProxy( 'success' => true, 'data' => GridData\DataObject::getData($object, $allParams['fields'], $requestedLanguage), ]; + } catch (NotFoundHttpException|AccessDeniedHttpException $e) { + throw $e; } catch (Exception $e) { return [ 'success' => false, @@ -103,14 +98,14 @@ protected function gridProxy( ]; } } else { // get list of objects/variants - $list = $gridHelperService->prepareListingForGrid($allParams, $requestedLanguage, $this->getAdminUser()); + $list = $this->gridHelperService->prepareListingForGrid($allParams, $requestedLanguage, $this->userContext->getAdminUser()); if ($objectType === DataObject::OBJECT_TYPE_OBJECT) { - $beforeListLoadEvent = new GenericEvent($this, [ + $beforeListLoadEvent = new GenericEvent($this->gridHelperService, [ 'list' => $list, 'context' => $allParams, ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); + $this->eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); /** @var DataObject\Listing\Concrete $list */ $list = $beforeListLoadEvent->getArgument('list'); } @@ -124,8 +119,8 @@ protected function gridProxy( $objects = []; foreach ($list->getObjects() as $object) { if ($csvMode) { - $o = DataObject\Service::getCsvDataForObject($object, $requestedLanguage, $allParams['fields'], GridData\DataObject::getHelperDefinitions(), $localeService, 'title', false, $allParams['context']); - // Like for treeGetChildrenByIdAction, so we respect isAllowed method which can be extended (object DI) for custom permissions, so relying only users_workspaces_object is insufficient and could lead security breach + $o = DataObject\Service::getCsvDataForObject($object, $requestedLanguage, $allParams['fields'], GridData\DataObject::getHelperDefinitions(), $this->localeService,'title', false, $allParams['context']); + // respect isAllowed method which can be extended via object DI for custom permissions if ($object->isAllowed('list')) { $objects[] = $o; } @@ -145,12 +140,12 @@ protected function gridProxy( ]; if ($objectType === DataObject::OBJECT_TYPE_OBJECT) { - $afterListLoadEvent = new GenericEvent($this, [ + $afterListLoadEvent = new GenericEvent($this->gridHelperService, [ 'list' => $result, 'context' => $allParams, ]); - $eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::OBJECT_LIST_AFTER_LIST_LOAD); + $this->eventDispatcher->dispatch($afterListLoadEvent, AdminEvents::OBJECT_LIST_AFTER_LIST_LOAD); $result = $afterListLoadEvent->getArgument('list'); } @@ -165,7 +160,6 @@ private function prepareObjectData( array $data, DataObject\Concrete $object, string $requestedLanguage, - LocaleServiceInterface $localeService ): array { $user = Tool\Admin::getCurrentUser(); $languagePermissions = []; @@ -271,7 +265,7 @@ private function prepareObjectData( if ($localized instanceof DataObject\ClassDefinition\Data\Localizedfields) { $field = $localized->getFieldDefinition($key); if ($field) { - $currentLocale = $localeService->findLocale(); + $currentLocale = $this->localeService->findLocale(); if (!in_array($currentLocale, $languagePermissions)) { continue; } @@ -292,12 +286,12 @@ private function prepareObjectData( return $objectData; } - protected function getFieldDefinition(DataObject\ClassDefinition $class, string $key): ?DataObject\ClassDefinition\Data + private function getFieldDefinition(DataObject\ClassDefinition $class, string $key): ?DataObject\ClassDefinition\Data { return $class->getFieldDefinition($key); } - protected function getFieldDefinitionFromBrick(string $brickType, string $key): ?DataObject\ClassDefinition\Data + private function getFieldDefinitionFromBrick(string $brickType, string $key): ?DataObject\ClassDefinition\Data { $brickDefinition = DataObject\Objectbrick\Definition::getByKey($brickType); if ($brickDefinition) { diff --git a/src/Service/DataObject/DataObjectPayloadMapper.php b/src/Service/DataObject/DataObjectPayloadMapper.php new file mode 100644 index 00000000..3d4cf752 --- /dev/null +++ b/src/Service/DataObject/DataObjectPayloadMapper.php @@ -0,0 +1,214 @@ +data !== []) { + try { + $this->applyChanges($object, $payload->data); + } catch (Throwable) { + $this->applyChanges($objectFromDatabase, $payload->data); + } + } + + $this->assignProperties($payload->properties, $object); + $this->applyScheduler($payload->scheduler, $object); + } + + public function applyChanges(DataObject\Concrete $object, array $changes): void + { + foreach ($changes as $key => $value) { + $fd = $object->getClass()->getFieldDefinition($key); + if ($fd) { + if ($fd instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $user = Tool\Admin::getCurrentUser(); + if (!$user->getAdmin()) { + $allowedLanguages = DataObject\Service::getLanguagePermissions($object, $user, 'lEdit'); + if (!is_null($allowedLanguages)) { + $allowedLanguages = array_keys($allowedLanguages); + $submittedLanguages = array_keys($changes[$key]); + foreach ($submittedLanguages as $submittedLanguage) { + if (!in_array($submittedLanguage, $allowedLanguages)) { + unset($value[$submittedLanguage]); + } + } + } + } + } + + if ($fd instanceof ReverseObjectRelation) { + $remoteClass = DataObject\ClassDefinition::getByName($fd->getOwnerClassName()); + $relations = $object->getRelationData($fd->getOwnerFieldName(), false, $remoteClass->getId()); + $toAdd = $this->detectAddedRemoteOwnerRelations($relations, $value); + $toDelete = $this->detectDeletedRemoteOwnerRelations($relations, $value); + if (count($toAdd) > 0 || count($toDelete) > 0) { + $this->processRemoteOwnerRelations($object, $toDelete, $toAdd, $fd->getOwnerFieldName()); + } + } else { + $object->setValue($key, $fd->getDataFromEditmode($value, $object)); + } + } + } + } + + private function assignProperties(array $propertiesData, DataObject\AbstractObject $object): void + { + if ($propertiesData === []) { + return; + } + + $properties = []; + foreach ($object->getProperties() as $p) { + if ($p->isInherited()) { + $properties[$p->getName()] = $p; + } + } + + foreach ($propertiesData as $propertyName => $propertyData) { + $value = $propertyData['data']; + + try { + $property = new Model\Property(); + $property->setType($propertyData['type']); + $property->setName($propertyName); + $property->setCtype('object'); + $property->setDataFromEditmode($value); + $property->setInheritable($propertyData['inheritable']); + + $properties[$propertyName] = $property; + } catch (Exception) { + Logger::err("Can't add " . $propertyName . ' to object ' . $object->getRealFullPath()); + } + } + + $object->setProperties($properties); + } + + private function applyScheduler(array $schedulerData, DataObject\AbstractObject $object): void + { + if ($schedulerData === []) { + return; + } + + $adminUser = $this->userContext->getAdminUser(); + $tasks = []; + foreach ($schedulerData as $taskData) { + $taskData['userId'] = $adminUser->getId(); + $task = new Task($taskData); + $tasks[] = $task; + } + + if ($object->isAllowed('settings') && method_exists($object, 'setScheduledTasks')) { + $object->setScheduledTasks($tasks); + } + } + + private function processRemoteOwnerRelations(DataObject\Concrete $object, array $toDelete, array $toAdd, string $ownerFieldName): void + { + $getter = 'get' . ucfirst($ownerFieldName); + $setter = 'set' . ucfirst($ownerFieldName); + + foreach ($toDelete as $id) { + $owner = DataObject::getById($id); + //TODO: lock ?! + if (method_exists($owner, $getter)) { + $currentData = $owner->$getter(); + if (is_array($currentData)) { + $counter = count($currentData); + for ($i = 0; $i < $counter; $i++) { + if ($currentData[$i]->getId() == $object->getId()) { + unset($currentData[$i]); + $owner->$setter($currentData); + + break; + } + } + } elseif ($currentData->getId() == $object->getId()) { + $owner->$setter(null); + } + } + $owner->setUserModification($object->getUserModification()); + $owner->save(); + Logger::debug('Saved object id [ ' . $owner->getId() . ' ] by remote modification through [' . $object->getId() . '], Action: deleted [ ' . $object->getId() . " ] from [ $ownerFieldName]"); + } + + foreach ($toAdd as $id) { + $owner = DataObject::getById($id); + //TODO: lock ?! + if (method_exists($owner, $getter)) { + $currentData = $owner->$getter(); + if (is_array($currentData)) { + $currentData[] = $object; + } else { + $currentData = $object; + } + $owner->$setter($currentData); + $owner->setUserModification($object->getUserModification()); + $owner->save(); + Logger::debug('Saved object id [ ' . $owner->getId() . ' ] by remote modification through [' . $object->getId() . '], Action: added [ ' . $object->getId() . " ] to [ $ownerFieldName ]"); + } + } + } + + private function detectDeletedRemoteOwnerRelations(array $relations, array $value): array + { + $originals = []; + $changed = []; + foreach ($relations as $r) { + $originals[] = $r['dest_id']; + } + + foreach ($value as $row) { + $changed[] = $row['id']; + } + + return array_diff($originals, $changed); + } + + private function detectAddedRemoteOwnerRelations(array $relations, array $value): array + { + $originals = []; + $changed = []; + + foreach ($relations as $r) { + $originals[] = $r['dest_id']; + } + + foreach ($value as $row) { + $changed[] = $row['id']; + } + + return array_diff($changed, $originals); + } +} diff --git a/src/Service/DataObject/DataObjectPersistenceCoordinator.php b/src/Service/DataObject/DataObjectPersistenceCoordinator.php new file mode 100644 index 00000000..c28264ba --- /dev/null +++ b/src/Service/DataObject/DataObjectPersistenceCoordinator.php @@ -0,0 +1,113 @@ +userContext->getAdminUser(); + + if ($task === 'unpublish') { + $object->setPublished(false); + } + + if ($task === 'publish') { + $object->setPublished(true); + } + + // unpublish and save version is possible without checking mandatory fields + if (in_array($task, ['unpublish', 'version', 'autoSave'])) { + $object->setOmitMandatoryCheck(true); + } + + if ($task === 'publish' || $task === 'unpublish') { + $object->save(); + $treeData = $this->elementService->getElementTreeNodeConfig($object); + $newObject = DataObject::getById($object->getId(), ['force' => true]); + + if ($task === 'publish') { + $object->deleteAutoSaveVersions($adminUser->getId()); + } + + return new SaveDataObjectResult( + modificationDate: $object->getModificationDate(), + versionDate: $newObject->getModificationDate(), + versionCount: $newObject->getVersionCount(), + treeData: $treeData, + draftData: [], + ); + } + + if ($task === 'scheduler' && $object->isAllowed('settings')) { + $object->saveScheduledTasks(); + + return new SaveDataObjectResult( + modificationDate: $object->getModificationDate(), + versionDate: $object->getModificationDate(), + versionCount: $object->getVersionCount(), + treeData: [], + draftData: [], + ); + } + + if ($object->isAllowed('save') || $object->isAllowed('publish')) { + $isAutoSave = $task === 'autoSave'; + $draftData = []; + + if ($object->isPublished() || $isAutoSave) { + $version = $object->saveVersion(true, true, null, $isAutoSave); + $draftData = [ + 'id' => $version->getId(), + 'modificationDate' => $version->getDate(), + 'isAutoSave' => $version->isAutoSave(), + ]; + } else { + $object->save(); + } + + if ($task === 'version') { + $object->deleteAutoSaveVersions($adminUser->getId()); + } + + $treeData = $this->elementService->getElementTreeNodeConfig($object); + $newObject = DataObject::getById($object->getId(), ['force' => true]); + + return new SaveDataObjectResult( + modificationDate: $object->getModificationDate(), + versionDate: $newObject->getModificationDate(), + versionCount: $newObject->getVersionCount(), + treeData: $treeData, + draftData: $draftData, + ); + } + + throw new AccessDeniedHttpException('Missing permission to save object'); + } +} diff --git a/src/Service/Document/DocumentPayloadMapper.php b/src/Service/Document/DocumentPayloadMapper.php new file mode 100644 index 00000000..b76b8c75 --- /dev/null +++ b/src/Service/Document/DocumentPayloadMapper.php @@ -0,0 +1,245 @@ +missingRequiredEditable !== null) { + $document->setMissingRequiredEditable($payload->missingRequiredEditable); + } + + if ($payload->settings !== null && ($payload->settings['published'] ?? false)) { + $document->setMissingRequiredEditable(null); + } + + $this->applySettings($payload->settings, $document); + $this->applyEditables($payload->editables, $payload->appendEditables, $document); + $this->applyProperties($payload->properties, $document); + $this->applyScheduler($payload->scheduler, $document); + } + + public function applyLinkPayload(SaveLinkPayload $payload, Link $document): void + { + $this->applyLinkData($payload->data, $document); + $this->applyProperties($payload->properties, $document); + $this->applyScheduler($payload->scheduler, $document); + } + + public function applyHardlinkPayload(SaveHardlinkPayload $payload, Hardlink $document): void + { + $this->applyHardlinkData($payload->data, $document); + $this->applyProperties($payload->properties, $document); + $this->applyScheduler($payload->scheduler, $document); + } + + public function applyFolderPayload(SaveFolderPayload $payload, Folder $document): void + { + $this->applyProperties($payload->properties, $document); + } + + private function applySettings(?array $settings, Document $document): void + { + if ($settings === null || !$document->isAllowed('settings')) { + return; + } + + if (array_key_exists('prettyUrl', $settings)) { + $settings['prettyUrl'] = htmlspecialchars($settings['prettyUrl']); + } + + $document->setValues($settings); + } + + private function applyEditables(?array $editables, bool $appendEditables, Document\PageSnippet $document): void + { + if ($editables === null) { + return; + } + + $isTargetSpecific = interface_exists(TargetingDocumentInterface::class) + && $document instanceof TargetingDocumentInterface + && $document->hasTargetGroupSpecificEditables(); + + if ($appendEditables || $isTargetSpecific) { + $document->getEditables(); + } else { + $document->setEditables(null); + } + + foreach ($editables as $name => $editableData) { + $document->setRawEditable($name, $editableData['type'], $editableData['data'] ?? null); + } + } + + private function applyProperties(?array $propertiesData, Document $document): void + { + if ($propertiesData === null) { + $document->getProperties(); + + return; + } + + $properties = []; + foreach ($document->getProperties() as $p) { + if ($p->isInherited()) { + $properties[$p->getName()] = $p; + } + } + + foreach ($propertiesData as $propertyName => $propertyData) { + $value = $propertyData['data']; + + try { + $property = new Property(); + $property->setType($propertyData['type']); + $property->setName($propertyName); + $property->setCtype('document'); + $property->setDataFromEditmode($value); + $property->setInheritable($propertyData['inheritable']); + + if ($propertyName === 'language') { + $property->setInherited($this->resolvePropertyInheritance($document, $propertyName, $value)); + } + + $properties[$propertyName] = $property; + } catch (Exception) { + Logger::warning("Can't add " . $propertyName . ' to document ' . $document->getRealFullPath()); + } + } + + if ($document->isAllowed('properties')) { + $document->setProperties($properties); + } + + $document->getProperties(); + } + + private function applyScheduler(?array $schedulerData, ElementInterface $element): void + { + if ($schedulerData === null || !$element->isAllowed('settings') || !method_exists($element, 'setScheduledTasks')) { + return; + } + + $userId = $this->userContext->getAdminUser()?->getId(); + $tasks = []; + + foreach ($schedulerData as $taskData) { + $taskData['userId'] = $userId; + $tasks[] = new Task($taskData); + } + + $element->setScheduledTasks($tasks); + } + + private function applyLinkData(?array $data, Link $document): void + { + if ($data === null) { + return; + } + + $path = $data['path']; + $target = null; + + if (!empty($path)) { + if ($data['linktype'] === 'internal' && $data['internalType']) { + $target = Element\Service::getElementByPath($data['internalType'], $path); + if ($target) { + $data['internal'] = $target->getId(); + } + } + + if (!$target) { + if ($target = Document::getByPath($path)) { + $data['internalType'] = 'document'; + $data['internal'] = $target->getId(); + } elseif ($target = Asset::getByPath($path)) { + $data['internalType'] = 'asset'; + $data['internal'] = $target->getId(); + } elseif ($target = Concrete::getByPath($path)) { + $data['internalType'] = 'object'; + $data['internal'] = $target->getId(); + } else { + $data['linktype'] = 'direct'; + $data['internalType'] = null; + $data['internal'] = null; + $data['direct'] = $path; + } + + if ($target) { + $data['linktype'] = 'internal'; + $data['direct'] = ''; + } + } + } else { + $data['linktype'] = 'internal'; + $data['direct'] = ''; + $data['internalType'] = null; + $data['internal'] = null; + } + + unset($data['path']); + $document->setValues($data); + } + + private function applyHardlinkData(?array $data, Hardlink $document): void + { + if ($data === null) { + return; + } + + $sourceId = null; + if ($sourceDocument = Document::getByPath($data['sourcePath'])) { + $sourceId = $sourceDocument->getId(); + } + + $document->setSourceId($sourceId); + $document->setValues($data); + } + + private function resolvePropertyInheritance(Document $document, string $propertyName, mixed $value): bool + { + if ($document->getParent()) { + return $value == $document->getParent()->getProperty($propertyName); + } + + return false; + } +} diff --git a/src/Service/Document/DocumentPersistenceCoordinator.php b/src/Service/Document/DocumentPersistenceCoordinator.php new file mode 100644 index 00000000..8c7075c6 --- /dev/null +++ b/src/Service/Document/DocumentPersistenceCoordinator.php @@ -0,0 +1,75 @@ +setModificationDate(time()); + $document->setUserModification($this->userContext->getAdminUser()->getId()); + + $version = null; + + if ($task === 'publish' && $document->isAllowed('publish')) { + $document->setPublished(true); + $document->save(); + } elseif ($task === 'unpublish' && $document->isAllowed('unpublish')) { + $document->setPublished(false); + $document->save(); + } elseif (in_array($task, ['save', 'version', 'autosave'], true) && $document->isAllowed('save')) { + if ($document instanceof Document\PageSnippet) { + if ($task === 'autosave' || $document->isPublished()) { + $version = $document->saveVersion(true, true, null, $task === 'autosave'); + } else { + $document->save(); + } + } + } elseif ($task === 'scheduler' && $document->isAllowed('settings')) { + if ($document instanceof Document\PageSnippet + || $document instanceof Document\Hardlink + || $document instanceof Document\Link) { + $document->saveScheduledTasks(); + } + } else { + throw new AccessDeniedHttpException(); + } + + if ($document instanceof Document\PageSnippet && in_array($task, ['publish', 'version'], true)) { + $document->deleteAutoSaveVersions(); + } + + return new DocumentSaveResult( + task: $task, + document: $document, + version: $version, + treeData: $this->elementService->getElementTreeNodeConfig($document), + ); + } +} diff --git a/src/Service/Document/DocumentSaveResult.php b/src/Service/Document/DocumentSaveResult.php new file mode 100644 index 00000000..a6e2b91f --- /dev/null +++ b/src/Service/Document/DocumentSaveResult.php @@ -0,0 +1,31 @@ +requestStack->getSession()->getId(); + + if (!Editlock::isLocked($id, $type, $sessionId)) { + Editlock::lock($id, $type, $sessionId); + + return; + } + + $lockData = ['task' => 'response']; + $eventArgs = ['data' => $lockData]; + if ($element !== null) { + $eventArgs['object'] = $element; + } + + $event = new GenericEvent(null, $eventArgs); + $this->eventDispatcher->dispatch($event, $eventName); + $task = $event->getArgument('data')['task']; + + if ($task === self::TASK_OVERWRITE) { + Editlock::lock($id, $type, $sessionId); + + return; + } + + $lock = Editlock::getByElement($id, $type); + throw new ElementLockedException($id, $type, $lock); + } +} diff --git a/src/Service/Element/SessionService.php b/src/Service/Element/SessionService.php new file mode 100644 index 00000000..a9e9463f --- /dev/null +++ b/src/Service/Element/SessionService.php @@ -0,0 +1,100 @@ +sessionId(); + DocumentService::saveElementToSession($doc, $sessionId); + if ($useForSave) { + DocumentService::saveElementToSession($doc, $sessionId, '_useForSave'); + } + } + + public function getDocument(Document $doc): ?Document + { + $sessionId = $this->sessionId(); + $sessionDoc = DocumentService::getElementFromSession('document', $doc->getId(), $sessionId); + if ($sessionDoc && DocumentService::getElementFromSession('document', $doc->getId(), $sessionId, '_useForSave')) { + DocumentService::removeElementFromSession('document', $doc->getId(), $sessionId, '_useForSave'); + } + + return $sessionDoc ?: null; + } + + public function getOrLoadDocument(int $id): ?Document + { + $sessionId = $this->sessionId(); + $doc = DocumentService::getElementFromSession('document', $id, $sessionId); + if ($doc instanceof Document) { + return $doc; + } + + $doc = Document\PageSnippet::getById($id); + if (!$doc) { + return null; + } + + $latestVersion = $doc->getLatestVersion(); + if ($latestVersion && ($latestDoc = $latestVersion->loadData()) instanceof Document\PageSnippet) { + return $latestDoc; + } + + return $doc; + } + + public function removeDocument(int $docId): void + { + DocumentService::removeElementFromSession('document', $docId, $this->sessionId()); + } + + public function saveObject(DataObject\AbstractObject $obj, string $suffix = ''): void + { + DataObject\Service::saveElementToSession($obj, $this->sessionId(), $suffix); + } + + public function getObject(string $type, int $id): ?DataObject\AbstractObject + { + return DataObject\Service::getElementFromSession($type, $id, $this->sessionId()) ?: null; + } + + public function removeObject(string $type, int $id): void + { + DataObject\Service::removeElementFromSession($type, $id, $this->sessionId()); + } + + public function saveAsset(Asset $asset): void + { + Asset\Service::saveElementToSession($asset, $this->sessionId()); + } + + private function sessionId(): string + { + return $this->requestStack->getSession()->getId(); + } +} diff --git a/src/Service/Grid/AssetGridColumnConfigResolver.php b/src/Service/Grid/AssetGridColumnConfigResolver.php new file mode 100644 index 00000000..28fe0749 --- /dev/null +++ b/src/Service/Grid/AssetGridColumnConfigResolver.php @@ -0,0 +1,176 @@ +userContext->getAdminUser(); + $classId = $params['id']; + $context = ['purpose' => 'gridconfig']; + $types = !empty($params['types']) ? explode(',', $params['types']) : []; + $userId = $user?->getId() ?? 0; + $requestedGridConfigId = $isDelete ? null : ($params['gridConfigId'] ?? null); + $searchType = $params['searchType']; + + if ((string) ($requestedGridConfigId ?? '') === '' && $classId) { + $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $classId, 0, $searchType); + if ($favourite) { + $requestedGridConfigId = $favourite->getGridConfigId(); + } + } + + $configData = $this->gridColumnConfigService->loadVerifiedGridConfig($requestedGridConfigId, $user, 'asset'); + $gridConfig = $configData->config; + + $availableFields = []; + if ($configData->isEmpty()) { + $availableFields = $this->getDefaultGridFields($params['noSystemColumns'], $context, $types); + } else { + foreach ($gridConfig['columns'] as $sc) { + if (!$sc['hidden']) { + $colConfig = $this->getFieldGridConfig($sc); + if ($colConfig) { + $availableFields[] = $colConfig; + } + } + } + } + usort($availableFields, static fn ($a, $b) => $a['position'] <=> $b['position']); + + $availableConfigs = $classId ? $this->gridColumnConfigService->getMyOwnColumnConfigs($userId, $classId, $searchType) : []; + $sharedConfigs = $classId ? $this->gridColumnConfigService->getSharedColumnConfigs($user, $classId, $searchType) : []; + $settings = $this->gridColumnConfigService->buildBaseSettings($configData); + + $gridContext = $gridConfig['context'] ?? null; + if ($gridContext) { + $gridContext = json_decode($gridContext, true); + } + + return new GridColumnConfigResult( + availableFields: $availableFields, + settings: $settings, + availableConfigs: $availableConfigs, + sharedConfigs: $sharedConfigs, + sortinfo: $gridConfig['sortinfo'] ?? false, + onlyDirectChildren: $gridConfig['onlyDirectChildren'] ?? false, + pageSize: $gridConfig['pageSize'] ?? false, + context: $gridContext, + onlyUnreferenced: $gridConfig['onlyUnreferenced'] ?? false, + ); + } + + private function getDefaultGridFields(bool $noSystemColumns, array $context, array $types = []): array + { + $count = 0; + $availableFields = []; + + if (!$noSystemColumns) { + foreach (Asset\Service::GRID_SYSTEM_COLUMNS as $sc) { + if ($types === []) { + $availableFields[] = [ + 'key' => $sc . '~system', + 'type' => 'system', + 'label' => $sc, + 'position' => $count, + ]; + $count++; + } + } + } + + return $availableFields; + } + + private function getFieldGridConfig(array $field, ?string $keyPrefix = null): ?array + { + $defaultMetadataFields = ['copyright', 'alt', 'title']; + $predefined = null; + + if (isset($field['fieldConfig']['layout']['name'])) { + $predefined = Metadata\Predefined::getByName($field['fieldConfig']['layout']['name']); + } + + $key = $field['name']; + if ($keyPrefix) { + $key = $keyPrefix . $key; + } + $fieldDef = explode('~', $field['name']); + $field['name'] = $fieldDef[0]; + + if (isset($fieldDef[1]) && $fieldDef[1] === 'system') { + $type = 'system'; + } elseif (in_array($fieldDef[0], $defaultMetadataFields)) { + $type = 'input'; + } else { + $type = $field['fieldConfig']['type']; + if (isset($fieldDef[1])) { + $field['fieldConfig']['label'] = $field['fieldConfig']['layout']['title'] = $fieldDef[0] . ' (' . $fieldDef[1] . ')'; + $field['fieldConfig']['layout']['icon'] = Tool::getLanguageFlagFile($fieldDef[1], true); + } + } + + $result = [ + 'key' => $key, + 'type' => $type, + 'label' => $field['fieldConfig']['label'] ?? $key, + 'width' => $field['width'], + 'position' => $field['position'], + 'language' => $field['fieldConfig']['language'] ?? null, + 'layout' => $field['fieldConfig']['layout'] ?? null, + ]; + + if (isset($field['locked'])) { + $result['locked'] = $field['locked']; + } + + if ($type === 'select' && $predefined) { + $field['fieldConfig']['layout']['config'] = $predefined->getConfig(); + $result['layout'] = $field['fieldConfig']['layout']; + } elseif (in_array($type, ['document', 'asset', 'object'], true)) { + $result['layout']['fieldtype'] = 'manyToOneRelation'; + $result['layout']['subtype'] = $type; + } + + $event = new GenericEvent(null, [ + 'field' => $field, + 'result' => $result, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::ASSET_GET_FIELD_GRID_CONFIG); + + return $event->getArgument('result'); + } +} diff --git a/src/Service/Grid/DataObjectGridColumnConfigResolver.php b/src/Service/Grid/DataObjectGridColumnConfigResolver.php new file mode 100644 index 00000000..3b8f4e65 --- /dev/null +++ b/src/Service/Grid/DataObjectGridColumnConfigResolver.php @@ -0,0 +1,457 @@ +userContext->getAdminUser(); + $class = null; + $fields = null; + + if ($params['id'] !== null) { + $class = DataObject\ClassDefinition::getById($params['id']); + } elseif ($params['name'] !== null) { + $class = DataObject\ClassDefinition::getByName($params['name']); + } + + $gridType = $params['gridtype'] ?? 'search'; + $objectId = $params['objectId'] !== null ? (int) $params['objectId'] : 0; + + if ($objectId) { + $fields = DataObject\Service::getCustomGridFieldDefinitions($class->getId(), $objectId); + } + + $context = ['purpose' => 'gridconfig']; + if ($class) { + $context['class'] = $class; + } + if ($objectId) { + $context['object'] = DataObject::getById($objectId); + } + + if (!$fields && $class) { + $fields = $class->getFieldDefinitions(); + } + + $types = $params['types'] !== null ? explode(',', $params['types']) : []; + $userId = $user?->getId() ?? 0; + $requestedGridConfigId = $isDelete ? null : $params['gridConfigId']; + $searchType = $params['searchType']; + + if ((string) ($requestedGridConfigId ?? '') === '' && $class) { + $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $class->getId(), $objectId ?: 0, $searchType); + if (!$favourite && $objectId) { + $favourite = GridConfigFavourite::getByOwnerAndClassAndObjectId($userId, $class->getId(), 0, $searchType); + } + if ($favourite) { + $requestedGridConfigId = $favourite->getGridConfigId(); + } + } + + $configData = $this->gridColumnConfigService->loadVerifiedGridConfig($requestedGridConfigId, $user); + $gridConfig = $configData->config; + + $localizedFields = []; + if (is_array($fields)) { + foreach ($fields as $field) { + if ($field instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $localizedFields[] = $field; + } + } + } + + $availableFields = []; + if ($configData->isEmpty()) { + $availableFields = $this->getDefaultGridFields( + $params['noSystemColumns'], + $class, + $gridType, + $params['noBrickColumns'], + $fields, + $context, + $objectId, + $types + ); + } else { + $savedColumns = $gridConfig['columns']; + foreach ($savedColumns as $key => $sc) { + if (!$sc['hidden']) { + if (in_array($key, self::SYSTEM_COLUMNS, true)) { + $colConfig = [ + 'key' => $key, + 'type' => 'system', + 'label' => $key, + 'position' => $sc['position'], + ]; + $colConfig = $this->injectCustomLayoutValues($colConfig, $sc); + $availableFields[] = $colConfig; + } else { + $keyParts = explode('~', $key); + + if (str_starts_with($key, '~')) { + $type = $keyParts[1]; + $groupAndKeyId = explode('-', $keyParts[3]); + $keyId = (int) $groupAndKeyId[1]; + + if ($type === 'classificationstore') { + $keyDef = DataObject\Classificationstore\KeyConfig::getById($keyId); + if ($keyDef) { + $keyFieldDef = json_decode($keyDef->getDefinition(), true); + if ($keyFieldDef) { + $keyFieldDef = DataObject\Classificationstore\Service::getFieldDefinitionFromJson($keyFieldDef, $keyDef->getType()); + $fieldConfig = $this->getFieldGridConfig($keyFieldDef, $gridType, (string) $sc['position'], true, null, $class, $objectId); + if ($fieldConfig) { + $fieldConfig['key'] = $key; + $fieldConfig['label'] = '#' . $keyFieldDef->getTitle(); + $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); + $availableFields[] = $fieldConfig; + } + } + } + } + } elseif (count($keyParts) > 1) { + $brick = $keyParts[0]; + $brickDescriptor = null; + + if (str_contains($brick, '?')) { + $brickDescriptor = substr($brick, 1); + $brickDescriptor = json_decode($brickDescriptor, true); + $keyPrefix = $brick . '~'; + $brick = $brickDescriptor['containerKey']; + } else { + $keyPrefix = $brick . '~'; + } + + $fieldname = $keyParts[1]; + $brickClass = DataObject\Objectbrick\Definition::getByKey($brick); + + $fd = null; + if ($brickClass instanceof DataObject\Objectbrick\Definition) { + if ($brickDescriptor) { + $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; + /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedField */ + $localizedField = $brickClass->getFieldDefinition($innerContainer); + $fd = $localizedField->getFieldDefinition($brickDescriptor['brickfield']); + } else { + $fd = $brickClass->getFieldDefinition($fieldname); + } + } + + if ($fd !== null) { + $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string) $sc['position'], true, $keyPrefix, $class, $objectId); + if (!empty($fieldConfig)) { + $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); + $availableFields[] = $fieldConfig; + } + } + } elseif (DataObject\Service::isHelperGridColumnConfig($key)) { + $calculatedColumnConfig = $helperColumnsBag !== null + ? $this->getCalculatedColumnConfig($helperColumnsBag, $sc) + : null; + if ($calculatedColumnConfig) { + $availableFields[] = $calculatedColumnConfig; + } + } else { + $fd = $class->getFieldDefinition($key); + if (empty($fd)) { + foreach ($localizedFields as $lf) { + $fd = $lf->getFieldDefinition($key); + if (!empty($fd)) { + break; + } + } + } + + if (!empty($fd)) { + $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string) $sc['position'], true, null, $class, $objectId); + if (!empty($fieldConfig)) { + $fieldConfig = $this->injectCustomLayoutValues($fieldConfig, $sc); + $availableFields[] = $fieldConfig; + } + } + } + } + } + } + } + + usort($availableFields, static fn ($a, $b) => $a['position'] <=> $b['position']); + + $frontendLanguages = Tool\Admin::reorderWebsiteLanguages(Tool\Admin::getCurrentUser(), $this->config['general']['valid_languages']); + $language = $frontendLanguages ? $frontendLanguages[0] : $locale; + if (!Tool::isValidLanguage($language)) { + $validLanguages = Tool::getValidLanguages(); + $language = $validLanguages[0]; + } + if (!empty($gridConfig) && !empty($gridConfig['language'])) { + $language = $gridConfig['language']; + } + + $availableConfigs = $class ? $this->gridColumnConfigService->getMyOwnColumnConfigs($userId, $class->getId(), $searchType) : []; + $sharedConfigs = $class ? $this->gridColumnConfigService->getSharedColumnConfigs($user, $class->getId(), $searchType) : []; + + $settings = $this->gridColumnConfigService->buildBaseSettings($configData); + $owner = null; + if ($configData->ownerId) { + $ownerObject = User::getById($configData->ownerId); + $owner = $ownerObject instanceof User ? $ownerObject->getName() : (string) $configData->ownerId; + } + $settings['owner'] = $owner; + $settings['modificationDate'] = $configData->modificationDate; + $settings['saveFilters'] = $configData->isEmpty() ? null : $configData->saveFilters; + $settings['allowVariants'] = $class && $class->getAllowVariants(); + + $gridContext = $gridConfig['context'] ?? null; + if ($gridContext) { + $gridContext = json_decode($gridContext, true); + } + + return new GridColumnConfigResult( + availableFields: $availableFields, + settings: $settings, + availableConfigs: $availableConfigs, + sharedConfigs: $sharedConfigs, + sortinfo: $gridConfig['sortinfo'] ?? false, + onlyDirectChildren: $gridConfig['onlyDirectChildren'] ?? false, + pageSize: $gridConfig['pageSize'] ?? false, + context: $gridContext, + language: $language, + searchFilter: $gridConfig['searchFilter'] ?? '', + filter: $gridConfig['filter'] ?? [], + ); + } + + /** + * @param DataObject\ClassDefinition\Data[]|null $fields + */ + private function getDefaultGridFields(bool $noSystemColumns, ?DataObject\ClassDefinition $class, string $gridType, bool $noBrickColumns, ?array $fields, array $context, int $objectId, array $types = []): array + { + $count = 0; + $availableFields = []; + + if (!$noSystemColumns && $class) { + $vis = $class->getPropertyVisibility(); + foreach (self::SYSTEM_COLUMNS as $sc) { + $key = $sc === 'fullpath' ? 'path' : $sc; + + if ($types === [] && (!empty($vis[$gridType][$key]) || $gridType === 'all')) { + $availableFields[] = [ + 'key' => $sc, + 'type' => 'system', + 'label' => $sc, + 'position' => $count, + ]; + $count++; + } + } + } + + $includeBricks = !$noBrickColumns; + + if (is_array($fields)) { + foreach ($fields as $field) { + if ($field instanceof DataObject\ClassDefinition\Data\Localizedfields) { + foreach ($field->getFieldDefinitions($context) as $fd) { + if ($types === [] || in_array($fd->getFieldType(), $types)) { + $fieldConfig = $this->getFieldGridConfig($fd, $gridType, (string) $count, false, null, $class, $objectId); + if (!empty($fieldConfig)) { + $availableFields[] = $fieldConfig; + $count++; + } + } + } + } elseif ($field instanceof DataObject\ClassDefinition\Data\Objectbricks && $includeBricks) { + if (in_array($field->getFieldType(), $types)) { + $fieldConfig = $this->getFieldGridConfig($field, $gridType, (string) $count, false, null, $class, $objectId); + if (!empty($fieldConfig)) { + $availableFields[] = $fieldConfig; + $count++; + } + } else { + $allowedTypes = $field->getAllowedTypes(); + foreach ($allowedTypes as $t) { + $brickClass = DataObject\Objectbrick\Definition::getByKey($t); + $brickFields = $brickClass->getFieldDefinitions($context); + $this->appendBrickFields($field, $brickFields, $availableFields, $gridType, $count, $t, $class, $objectId); + } + } + } elseif ($types === [] || in_array($field->getFieldType(), $types)) { + $fieldConfig = $this->getFieldGridConfig($field, $gridType, (string) $count, $types !== [], null, $class, $objectId); + if (!empty($fieldConfig)) { + $availableFields[] = $fieldConfig; + $count++; + } + } + } + } + + return $availableFields; + } + + /** + * @param DataObject\ClassDefinition\Data[] $brickFields + */ + private function appendBrickFields(DataObject\ClassDefinition\Data $field, array $brickFields, array &$availableFields, string $gridType, int &$count, string $brickType, DataObject\ClassDefinition $class, int $objectId, ?array $context = null): void + { + foreach ($brickFields as $bf) { + if ($bf instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $localizedFieldDefinitions = $bf->getFieldDefinitions(); + $localizedContext = [ + 'containerKey' => $brickType, + 'fieldname' => $field->getName(), + ]; + $this->appendBrickFields($bf, $localizedFieldDefinitions, $availableFields, $gridType, $count, $brickType, $class, $objectId, $localizedContext); + } else { + if ($context) { + $context['brickfield'] = $bf->getName(); + $keyPrefix = '?' . json_encode($context) . '~'; + } else { + $keyPrefix = $brickType . '~'; + } + $fieldConfig = $this->getFieldGridConfig($bf, $gridType, (string) $count, false, $keyPrefix, $class, $objectId); + if (!empty($fieldConfig)) { + $availableFields[] = $fieldConfig; + $count++; + } + } + } + } + + private function injectCustomLayoutValues(array $fieldConfig, array $savedColumn): array + { + foreach (['width', 'locked'] as $key) { + if (isset($savedColumn[$key])) { + $fieldConfig[$key] = $savedColumn[$key]; + } + } + + if (isset($savedColumn['fieldConfig']['layout']['noteditable'])) { + $fieldConfig['layout']->setNoteditable($savedColumn['fieldConfig']['layout']['noteditable']); + } + + return $fieldConfig; + } + + private function getCalculatedColumnConfig(AttributeBagInterface $helperColumnsBag, array $config): mixed + { + try { + $existingKey = $config['fieldConfig']['key']; + $calculatedColumnConfig['key'] = $existingKey; + $calculatedColumnConfig['position'] = $config['position']; + $calculatedColumnConfig['isOperator'] = true; + $calculatedColumnConfig['attributes'] = $config['fieldConfig']['attributes']; + $calculatedColumnConfig['width'] = $config['width']; + $calculatedColumnConfig['locked'] = $config['locked']; + + $existingColumns = $helperColumnsBag->get('helpercolumns', []); + + if (isset($existingColumns[$existingKey])) { + return $calculatedColumnConfig; + } + + $newKey = '#' . uniqid('', false); + $calculatedColumnConfig['key'] = $newKey; + + $phpConfig = json_encode($config['fieldConfig']); + $phpConfig = json_decode($phpConfig); + $helperColumns = [$newKey => $phpConfig, ...$existingColumns]; + $helperColumnsBag->set('helpercolumns', $helperColumns); + + return $calculatedColumnConfig; + } catch (Exception $e) { + Logger::error((string) $e); + } + + return null; + } + + private function getFieldGridConfig(DataObject\ClassDefinition\Data $field, string $gridType, string $position, bool $force = false, ?string $keyPrefix = null, ?DataObject\ClassDefinition $class = null, ?int $objectId = null): ?array + { + $key = $keyPrefix . $field->getName(); + $config = null; + $title = !empty($field->getTitle()) ? $field->getTitle() : $field->getName(); + + if ($field instanceof DataObject\ClassDefinition\Data\Slider) { + $config['minValue'] = $field->getMinValue(); + $config['maxValue'] = $field->getMaxValue(); + $config['increment'] = $field->getIncrement(); + } + + if (method_exists($field, 'getWidth')) { + $config['width'] = $field->getWidth(); + } + if (method_exists($field, 'getHeight')) { + $config['height'] = $field->getHeight(); + } + + $visible = match ($gridType) { + 'search' => $field->getVisibleSearch(), + 'grid' => $field->getVisibleGridView(), + default => true, + }; + + if (!$field->getInvisible() && ($force || $visible)) { + $context = ['purpose' => 'gridconfig']; + if ($class) { + $context['class'] = $class; + } + if ($objectId) { + $context['object'] = DataObject::getById($objectId); + } + DataObject\Service::enrichLayoutDefinition($field, null, $context); + + $result = [ + 'key' => $key, + 'type' => $field->getFieldType(), + 'label' => $title, + 'config' => $config, + 'layout' => $field, + 'position' => $position, + ]; + + if ($field instanceof DataObject\ClassDefinition\Data\EncryptedField) { + $result['delegateDatatype'] = $field->getDelegateDatatype(); + } + + return $result; + } + + return null; + } +} diff --git a/src/Service/Grid/Dto/GridColumnConfigResult.php b/src/Service/Grid/Dto/GridColumnConfigResult.php new file mode 100644 index 00000000..4db98b4a --- /dev/null +++ b/src/Service/Grid/Dto/GridColumnConfigResult.php @@ -0,0 +1,66 @@ + $this->sortinfo, + 'availableFields' => $this->availableFields, + 'settings' => $this->settings, + 'onlyDirectChildren' => $this->onlyDirectChildren, + 'pageSize' => $this->pageSize, + 'availableConfigs' => $this->availableConfigs, + 'sharedConfigs' => $this->sharedConfigs, + 'context' => $this->context, + ]; + + if ($this->onlyUnreferenced !== null) { + $data['onlyUnreferenced'] = $this->onlyUnreferenced; + } + if ($this->language !== null) { + $data['language'] = $this->language; + } + if ($this->searchFilter !== null) { + $data['searchFilter'] = $this->searchFilter; + } + if ($this->filter !== null) { + $data['filter'] = $this->filter; + } + + return $data; + } +} diff --git a/src/Service/Grid/Dto/GridConfigData.php b/src/Service/Grid/Dto/GridConfigData.php new file mode 100644 index 00000000..9c94a6fa --- /dev/null +++ b/src/Service/Grid/Dto/GridConfigData.php @@ -0,0 +1,38 @@ +id === 0; + } +} diff --git a/src/Service/Grid/GridBatchService.php b/src/Service/Grid/GridBatchService.php new file mode 100644 index 00000000..a2e01270 --- /dev/null +++ b/src/Service/Grid/GridBatchService.php @@ -0,0 +1,384 @@ +gridHelperService->prepareAssetListingForGrid($params, $adminUser); + + return $list->loadIdList(); + } + + /** + * @return int[] + */ + public function getObjectBatchJobIds(array $params, string $locale, User $adminUser): array + { + $list = $this->gridHelperService->prepareListingForGrid($params, $locale, $adminUser); + + return $list->loadIdList(); + } + + /** + * Executes a batch metadata update on a single asset. + * + * Returns true when the asset was saved or the update was handled by an event subscriber. + * Returns false when there is no asset to update (job already completed). + * + * @throws Exception on permission denied or save failure + */ + public function executeAssetBatch(array $data, User $adminUser): bool + { + $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); + + $updateEvent = new GenericEvent(null, [ + 'data' => $data, + 'processed' => false, + ]); + + $this->eventDispatcher->dispatch($updateEvent, AdminEvents::ASSET_LIST_BEFORE_BATCH_UPDATE); + + if ($updateEvent->getArgument('processed')) { + return true; + } + + $language = null; + if (isset($data['language'])) { + $language = $data['language'] !== 'default' ? $data['language'] : null; + } + + $asset = Asset::getById((int) $data['job']); + + if (!$asset) { + Logger::debug('GridBatchService::executeAssetBatch => There is no asset left to update.'); + + return false; + } + + if (!$asset->isAllowed('publish')) { + throw new Exception("Permission denied. You don't have the rights to save this asset."); + } + + $metadata = $asset->getMetadata(null, null, false, true); + $dirty = false; + + $name = $data['name']; + $value = $data['value']; + + if ($data['valueType'] === 'object') { + $value = json_decode($value); + } + + $fieldDef = explode('~', $name); + $name = $fieldDef[0]; + if (count($fieldDef) > 1) { + $language = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); + } + + foreach ($metadata as &$em) { + if ($em['name'] == $name && $em['language'] == $language) { + try { + $dataImpl = $loader->build($em['type']); + $value = $dataImpl->getDataFromListfolderGrid($value, $em); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $em['type']); + } + $em['data'] = $value; + $dirty = true; + + break; + } + } + + if (!$dirty) { + $defaultMetadata = ['title', 'alt', 'copyright']; + if (in_array($name, $defaultMetadata)) { + $newEm = [ + 'name' => $name, + 'language' => $language, + 'type' => 'input', + 'data' => $value, + ]; + + try { + $dataImpl = $loader->build($newEm['type']); + $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $newEm['type']); + } + + $metadata[] = $newEm; + $dirty = true; + } else { + $predefined = Metadata\Predefined::getByName($name); + if ($predefined && (empty($predefined->getTargetSubtype()) + || $predefined->getTargetSubtype() === $asset->getType())) { + $newEm = [ + 'name' => $name, + 'language' => $language, + 'type' => $predefined->getType(), + 'data' => $value, + ]; + + try { + $dataImpl = $loader->build($newEm['type']); + $newEm['data'] = $dataImpl->getDataFromListfolderGrid($value, $newEm); + } catch (UnsupportedException) { + Logger::error('could not resolve metadata implementation for ' . $newEm['type']); + } + + $metadata[] = $newEm; + $dirty = true; + } + } + } + + if ($dirty) { + $metadataEvent = new GenericEvent(null, [ + 'id' => $asset->getId(), + 'metadata' => $metadata, + ]); + + $this->eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); + + $asset->setMetadataRaw($metadata); + $asset->save(); + + return true; + } + + return false; + } + + /** + * Executes a batch field update on a single DataObject. + * + * Returns true when the object was saved. + * Returns false when there is no object to update (job already completed). + * + * @throws Exception on permission denied or save failure + */ + public function executeObjectBatch(array $params, string $locale, User $adminUser): bool + { + $object = DataObject\Concrete::getById($params['job']); + + if (!$object) { + Logger::debug('GridBatchService::executeObjectBatch => There is no object left to update.'); + + return false; + } + + $requestedLanguage = $params['language']; + if (!$requestedLanguage) { + $requestedLanguage = $locale; + } elseif ($requestedLanguage === 'default') { + $requestedLanguage = $locale; + } + + $name = $params['name']; + + if (!$object->isAllowed('save') || ($name === 'published' && !$object->isAllowed('publish'))) { + throw new Exception("Permission denied. You don't have the rights to save this object."); + } + + $append = $params['append'] ?? false; + $remove = $params['remove'] ?? false; + + $className = $object->getClassName(); + $class = DataObject\ClassDefinition::getByName($className); + $value = $params['value']; + if ($params['valueType'] === 'object') { + $value = json_decode($value, true); + } + + $parts = explode('~', $name); + + if (str_starts_with($name, '~')) { + $type = $parts[1]; + $field = $parts[2]; + $keyId = $parts[3]; + + if ($type === 'classificationstore') { + $groupKeyId = explode('-', $keyId); + $groupId = (int) $groupKeyId[0]; + $keyId = (int) $groupKeyId[1]; + + $getter = 'get' . ucfirst($field); + if (method_exists($object, $getter)) { + /** @var DataObject\ClassDefinition\Data\Classificationstore $csFieldDefinition */ + $csFieldDefinition = $object->getClass()->getFieldDefinition($field); + $csLanguage = $requestedLanguage; + if (!$csFieldDefinition->isLocalized()) { + $csLanguage = 'default'; + } + + /** @var DataObject\ClassDefinition\Data\Classificationstore $fd */ + $fd = $class->getFieldDefinition($field); + $keyConfig = $fd->getKeyConfiguration($keyId); + $dataDefinition = DataObject\Classificationstore\Service::getFieldDefinitionFromKeyConfig($keyConfig); + + /** @var DataObject\Classificationstore $classificationStoreData */ + $classificationStoreData = $object->$getter(); + if ($append) { + $oldValue = $classificationStoreData->getLocalizedKeyValue($groupId, $keyId); + $value = $dataDefinition->appendData($oldValue, $value); + } + if ($remove) { + $oldValue = $classificationStoreData->getLocalizedKeyValue($groupId, $keyId); + $value = $dataDefinition->removeData($oldValue, $value); + } + $classificationStoreData->setLocalizedKeyValue( + $groupId, + $keyId, + $dataDefinition->getDataFromEditmode($value), + $csLanguage + ); + $object->markFieldDirty($field); + } + } + } elseif (count($parts) > 1) { + // check for bricks + $brickType = $parts[0]; + + if (str_contains($brickType, '?')) { + $brickDescriptor = substr($brickType, 1); + $brickDescriptor = json_decode($brickDescriptor, true); + $brickType = $brickDescriptor['containerKey']; + } + $brickKey = $parts[1]; + $brickField = DataObject\Service::getFieldForBrickType($object->getClass(), $brickType); + + $fieldGetter = 'get' . ucfirst($brickField); + $brickGetter = 'get' . ucfirst($brickType); + $valueSetter = 'set' . ucfirst($brickKey); + + $brick = $object->$fieldGetter()->$brickGetter(); + if (empty($brick)) { + $classname = '\\OpenDxp\\Model\\DataObject\\Objectbrick\\Data\\' . ucfirst($brickType); + $brickSetter = 'set' . ucfirst($brickType); + $brick = new $classname($object); + $object->$fieldGetter()->$brickSetter($brick); + } + + $brickClass = DataObject\Objectbrick\Definition::getByKey($brickType); + $field = $brickClass->getFieldDefinition($brickKey); + + $newData = $field->getDataFromEditmode($value, $object); + + if ($append) { + $valueGetter = 'get' . ucfirst($brickKey); + $existingData = $brick->$valueGetter(); + $newData = $field->appendData($existingData, $newData); + } + if ($remove) { + $valueGetter = 'get' . ucfirst($brickKey); + $existingData = $brick->$valueGetter(); + $newData = $field->removeData($existingData, $newData); + } + + $localizedFields = $brickClass->getFieldDefinition('localizedfields'); + $isLocalizedField = false; + if ($localizedFields instanceof DataObject\ClassDefinition\Data\Localizedfields && $localizedFields->getFieldDefinition($brickKey)) { + $isLocalizedField = true; + } + + if ($isLocalizedField) { + $brick->$valueSetter($newData, $params['language']); + } else { + $brick->$valueSetter($newData); + } + } else { + // everything else + $field = $class->getFieldDefinition($name); + if ($field) { + $newData = $field->getDataFromEditmode($value, $object); + + if ($append) { + $existingData = $object->{'get' . $name}(); + $newData = $field->appendData($existingData, $newData); + } + if ($remove) { + $existingData = $object->{'get' . $name}(); + $newData = $field->removeData($existingData, $newData); + } + $object->setValue($name, $newData); + } else { + // check if it is a localized field + if ($params['language']) { + $localizedField = $class->getFieldDefinition('localizedfields'); + if ($localizedField instanceof DataObject\ClassDefinition\Data\Localizedfields) { + $field = $localizedField->getFieldDefinition($name); + if ($field) { + $getter = 'get' . $name; + $setter = 'set' . $name; + $newData = $field->getDataFromEditmode($value, $object); + if ($append) { + $existingData = $object->$getter($params['language']); + $newData = $field->appendData($existingData, $newData); + } + if ($remove) { + $existingData = $object->$getter($params['language']); + $newData = $field->removeData($existingData, $newData); + } + + $object->$setter($newData, $params['language']); + } + } + } + + // seems to be a system field, this is actually only possible for the "published" field yet + if ($name === 'published') { + if ($value === 'false' || empty($value)) { + $object->setPublished(false); + } else { + $object->setPublished(true); + } + } + } + } + + // don't check for mandatory fields here + $object->setOmitMandatoryCheck(!$object->isPublished()); + $object->setUserModification($adminUser->getId()); + $object->save(); + + return true; + } +} \ No newline at end of file diff --git a/src/Service/Grid/GridColumnConfigService.php b/src/Service/Grid/GridColumnConfigService.php new file mode 100644 index 00000000..1b09cec2 --- /dev/null +++ b/src/Service/Grid/GridColumnConfigService.php @@ -0,0 +1,302 @@ +quote($classId), + ]; + + if ($searchType) { + $conditionParts[] = 'searchType = ' . $db->quote($searchType); + } + + $listing = new GridConfig\Listing(); + $listing->setOrderKey('name'); + $listing->setOrder('ASC'); + $listing->setCondition(implode(' AND ', $conditionParts)); + $listing = $listing->load(); + + $data = []; + foreach ($listing as $config) { + $data[] = $config->getObjectVars(); + } + + return $data; + } + + public function getSharedColumnConfigs(?User $user, string $classId, ?string $searchType = null): array + { + if (!$user) { + return []; + } + + $db = Db::get(); + + $userIds = [$user->getId(), ...$user->getRoles()]; + + $ids = $db->fetchFirstColumn( + 'SELECT DISTINCT c1.id FROM gridconfigs c1, gridconfig_shares s + WHERE (c1.searchType = ? AND c1.id = s.gridConfigId AND s.sharedWithUserId IN (?) AND c1.classId = ?) + UNION DISTINCT SELECT c2.id FROM gridconfigs c2 WHERE shareGlobally = 1 AND c2.classId = ? AND c2.ownerId != ?', + [$searchType, $userIds, $classId, $classId, $user->getId()], + [ParameterType::STRING, ArrayParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER] + ); + + $data = []; + if ($ids) { + $listing = new GridConfig\Listing(); + $listing->setOrderKey('name'); + $listing->setOrder('ASC'); + $listing->setCondition('id in (' . implode(',', $ids) . ')'); + + foreach ($listing->load() as $config) { + $data[] = $config->getObjectVars(); + } + } + + return $data; + } + + public function getShareSettings(int $gridConfigId): array + { + $result = [ + 'sharedUserIds' => [], + 'sharedRoleIds' => [], + ]; + + $db = Db::get(); + $allShares = $db->fetchAllAssociative( + 'SELECT s.sharedWithUserId, u.type FROM gridconfig_shares s, users u + WHERE s.sharedWithUserId = u.id AND s.gridConfigId = ?', + [$gridConfigId] + ); + + foreach ($allShares as $share) { + $result['shared' . ucfirst($share['type']) . 'Ids'][] = $share['sharedWithUserId']; + } + + foreach ($result as $idx => $value) { + $result[$idx] = $value ? implode(',', $value) : ''; + } + + return $result; + } + + /** + * @throws Exception + */ + public function updateGridConfigShares(?GridConfig $gridConfig, array $metadata, ?User $user, bool $adminCanEditAll = false): void + { + if (!$gridConfig || !$user || !$user->isAllowed('share_configurations')) { + return; + } + + $ownerMismatch = $gridConfig->getOwnerId() !== $user->getId(); + if ($ownerMismatch && (!$adminCanEditAll || !$user->isAdmin())) { + throw new Exception("don't mess with someone elses grid config"); + } + + $combinedShares = []; + if ($metadata['sharedUserIds']) { + $combinedShares = explode(',', $metadata['sharedUserIds']); + } + if ($metadata['sharedRoleIds']) { + $combinedShares = [...$combinedShares, ...explode(',', $metadata['sharedRoleIds'])]; + } + + $db = Db::get(); + $db->delete('gridconfig_shares', ['gridConfigId' => $gridConfig->getId()]); + + foreach ($combinedShares as $id) { + $share = new GridConfigShare(); + $share->setGridConfigId($gridConfig->getId()); + $share->setSharedWithUserId((int) $id); + $share->save(); + } + } + + /** + * Loads a GridConfig by ID, verifies the user has access, and returns a populated DTO. + * Returns an empty GridConfigData when no valid config ID is given or the config is not found. + * + * @throws Exception when the user has neither ownership nor a share grant + */ + public function loadVerifiedGridConfig( + int|string|null $requestedConfigId, + ?User $user, + ?string $expectedType = null, + ): GridConfigData { + if (!is_numeric($requestedConfigId) || (int) $requestedConfigId <= 0) { + return new GridConfigData(); + } + + $savedGridConfig = GridConfig::getById((int) $requestedConfigId); + if (!$savedGridConfig) { + return new GridConfigData(); + } + if ($expectedType !== null && $savedGridConfig->getType() !== $expectedType) { + return new GridConfigData(); + } + + $isShared = false; + if (!$user) { + return new GridConfigData(); + } + if (!$user->isAdmin()) { + $userIds = [$user->getId(), ...$user->getRoles()]; + $isSharedGlobally = $savedGridConfig->getOwnerId() !== $user->getId() && $savedGridConfig->isShareGlobally(); + + $db = Db::get(); + $isSharedWithUser = (bool) $db->fetchOne( + 'SELECT 1 FROM gridconfig_shares WHERE sharedWithUserId IN (?) AND gridConfigId = ?', + [$userIds, $savedGridConfig->getId()], + [ArrayParameterType::INTEGER, ParameterType::INTEGER] + ); + + $isShared = $isSharedGlobally || $isSharedWithUser; + + if (!$isShared && $savedGridConfig->getOwnerId() !== $user->getId()) { + throw new Exception('You are neither the owner of this config nor it is shared with you'); + } + } + + $config = json_decode($savedGridConfig->getConfig(), true); + foreach ($config['columns'] as &$column) { + if (array_key_exists('isOperator', $column) && $column['isOperator']) { + $colAttributes = &$column['fieldConfig']['attributes']; + SecurityHelper::convertHtmlSpecialCharsArrayKeys($colAttributes, ['label', 'attribute', 'param1']); + } + } + + return new GridConfigData( + id: $savedGridConfig->getId(), + config: $config, + name: SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getName()), + description: SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getDescription()), + sharedGlobally: $savedGridConfig->isShareGlobally(), + setAsFavourite: $savedGridConfig->isSetAsFavourite(), + isShared: $isShared, + ownerId: $savedGridConfig->getOwnerId(), + modificationDate: $savedGridConfig->getModificationDate(), + saveFilters: $savedGridConfig->isSaveFilters(), + ); + } + + public function buildBaseSettings(GridConfigData $data): array + { + $settings = $this->getShareSettings($data->id); + $settings['gridConfigId'] = $data->id; + $settings['gridConfigName'] = $data->isEmpty() ? null : $data->name; + $settings['gridConfigDescription'] = $data->isEmpty() ? null : $data->description; + $settings['shareGlobally'] = $data->isEmpty() ? null : $data->sharedGlobally; + $settings['setAsFavourite'] = $data->isEmpty() ? null : $data->setAsFavourite; + $settings['isShared'] = $data->isEmpty() || $data->isShared; + + return $settings; + } + + /** + * @throws Exception + */ + public function updateGridConfigFavourites(GridConfig $gridConfig, array $metadata, ?User $user, int $objectId = 0): void + { + if (!$user || !$user->isAllowed('share_configurations')) { + return; + } + if (!$user->isAdmin() && $gridConfig->getOwnerId() !== $user->getId()) { + throw new Exception("don't mess with someone elses grid config"); + } + + $sharedUsers = []; + if ($metadata['shareGlobally'] === false && $metadata['sharedUserIds']) { + $sharedUsers = array_map(intval(...), explode(',', $metadata['sharedUserIds'])); + } elseif ($metadata['shareGlobally'] === true) { + $users = new User\Listing(); + $users->setCondition('id = ?', $user->getId()); + foreach ($users as $u) { + $sharedUsers[] = $u->getId(); + } + } + + foreach ($sharedUsers as $id) { + if (!$this->canOverwriteFavourite($id, $gridConfig, $objectId)) { + continue; + } + + $favourite = new GridConfigFavourite(); + $favourite->setGridConfigId($gridConfig->getId()); + $favourite->setClassId($gridConfig->getClassId()); + $favourite->setObjectId($objectId); + $favourite->setOwnerId($id); + $favourite->setType($gridConfig->getType()); + $favourite->setSearchType($gridConfig->getSearchType()); + $favourite->save(); + + if ($objectId !== 0 && $this->canOverwriteFavourite($id, $gridConfig, 0)) { + $favourite->setObjectId(0); + $favourite->save(); + } + } + } + + private function canOverwriteFavourite(int $userId, GridConfig $gridConfig, int $objectId): bool + { + $existing = GridConfigFavourite::getByOwnerAndClassAndObjectId( + $userId, + $gridConfig->getClassId(), + $objectId, + $gridConfig->getSearchType() + ); + + if (!($existing instanceof GridConfigFavourite)) { + return true; + } + + $existingConfig = GridConfig::getById($existing->getGridConfigId()); + if (!($existingConfig instanceof GridConfig)) { + return true; + } + + return $existingConfig->isShareGlobally() && $existingConfig->getOwnerId() !== $userId; + } + + public function encode(null|string|array $value): string + { + if (is_array($value)) { + $value = implode(',', $value); + } + + return '"' . str_replace('"', '""', $value ?? '') . '"'; + } +} diff --git a/src/Service/Grid/GridExportService.php b/src/Service/Grid/GridExportService.php new file mode 100644 index 00000000..551932ec --- /dev/null +++ b/src/Service/Grid/GridExportService.php @@ -0,0 +1,78 @@ +getCsvFile(File::getValidFilename($fileHandle)); + + try { + $csvData = $storage->read($csvFile); + $response = new Response($csvData); + $response->headers->set('Content-Type', 'application/csv'); + $response->headers->set( + 'Content-Disposition', + HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, 'export.csv') + ); + $storage->delete($csvFile); + + return $response; + } catch (FilesystemException | UnableToReadFile) { + throw new \RuntimeException('CSV file not found'); + } + } + + /** + * @throws FilesystemException + */ + public function downloadXlsxFile(string $fileHandle): BinaryFileResponse + { + $storage = Storage::get('temp'); + $csvFile = $this->getCsvFile(File::getValidFilename($fileHandle)); + + try { + return $this->gridHelperService->createXlsxExportFile($storage, File::getValidFilename($fileHandle), $csvFile); + } catch (Exception | FilesystemException | UnableToReadFile) { + throw new \RuntimeException('XLSX file not found'); + } + } +} diff --git a/src/Service/Login/LoginPageService.php b/src/Service/Login/LoginPageService.php new file mode 100644 index 00000000..3df84524 --- /dev/null +++ b/src/Service/Login/LoginPageService.php @@ -0,0 +1,110 @@ +base() + [ + 'csrfTokenRefreshInterval' => ($gcMaxlifetime - 60) * 1000, + 'browserSupported' => $this->detectBrowser(), + 'debug' => OpenDxp::inDebugMode(), + 'includeTemplates' => [], + 'deeplink' => $this->request->query->has('deeplink'), + 'error' => $this->resolveError($tooManyAttempts), + 'login_error' => $this->authenticationUtils->getLastAuthenticationError(), + ]; + + $event = new GenericEvent(null, [ + 'parameters' => $params, + 'config' => $this->config, + 'request' => $this->request, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::LOGIN_BEFORE_RENDER); + + return $event->getArgument('parameters'); + } + + /** + * Base params shared across all login-area pages + * (lost password, 2FA, 2FA setup). + */ + public function base(): array + { + return [ + 'config' => $this->config, + 'adminSettings' => AdminConfig::get(), + 'pluginCssPaths' => $this->bundleManager->getCssPaths(), + ]; + } + + private function resolveError(?string $tooManyAttempts): ?string + { + if ($tooManyAttempts !== null) { + return SecurityHelper::convertHtmlSpecialChars($tooManyAttempts); + } + if ($this->request->query->has('auth_failed')) { + return 'error_auth_failed'; + } + if ($this->request->query->has('session_expired')) { + return 'error_session_expired'; + } + + return null; + } + + private function detectBrowser(): bool + { + $browser = new Browser(); + $version = (float) $browser->getVersion(); + + return match ($browser->getBrowser()) { + Browser::BROWSER_FIREFOX => $version >= 72, + Browser::BROWSER_CHROME => $version >= 84, + Browser::BROWSER_SAFARI => $version >= 13.1, + Browser::BROWSER_EDGE => $version >= 90, + default => false, + }; + } +} diff --git a/src/Service/Translation/AdminSearchTermResolver.php b/src/Service/Translation/AdminSearchTermResolver.php new file mode 100644 index 00000000..c87e6493 --- /dev/null +++ b/src/Service/Translation/AdminSearchTermResolver.php @@ -0,0 +1,46 @@ +setDomain(Translation::DOMAIN_ADMIN); + $translationListing->setCondition( + $translationListing->quoteIdentifier('language') . ' = ? AND ' . + $translationListing->quoteIdentifier('text') . ' LIKE ?', + [$user->getLanguage(), '%' . $searchTerm . '%'] + ); + foreach ($translationListing as $translation) { + $terms[] = $translation->getKey(); + } + } + + return $terms; + } +} diff --git a/src/Service/Workflow/WorkflowElementResolver.php b/src/Service/Workflow/WorkflowElementResolver.php new file mode 100644 index 00000000..5967f2fe --- /dev/null +++ b/src/Service/Workflow/WorkflowElementResolver.php @@ -0,0 +1,82 @@ + Document::getById($cid), + 'asset' => Asset::getById($cid), + 'object' => ConcreteObject::getById($cid), + default => null, + }; + + if ($element === null) { + throw new Exception('Cannot load element ' . $cid . ' of type \'' . $ctype . '\''); + } + + $element = $this->getLatestVersion($element); + $element->setUserModification((int) $this->userContext->getAdminUser()->getId()); + + return $element; + } + + private function getLatestVersion(ConcreteObject|Document|Asset $element): ConcreteObject|Document|Asset + { + if ( + $element instanceof Document\Folder + || $element instanceof Asset\Folder + || $element instanceof Document\Hardlink + || $element instanceof Document\Link + ) { + return $element; + } + + if ($element instanceof Document\PageSnippet) { + $latestVersion = $element->getLatestVersion(); + if ($latestVersion) { + $latestDoc = $latestVersion->loadData(); + if ($latestDoc instanceof Document\PageSnippet) { + $element = $latestDoc; + } + } + } + + if ($element instanceof ConcreteObject) { + $latestVersion = $element->getLatestVersion(); + if ($latestVersion) { + $latestObj = $latestVersion->loadData(); + if ($latestObj instanceof ConcreteObject) { + $element = $latestObj; + } + } + } + + return $element; + } +} diff --git a/tests/Model/Permissions/AbstractPermissionTest.php b/tests/Model/Permissions/AbstractPermissionTest.php index 2bbe04b8..7a99e63f 100644 --- a/tests/Model/Permissions/AbstractPermissionTest.php +++ b/tests/Model/Permissions/AbstractPermissionTest.php @@ -18,6 +18,7 @@ namespace OpenDxp\Bundle\AdminBundle\Tests\Model\Controller; use Codeception\Stub; +use OpenDxp\Bundle\AdminBundle\Service\AdminUserContextInterface; use OpenDxp\Bundle\AdminBundle\Service\ElementService; use OpenDxp\Config; use OpenDxp\Model\User; @@ -29,11 +30,12 @@ abstract class AbstractPermissionTest extends ModelTestCase { - protected function buildController(string $classname, User $user): mixed + protected function buildElementService(User $user): ElementService { $openDxpModule = $this->getModule('\\'.OpenDxp::class); $config = $openDxpModule->grabService(Config::class); - $elementService = Stub::construct( + + return Stub::construct( ElementService::class, [ Stub::makeEmpty(UrlGeneratorInterface::class), @@ -45,8 +47,22 @@ protected function buildController(string $classname, User $user): mixed ]), ] ); + } + + protected function buildUserContext(User $user): AdminUserContextInterface + { + return Stub::makeEmpty(AdminUserContextInterface::class, [ + 'getAdminUser' => function () use ($user) { + return $user; + }, + ]); + } + + protected function buildController(string $classname, User $user, array $extraConstructorArgs = []): mixed + { + $elementService = $this->buildElementService($user); - return Stub::construct($classname, [$elementService], [ + return Stub::construct($classname, [$elementService, ...$extraConstructorArgs], [ 'getAdminUser' => function () use ($user) { return $user; }, @@ -63,4 +79,4 @@ protected function buildController(string $classname, User $user): mixed } abstract public function testTreeGetChildrenById(): void; -} +} \ No newline at end of file diff --git a/tests/Model/Permissions/ModelAssetPermissionsTest.php b/tests/Model/Permissions/ModelAssetPermissionsTest.php index cb53fede..28faaf00 100644 --- a/tests/Model/Permissions/ModelAssetPermissionsTest.php +++ b/tests/Model/Permissions/ModelAssetPermissionsTest.php @@ -18,6 +18,9 @@ namespace OpenDxp\Bundle\AdminBundle\Tests\Model\Controller; use OpenDxp\Bundle\AdminBundle\Controller\Admin\Asset\AssetController; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\GetAssetChildren\GetAssetChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\GetAssetChildren\GetAssetChildrenPayload; +use OpenDxp\Bundle\AdminBundle\Service\Asset\AssetGridService; use OpenDxp\Model\Asset; use OpenDxp\Model\Property; use OpenDxp\Model\User; @@ -290,19 +293,21 @@ public function testTreeGetChildrenById(): void protected function doTestTreeGetChildrenById(Asset $element, User $user, array $expectedChildren): void { - $controller = $this->buildController(AssetController::class, $user); + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $handler = new GetAssetChildrenHandler($userContext, $elementService, new EventDispatcher()); + + $controller = $this->buildController(AssetController::class, $user, [ + (new \ReflectionClass(AssetGridService::class))->newInstanceWithoutConstructor(), + ]); $request = new Request([ 'node' => $element->getId(), 'limit' => 100, 'view' => 0, ]); - $eventDispatcher = new EventDispatcher(); - $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher - ); + $responseData = $controller->treeGetChildrenByIdAction(GetAssetChildrenPayload::fromRequest($request), $handler); $responsePaths = []; $responseData = json_decode($responseData->getContent(), true); foreach ($responseData['nodes'] as $node) { diff --git a/tests/Model/Permissions/ModelDataObjectPermissionsTest.php b/tests/Model/Permissions/ModelDataObjectPermissionsTest.php index 9cfd969a..a3c37adf 100644 --- a/tests/Model/Permissions/ModelDataObjectPermissionsTest.php +++ b/tests/Model/Permissions/ModelDataObjectPermissionsTest.php @@ -19,6 +19,10 @@ use Exception; use OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject\DataObjectController; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\GetDataObjectChildren\GetDataObjectChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetChildrenById\TreeGetChildrenByIdHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetChildrenById\TreeGetChildrenByIdPayload; +use OpenDxp\Bundle\AdminBundle\Service\Element\SessionService; use OpenDxp\Model\DataObject; use OpenDxp\Model\User; use OpenDxp\Tests\Support\Util\TestHelper; @@ -216,18 +220,22 @@ protected function doTestTreeGetChildrenById( User $user, ?array $expectedChildren ): void { - $controller = $this->buildController(DataObjectController::class, $user); + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $childrenHandler = new GetDataObjectChildrenHandler($userContext, $elementService); + $handler = new TreeGetChildrenByIdHandler($userContext, $elementService, $childrenHandler, new EventDispatcher()); - $request = new Request([ - 'node' => $element->getId(), + $controller = $this->buildController(DataObjectController::class, $user, [ + (new \ReflectionClass(SessionService::class))->newInstanceWithoutConstructor(), ]); - $eventDispatcher = new EventDispatcher(); + + $request = new Request(['node' => $element->getId()]); try { TestHelper::callMethod($controller, 'checkPermission', ['objects']); $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher + TreeGetChildrenByIdPayload::fromRequest($request), + $handler, ); } catch (Exception $e) { if (is_null($expectedChildren)) { diff --git a/tests/Model/Permissions/ModelDocumentPermissionsTest.php b/tests/Model/Permissions/ModelDocumentPermissionsTest.php index c2d0f259..80cfa40f 100644 --- a/tests/Model/Permissions/ModelDocumentPermissionsTest.php +++ b/tests/Model/Permissions/ModelDocumentPermissionsTest.php @@ -18,6 +18,9 @@ namespace OpenDxp\Bundle\AdminBundle\Tests\Model\Controller; use OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentController; +use OpenDxp\Bundle\AdminBundle\Handler\Document\GetDocumentChildren\GetDocumentChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\TreeGetDocumentChildren\TreeGetDocumentChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\TreeGetDocumentChildren\TreeGetDocumentChildrenPayload; use OpenDxp\Model\Document; use OpenDxp\Model\Document\Page; use OpenDxp\Model\User; @@ -162,6 +165,11 @@ protected function prepareUsers(): void protected function doTestTreeGetChildrenById(Document $element, User $user, array $expectedChildren): void { + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $childrenHandler = new GetDocumentChildrenHandler($elementService); + $handler = new TreeGetDocumentChildrenHandler($userContext, $elementService, $childrenHandler, new EventDispatcher()); + $controller = $this->buildController(DocumentController::class, $user); $request = new Request([ @@ -169,12 +177,8 @@ protected function doTestTreeGetChildrenById(Document $element, User $user, arra 'limit' => 100, 'view' => 0, ]); - $eventDispatcher = new EventDispatcher(); - $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher - ); + $responseData = $controller->treeGetChildrenByIdAction($handler, TreeGetDocumentChildrenPayload::fromRequest($request)); $responsePaths = []; $responseData = json_decode($responseData->getContent(), true);