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 = '' . implode('', array_map(static fn ($item) => '- ' . $item . '
', $e->getSubItems())) . '
';
- }
-
- $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 = '' . implode('', array_map(static fn ($item) => '- ' . $item . '
', $e->getSubItems())) . '
';
+ }
- $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 = '' . implode('', array_map(static fn ($item) => '- ' . $item . '
', $e->getSubItems())) . '
';
}
- $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);