diff --git a/config/event_listeners.yaml b/config/event_listeners.yaml index 83bff29c..2aea1d29 100644 --- a/config/event_listeners.yaml +++ b/config/event_listeners.yaml @@ -23,6 +23,8 @@ services: OpenDxp\Bundle\AdminBundle\EventListener\AdminExceptionListener: ~ + OpenDxp\Bundle\AdminBundle\EventListener\HtmlContentTypeResponseListener: ~ + # # GRID CONFIG CLEANUP # diff --git a/config/legacy_services.yaml b/config/legacy_services.yaml new file mode 100644 index 00000000..d7ee2efd --- /dev/null +++ b/config/legacy_services.yaml @@ -0,0 +1,9 @@ +# Deprecated service id aliases kept for BC +services: + OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface: + alias: OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface + deprecated: + package: 'open-dxp/admin-bundle' + version: '1.4' + message: 'The "%alias_id%" service id is deprecated, use "OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface" instead. It will be removed in 2.0.' + 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..f258e0c0 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 @@ -78,7 +79,7 @@ services: # OpenDxp\Model\DataObject\ClassDefinition\PreviewGeneratorInterface: - class: OpenDxp\Bundle\AdminBundle\Service\PreviewGenerator + class: OpenDxp\Bundle\AdminBundle\Generator\PreviewGenerator # # Twig Extensions @@ -91,6 +92,7 @@ services: # OpenDxp\Bundle\AdminBundle\Service\Workflow\ActionsButtonService: ~ + OpenDxp\Bundle\AdminBundle\Resolver\Workflow\WorkflowElementResolver: ~ # # Admin Config Services @@ -100,5 +102,99 @@ services: # # Element Services # - OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface: - class: OpenDxp\Bundle\AdminBundle\Service\ElementService + OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface: + class: OpenDxp\Bundle\AdminBundle\Service\Element\ElementService + + OpenDxp\Bundle\AdminBundle\Generator\CustomLoginUrlGenerator: ~ + + OpenDxp\Bundle\AdminBundle\Service\Admin\AdminUserContextInterface: + class: OpenDxp\Bundle\AdminBundle\Service\Admin\AdminUserContext + + OpenDxp\Bundle\AdminBundle\Session\SessionIdentityInterface: + class: OpenDxp\Bundle\AdminBundle\Session\SessionIdentity + + OpenDxp\Bundle\AdminBundle\Session\Gateway\: + resource: '../src/Session/Gateway' + + OpenDxp\Bundle\AdminBundle\Service\Element\ElementDraftService: ~ + OpenDxp\Bundle\AdminBundle\Service\Element\EditLockService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Login\LoginPageService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Notification\NotificationFilterParser: ~ + + OpenDxp\Bundle\AdminBundle\Mapper\Document\DocumentPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Coordinator\Document\DocumentPersistenceCoordinator: ~ + + OpenDxp\Bundle\AdminBundle\Mapper\DataObject\DataObjectPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Coordinator\DataObject\DataObjectPersistenceCoordinator: ~ + OpenDxp\Bundle\AdminBundle\Service\DataObject\DataObjectGridService: ~ + OpenDxp\Bundle\AdminBundle\Mapper\DataObject\ClassificationstoreKeyConfigMapper: ~ + + OpenDxp\Bundle\AdminBundle\Mapper\Asset\AssetPayloadMapper: ~ + OpenDxp\Bundle\AdminBundle\Coordinator\Asset\AssetPersistenceCoordinator: ~ + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetUploadService: ~ + OpenDxp\Bundle\AdminBundle\Service\Asset\AssetGridService: ~ + OpenDxp\Bundle\AdminBundle\Resolver\Asset\ImageThumbnailResolver: ~ + + OpenDxp\Bundle\AdminBundle\Service\Grid\GridColumnConfigService: ~ + OpenDxp\Bundle\AdminBundle\Resolver\Grid\AssetGridColumnConfigResolver: ~ + OpenDxp\Bundle\AdminBundle\Resolver\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\TranslationQueryService: ~ + OpenDxp\Bundle\AdminBundle\Resolver\Translation\AdminSearchTermResolver: ~ + + OpenDxp\Bundle\AdminBundle\Service\Portal\DashboardService: ~ + + OpenDxp\Bundle\AdminBundle\Service\Admin\AdminSettingsService: ~ + OpenDxp\Bundle\AdminBundle\Service\Admin\AdminStatisticsService: ~ + + # + # Repositories + # + OpenDxp\Bundle\AdminBundle\Repository\: + resource: '../src/Repository' + + # + # Factories + # + OpenDxp\Bundle\AdminBundle\Factory\: + resource: '../src/Factory' + + # + # 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/docs/00_Architecture/README.md b/docs/00_Architecture/README.md index 693cee7b..177e010c 100644 --- a/docs/00_Architecture/README.md +++ b/docs/00_Architecture/README.md @@ -26,28 +26,110 @@ events that are specific to the admin UI lifecycle. ``` src/ -├── Command/ # CLI commands (e.g. cache warm-up, admin user management) +├── Attribute/ # PHP attributes driving centralized listener behavior (e.g. AsHtmlContentTypeResponse) +├── Command/ # CLI commands (e.g. cache warm-up, admin user management) ├── Controller/ -│ ├── Admin/ # Admin controllers, one subfolder per element type +│ ├── Admin/ # Admin controllers, one subfolder per element type, thin pass-throughs to Handlers │ │ ├── Asset/ │ │ ├── DataObject/ -│ │ ├── Document/ # DocumentController — tree, site panel, site custom settings +│ │ ├── Document/ # DocumentController: tree, site panel, site custom settings │ │ └── ... -│ └── Traits/ # Shared controller traits -├── DependencyInjection/ # Bundle extension + compiler passes -├── Enum/ # PHP enums for typed config values -├── Event/ # Admin event classes + AdminEvents constants -├── EventListener/ # Symfony event subscribers (internal) -├── Helper/ # Stateless service helpers (e.g. GridHelperService) -├── Model/ # Admin-only models (GridConfig, GridConfigShare, etc.) -├── Perspective/ # Perspective resolution and serialization -├── Security/ # Admin authentication, authenticators, security tokens -├── Service/ # Application services (grid data, workflow) -├── System/ # System-level services -├── Translation/ # Admin translation handling -└── Twig/ # Twig extensions for admin templates +│ └── Traits/ # Shared controller traits +├── CustomView/ # Custom document/asset/object tree view configuration +├── DataObject/ # Grid column config element definitions shared by DataObject grid Handlers +├── DependencyInjection/ # Bundle extension + compiler passes +├── Dto/ # Data transfer objects (grid configs, site custom settings, admin bootstrap settings) +├── Enricher/ # Adds admin-only data (permissions, admin styles) to tree/editor payloads +├── Enum/ # PHP enums for typed config values +├── Event/ # Admin event classes + AdminEvents constants +├── EventListener/ # Symfony event subscribers (internal) +├── Exception/ # Admin-specific exceptions (e.g. AdminOperationFailedException) +├── Factory/ # Factories for services that can't be constructed via plain DI +├── GDPR/ # GDPR data provider integrations (export/anonymize document, asset, object, mail data) +├── Handler/ # Business logic, one invokable class per action, see below +├── Helper/ # Stateless service helpers (e.g. GridHelperService) +├── Http/ # ExtJsValueResolver: resolves Payload::fromRequest() as a controller argument +├── Model/ # Admin-only models (GridConfig, GridConfigShare, etc.) +├── Payload/ # Shared request DTOs consumed by Handlers; action-specific ones live next to their Handler +├── Perspective/ # Perspective resolution and serialization +├── Repository/ # Persistence for admin-only data (e.g. per-user dashboard config), keyed by explicit params, no bound state +├── Security/ # Admin authentication, authenticators, security tokens +├── Service/ # Application services shared across Handlers (grid data, workflow, element resolution) +├── Session/ # Session access boundary: SessionGatewayInterface/SessionIdentityInterface and their Gateway/ implementations, see below +├── System/ # System-level services +├── Translation/ # Admin translation handling +└── Twig/ # Twig extensions for admin templates ``` +## Handler / Payload / Service Pattern + +Admin controller actions contain no business logic. Each action is a thin pass-through that +resolves a typed request Payload, invokes a Handler, and maps the result to JSON: + +```php +#[Route('/save', name: 'save', methods: ['PUT'])] +public function saveAction( + SaveClassDefinitionPayload $payload, + SaveClassDefinitionHandler $handler, +): JsonResponse { + return $this->apiJson($handler($payload)); +} +``` + +`AdminAbstractController::apiJson()` serializes a Handler's Result into the wire envelope +`{success: true, ...resultProperties}`. Pass `rootProperty: 'nodes'` when the established +wire contract for an endpoint is the bare value of a single Result property instead of the +enveloped shape. For a void Handler with nothing to report, use `apiOk()` (`{success: true}`). +Never build the response body by hand with `adminJson()` in a controller action, if a +Result's shape does not match what an endpoint needs to return, change the Result and the +Handler, not the controller. + +| Layer | Location | Responsibility | +|------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Payload | `src/Payload/Common/` or colocated with a Handler | Typed request DTO. `fromRequest()` is resolved automatically by `ExtJsValueResolver` before the controller action runs, the only place request data is read. | +| Handler | `src/Handler/**` | One invokable (`__invoke`) class per action. Holds all business logic. Transport-agnostic, never receives the HTTP `Request`. | +| Result | colocated with its Handler | Typed return value, mapped to a JSON response by the controller. | +| Service | `src/Service/**` | Logic shared across multiple Handlers (grid data assembly, workflow resolution, element resolution). | +| Repository | `src/Repository/**` | Persistence only. | +| Enricher | `src/Enricher/**` | Adds admin-only presentation data (permissions, admin styles) to tree/editor node data, shared across Handlers. | + +**Error handling:** + +Handlers always throw on failure, they never return a `success: false` or `errorMessage` field. + +`AdminExceptionListener` converts any exception raised during an admin XHR request into `{success: false, message, ...}` JSON at the exception's real HTTP status. + +### AdminOperationFailedException +For an expected, recoverable business-rule failure that the admin UI handles locally as `success: false` at HTTP 200, not a real HTTP error, +throw `OpenDxp\Bundle\AdminBundle\Exception\AdminOperationFailedException` instead of a generic exception; the listener maps it to 200 explicitly rather than a 4xx/5xx status. + +### ElementLockedException +Some exceptions carry their own dedicated response shape instead of the generic `{success: false, message}` body: `OpenDxp\Bundle\AdminBundle\Exception\ElementLockedException` is mapped by the listener to a 200 response with an `editlock` payload, +which the admin UI uses to show the lock dialog. + +## Session Access + +Controllers and Payloads never touch Symfony's HTTP session (`SessionInterface`, `AttributeBagInterface`, `Request::getSession()`), not even to read it. +A Payload only carries values taken from the current `Request`; anything session-derived is looked up by the Handler itself, at the point it is actually needed. Only two kinds of classes may reference session storage: + +1. **A Gateway** implementing `OpenDxp\Bundle\AdminBundle\Session\SessionGatewayInterface`, living in `src/Session/Gateway/`. Each Gateway wraps exactly one session bag (see the `BAG_*` constants on `SessionGatewayInterface` for the full list of bags in use) behind a small, domain-named API, e.g. `CopySessionGateway::rememberParentId()`, not a raw `get()`/`set()`. Handlers and Services inject the Gateway they need. +2. **`OpenDxp\Bundle\AdminBundle\Session\SessionIdentityInterface`**, for the narrower case of needing only the current session id as a correlation token (lock ownership, bootstrap settings), not a session value. Carries no mutable state, so it may be injected anywhere. + +Controller actions never contain session code, not even a call into a Gateway. Since that means an endpoint's session footprint is otherwise invisible without opening its Handler, mark the action with an attribute instead: + +```php +#[SessionGatewayAware(CopySessionGateway::class)] +#[Route('/copy', name: 'opendxp_admin_asset_copy', methods: ['POST'])] +public function copyAction(CopyAssetPayload $payload, CopyAssetHandler $handler): JsonResponse +{ + $handler($payload); + + return $this->apiOk(); +} +``` + +Use `#[SessionIdentityAware]` (no argument) when only `SessionIdentityInterface` is involved. + ## Frontend Assets JavaScript and CSS live in `public/js/` and `public/css/`. The bundle uses @@ -55,8 +137,8 @@ JavaScript and CSS live in `public/js/` and `public/css/`. The bundle uses (`webpack.config.js`) for asset compilation. Key JS entry points: -- `public/js/opendxp/events.js` — all frontend event constants -- `public/js/opendxp/document/tree.js` — document tree rendering +- `public/js/opendxp/events.js`: all frontend event constants +- `public/js/opendxp/document/tree.js`: document tree rendering ## Configuration Namespace @@ -68,4 +150,4 @@ opendxp_admin: custom_admin_route_name: ~ user: default_key_bindings: ~ -``` \ No newline at end of file +``` diff --git a/docs/10_Extension_Points/01_Events.md b/docs/10_Extension_Points/01_Events.md index 78d2e718..c64f8409 100644 --- a/docs/10_Extension_Points/01_Events.md +++ b/docs/10_Extension_Points/01_Events.md @@ -37,27 +37,30 @@ Symfony autoconfiguration registers the subscriber automatically when `EventSubs ### Document Events -| Constant | Event Class | Description | -|----------------------------------|-------------|----------------------------------------------| -| `DOCUMENT_GET_PRE_SEND_DATA` | — | Before document data is sent to the frontend | -| `DOCUMENT_LIST_BEFORE_LIST_LOAD` | — | Before document listing is loaded | -| `DOCUMENT_LIST_AFTER_LIST_LOAD` | — | After document listing is loaded | +| Constant | Event Class | Description | +|----------------------------------|-------------|------------------------------------------------| +| `DOCUMENT_GET_PRE_SEND_DATA` | — | Before document data is sent to the frontend | +| `DOCUMENT_LIST_BEFORE_LIST_LOAD` | — | Before document listing is loaded | +| `DOCUMENT_LIST_AFTER_LIST_LOAD` | — | After document listing is loaded | +| `DOCUMENT_GET_IS_LOCKED` | — | Before the edit lock is handled for a document | ### Asset Events -| Constant | Event Class | Description | -|-------------------------------|-------------|-------------------------------------------| -| `ASSET_GET_PRE_SEND_DATA` | — | Before asset data is sent to the frontend | -| `ASSET_LIST_BEFORE_LIST_LOAD` | — | Before asset listing is loaded | -| `ASSET_LIST_AFTER_LIST_LOAD` | — | After asset listing is loaded | +| Constant | Event Class | Description | +|-------------------------------|-------------|----------------------------------------------| +| `ASSET_GET_PRE_SEND_DATA` | — | Before asset data is sent to the frontend | +| `ASSET_LIST_BEFORE_LIST_LOAD` | — | Before asset listing is loaded | +| `ASSET_LIST_AFTER_LIST_LOAD` | — | After asset listing is loaded | +| `ASSET_GET_IS_LOCKED` | — | Before the edit lock is handled for an asset | ### Object Events -| Constant | Event Class | Description | -|--------------------------------|-------------|-------------------------------------------------| -| `OBJECT_GET_PRE_SEND_DATA` | — | Before data object data is sent to the frontend | -| `OBJECT_LIST_BEFORE_LIST_LOAD` | — | Before object listing is loaded | -| `OBJECT_LIST_AFTER_LIST_LOAD` | — | After object listing is loaded | +| Constant | Event Class | Description | +|--------------------------------|-------------|---------------------------------------------------| +| `OBJECT_GET_PRE_SEND_DATA` | — | Before data object data is sent to the frontend | +| `OBJECT_LIST_BEFORE_LIST_LOAD` | — | Before object listing is loaded | +| `OBJECT_LIST_AFTER_LIST_LOAD` | — | After object listing is loaded | +| `OBJECT_GET_IS_LOCKED` | — | Before the edit lock is handled for a data object | ### Element Style Events diff --git a/docs/90_Testing/README.md b/docs/90_Testing/README.md index 16db6eed..4da6273b 100644 --- a/docs/90_Testing/README.md +++ b/docs/90_Testing/README.md @@ -6,7 +6,7 @@ This bundle uses **Codeception** for integration tests and plain **PHPUnit** for ``` tests/ -├── Model/ # Integration tests — require a running OpenDXP environment +├── Model/ # Integration tests, require a running OpenDXP environment │ ├── GridHelper/ # Grid/listing logic tests │ └── Permissions/ # Permission model tests └── Support/ @@ -21,7 +21,7 @@ Configuration: `codeception.dist.yml` in the bundle root. | Base Class | From | Use When | |--------------------------------------------|--------------------|-------------------------------------------------------------------------| | `OpenDxp\Tests\Support\Test\ModelTestCase` | `open-dxp/opendxp` | Test needs DB, models, or a running OpenDXP (documents, objects, sites) | -| `PHPUnit\Framework\TestCase` | `PHPUnit` | Pure unit test — event classes, enums, services without I/O | +| `PHPUnit\Framework\TestCase` | `PHPUnit` | Pure unit test: event classes, enums, services without I/O | ## When to Write Which Type @@ -33,7 +33,10 @@ Configuration: `codeception.dist.yml` in the bundle root. **Integration tests** (`ModelTestCase`): - Permission checks against real user/role objects - Grid helpers that run queries -- Controller-level behaviour that reads/writes models +- Handler/Service-level behaviour that reads/writes models. Business logic lives in + `src/Handler/**` and `src/Service/**`, not in controllers, so this is where most + integration coverage belongs; test the Handler/Service directly rather than through + the controller action where possible ## Unit Test Example 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/helpers.js b/public/js/opendxp/helpers.js index 8bb7b9b2..32fbe06c 100644 --- a/public/js/opendxp/helpers.js +++ b/public/js/opendxp/helpers.js @@ -1152,6 +1152,7 @@ opendxp.helpers.uploadDialog = function (url, filename, success, failure, descri request.addEventListener("error", errorWrapper, false); request.addEventListener("abort", errorWrapper, false); request.open('POST', url); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); request.send(data); }); diff --git a/public/js/opendxp/layout/viewport.js b/public/js/opendxp/layout/viewport.js new file mode 100644 index 00000000..b8f37c9f --- /dev/null +++ b/public/js/opendxp/layout/viewport.js @@ -0,0 +1,73 @@ +/** + * OpenDXP + * + * This source file is licensed under the GNU General Public License version 3 (GPLv3). + * + * Full copyright and license information is available in + * LICENSE.md which is distributed with this source code. + * + * @copyright Copyright (c) OpenDXP (https://www.opendxp.io) + * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) + */ + +/** + * Generic extension point for plugins/bundles that need a permanent region docked to the + * main application viewport (border layout: north / south / east / west), next to the + * center region ("opendxp_body") that hosts the tree panels and the main tab panel. + * + * This namespace intentionally has no knowledge of any specific plugin - it only manages + * the viewport's border-layout regions on their behalf. + */ +opendxp.registerNS("opendxp.layout.viewport"); +opendxp.layout.viewport = { + + /** + * @private + */ + regions: {}, + + addRegion: function (config) { + var viewport = Ext.getCmp("opendxp_viewport"); + + if (!viewport) { + throw new Error("opendxp.layout.viewport.addRegion: viewport is not ready yet"); + } + + if (!config || !config.id) { + throw new Error("opendxp.layout.viewport.addRegion: config.id is required"); + } + + if (!config.region || config.region === "center") { + throw new Error("opendxp.layout.viewport.addRegion: config.region must be one of north, south, east, west (\"center\" is reserved for opendxp_body)"); + } + + if (this.regions[config.id]) { + return this.regions[config.id]; + } + + var region = viewport.add(config); + this.regions[config.id] = region; + + region.on("destroy", function () { + delete this.regions[config.id]; + }.bind(this)); + + return region; + }, + + removeRegion: function (id, destroy) { + var viewport = Ext.getCmp("opendxp_viewport"); + var region = this.regions[id]; + + if (!viewport || !region) { + return; + } + + viewport.remove(region, !!destroy); + delete this.regions[id]; + }, + + getRegion: function (id) { + return this.regions[id] || null; + } +}; 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/public/js/opendxp/object/tags/hotspotimage.js b/public/js/opendxp/object/tags/hotspotimage.js index 20c82081..0697d8dc 100644 --- a/public/js/opendxp/object/tags/hotspotimage.js +++ b/public/js/opendxp/object/tags/hotspotimage.js @@ -352,12 +352,14 @@ opendxp.object.tags.hotspotimage = Class.create(opendxp.object.tags.image, { height = body.getHeight() - 10; } - var path = Routing.generate('opendxp_admin_asset_getimagethumbnail', Ext.merge({ + var params = Ext.merge({ id: this.data.id, width: width, height: height, contain: true - }, this.crop)); + }, this.crop); + + var path = Routing.generate('opendxp_admin_asset_getimagethumbnail', params); body.setStyle({ backgroundImage: "url(" + path + ")", @@ -365,7 +367,7 @@ opendxp.object.tags.hotspotimage = Class.create(opendxp.object.tags.image, { backgroundRepeat: "no-repeat" }); - this.getFileInfo(path); + this.getFileInfo(params); } else { this.fileinfo = null; body.setStyle({}); @@ -377,13 +379,10 @@ opendxp.object.tags.hotspotimage = Class.create(opendxp.object.tags.image, { this.showPreview(); }, - getFileInfo: function (path) { + getFileInfo: function (params) { if (!this.fileinfo) { Ext.Ajax.request({ - url: path, - params: { - fileinfo: 1 - }, + url: Routing.generate('opendxp_admin_asset_getimagethumbnail_fileinfo', params), success: function (response) { this.fileinfo = Ext.decode(response.responseText); this.showPreview(); diff --git a/public/js/opendxp/startup.js b/public/js/opendxp/startup.js index 7d86a584..6e8c5c6a 100644 --- a/public/js/opendxp/startup.js +++ b/public/js/opendxp/startup.js @@ -612,11 +612,12 @@ Ext.onReady(function () { try { opendxp.viewport = Ext.create('Ext.container.Viewport', { id: 'opendxp_viewport', - layout: 'fit', + layout: 'border', items: [ { xtype: 'panel', id: 'opendxp_body', + region: 'center', cls: 'opendxp_body', layout: 'border', border: false, diff --git a/src/Attribute/AsHtmlContentTypeResponse.php b/src/Attribute/AsHtmlContentTypeResponse.php new file mode 100644 index 00000000..d8fd32ea --- /dev/null +++ b/src/Attribute/AsHtmlContentTypeResponse.php @@ -0,0 +1,22 @@ + $gateway + */ + public function __construct(public readonly string $gateway) {} +} \ No newline at end of file diff --git a/src/Attribute/SessionIdentityAware.php b/src/Attribute/SessionIdentityAware.php new file mode 100644 index 00000000..d3b49b45 --- /dev/null +++ b/src/Attribute/SessionIdentityAware.php @@ -0,0 +1,26 @@ +value)] +class AssetController extends ElementControllerBase { - use AdminStyleTrait; - use ElementEditLockHelperTrait; - use ApplySchedulerDataTrait; - use UserNameTrait; - final const string PDF_MIMETYPE = 'application/pdf'; + #[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->apiJson($handler($payload), rootProperty: '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( + GetTreeRootPayload $payload, + GetTreeRootHandler $handler, + ): JsonResponse { + return parent::treeGetRootAction($payload, $handler); } #[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); } + #[SessionIdentityAware] #[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(); + public function getDataByIdAction( + GetAssetDataPayload $payload, + GetAssetDataHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); } #[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(), - ]); - } + public function treeGetChildrenByIdAction( + Request $request, + TreeGetAssetChildrenPayload $payload, + ): Response { + return match ($payload->hasPagination()) { + true => $this->forward(self::class . '::treeGetChildrenByIdPaginatedAction', [], $request->query->all()), + false => $this->forward(self::class . '::treeGetChildrenByIdListAction', [], $request->query->all()), + }; } - #[Route('/add-asset-compatibility', name: 'opendxp_admin_asset_addassetcompatibility', methods: ['POST'])] - public function addAssetCompatibilityAction(Request $request, Config $config): 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 = ''; - } - - while (true) { - if (Asset\Service::pathExists($targetPath . '/' . $filename)) { - $filename = $originalFilename . '_' . $count . $originalFileextension; - $count++; - } else { - return $filename; - } - } + #[Route('/tree-get-children-by-id-paginated', name: 'opendxp_admin_asset_treegetchildrenbyidpaginated', methods: ['GET'])] + public function treeGetChildrenByIdPaginatedAction( + TreeGetAssetChildrenPayload $payload, + TreeGetAssetChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } - /** - * @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); - - if ($newType !== $asset->getType()) { - 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, - ]); - - // 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'); + #[Route('/tree-get-children-by-id-list', name: 'opendxp_admin_asset_treegetchildrenbyidlist', methods: ['GET'])] + public function treeGetChildrenByIdListAction( + TreeGetAssetChildrenPayload $payload, + TreeGetAssetChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'nodes'); } #[Route('/add-folder', name: 'opendxp_admin_asset_addfolder', methods: ['POST'])] - public function addFolderAction(Request $request): JsonResponse + public function addFolderAction(CreateAssetFolderPayload $payload, CreateAssetFolderHandler $handler): 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'); - } + $handler($payload); - return $this->adminJson(['success' => $success]); + return $this->apiOk(); } #[Route('/delete', name: 'opendxp_admin_asset_delete', methods: ['DELETE'])] - public function deleteAction(Request $request): JsonResponse + public function deleteAction(DeleteAssetPayload $payload, DeleteAssetHandler $handler): JsonResponse { - $type = $request->request->get('type'); + $result = $handler($payload); - if ($type === 'children') { - $parentAsset = Asset::getById((int) $request->request->get('id')); - - $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 !== null) { + return $this->apiJson($result); } - throw $this->createAccessDeniedHttpException(); - } - - /** - * @throws Exception - */ - #[Override] - protected function getTreeNodeConfig(ElementInterface $element): array - { - return $this->elementService->getElementTreeNodeConfig($element); + return $this->apiOk(); } - /** - * @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 $handler): 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 '); - } - - return $this->adminJson($data); + return $this->apiJson($handler($payload)); } - /** - * @throws Exception - */ + #[SessionIdentityAware] #[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 $handler, 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 - ); - } - - 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, - ] - ); - } - - #[Route('/download', name: 'opendxp_admin_asset_download', methods: ['GET'])] - public function downloadAction(Request $request): StreamedResponse - { - $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()); - } - - 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(), - ]); - } - - #[Route('/download-image-thumbnail', name: 'opendxp_admin_asset_downloadimagethumbnail', methods: ['GET'])] - public function downloadImageThumbnailAction(Request $request): BinaryFileResponse - { - $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'); + return $this->apiJson($handler($payload)); } - #[Route('/get-asset', name: 'opendxp_admin_asset_getasset', methods: ['GET'])] - public function getAssetAction(Request $request): StreamedResponse + #[Route('/clear-thumbnail', name: 'opendxp_admin_asset_clearthumbnail', methods: ['POST'])] + public function clearThumbnailAction(ClearAssetThumbnailPayload $payload, ClearAssetThumbnailHandler $handler): JsonResponse { - $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'); - } + $handler($payload); - $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; + return $this->apiOk(); } - #[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)); - } - } - } } diff --git a/src/Controller/Admin/Asset/AssetCopyController.php b/src/Controller/Admin/Asset/AssetCopyController.php new file mode 100644 index 00000000..0f55bec2 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetCopyController.php @@ -0,0 +1,58 @@ +value)] +class AssetCopyController extends AdminAbstractController +{ + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy-info', name: 'opendxp_admin_asset_copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); + } + + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy', name: 'opendxp_admin_asset_copy', methods: ['POST'])] + public function copyAction( + CopyAssetPayload $payload, + CopyAssetHandler $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/Asset/AssetDownloadController.php b/src/Controller/Admin/Asset/AssetDownloadController.php new file mode 100644 index 00000000..e442a522 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetDownloadController.php @@ -0,0 +1,143 @@ +value)] +class AssetDownloadController extends AdminAbstractController +{ + #[Route('/download', name: 'opendxp_admin_asset_download', methods: ['GET'])] + public function downloadAction(DownloadAssetPayload $payload, DownloadAssetHandler $handler): StreamedResponse + { + $result = $handler($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 $handler, + ): BinaryFileResponse { + $result = $handler($payload); + + $imageFilename = $result->image->getFilename() ?: ''; + + $downloadFilename = preg_replace( + '/\.' . preg_quote(pathinfo($imageFilename, PATHINFO_EXTENSION), '/') . '$/i', + '.' . $result->thumbnail->getFileExtension(), + $imageFilename + ) ?? ''; + + 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[Route('/download-as-zip-add-files', name: 'opendxp_admin_asset_downloadaszipaddfiles', methods: ['GET'])] + public function downloadAsZipAddFilesAction( + AddFilesToZipPayload $payload, + AddFilesToZipHandler $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } + + /** + * 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 $handler, + ): BinaryFileResponse { + $result = $handler($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..db33ed5a --- /dev/null +++ b/src/Controller/Admin/Asset/AssetEditorController.php @@ -0,0 +1,55 @@ +value)] +class AssetEditorController extends AdminAbstractController +{ + #[Route('/image-editor', name: 'opendxp_admin_asset_imageeditor', methods: ['GET'])] + public function imageEditorAction(LoadAssetForEditorPayload $payload, LoadAssetForEditorHandler $handler): Response + { + $result = $handler($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 $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/Asset/AssetHelperController.php b/src/Controller/Admin/Asset/AssetHelperController.php index 019f3beb..119f7e79 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\Attribute\SessionGatewayAware; 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\Tool\Session; -use OpenDxp\Tool\Storage; -use OpenDxp\Version; -use stdClass; -use Symfony\Component\EventDispatcher\GenericEvent; +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\GetGridColumnConfig\GetGridColumnConfigPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\Helper\GetGridColumnConfig\GetGridColumnConfigHandler; +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\GridExportService; +use OpenDxp\Bundle\AdminBundle\Session\Gateway\GridColumnConfigSessionGateway; 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,108 @@ #[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; - } - #[Route('/grid-delete-column-config', name: 'opendxp_admin_asset_assethelper_griddeletecolumnconfig', methods: ['DELETE'])] - public function gridDeleteColumnConfigAction(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'), - ]; - - $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"); - } - - $gridConfig->delete(); - $success = true; - } - - $newGridConfig = $this->doGetGridColumnConfig($params, true); - $newGridConfig['deleteSuccess'] = $success; - - return $this->adminJson($newGridConfig); + public function gridDeleteColumnConfigAction( + DeleteGridColumnConfigPayload $payload, + DeleteGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); } #[Route('/grid-get-column-config', name: 'opendxp_admin_asset_assethelper_gridgetcolumnconfig', methods: ['GET'])] - public function gridGetColumnConfigAction(Request $request): 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'), - ]; - - $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; + public function gridGetColumnConfigAction( + GetGridColumnConfigPayload $payload, + GetGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); } + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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 { - $existingColumns = $session->get('helpercolumns', []); - $helperColumns = [...$helperColumns, ...$existingColumns]; - $session->set('helpercolumns', $helperColumns); - }, 'opendxp_gridconfig'); - - return $this->adminJson(['success' => true, 'columns' => $newData]); + public function prepareHelperColumnConfigs( + PrepareHelperColumnConfigsPayload $payload, + PrepareHelperColumnConfigsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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()); - } - - $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; + public function gridMarkFavouriteColumnConfigAction( + MarkGridConfigFavouritePayload $payload, + MarkGridConfigFavouriteHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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(); - - $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(); - } + public function gridSaveColumnConfigAction( + SaveGridColumnConfigPayload $payload, + SaveGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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 $handler): JsonResponse { - $allParams = [...$request->request->all(), ...$request->query->all()]; - $list = $gridHelperService->prepareAssetListingForGrid($allParams, $this->getAdminUser()); - - 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->apiJson($handler($payload)); } - /** - * @throws FilesystemException - */ #[Route('/do-export', name: 'opendxp_admin_asset_assethelper_doexport', methods: ['POST'])] - public function doExportAction(Request $request): JsonResponse + public function doExportAction(DoAssetExportPayload $payload, DoAssetExportHandler $handler): 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); + $handler($payload); - $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 - { - if (is_array($value)) { - $value = implode(',', $value); - } - $value = str_replace('"', '""', $value ?? ''); - - //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->apiOk(); } #[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 $handler): 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; - - return $this->adminJson($result); + return $this->apiJson($handler(), rootProperty: '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 $handler, 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(); - - return $this->adminJson(['success' => true, 'jobs' => $jobs]); + return $this->apiJson($handler($payload)); } #[Route('/batch', name: 'opendxp_admin_asset_assethelper_batch', methods: ['PUT'])] - public function batchAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse + public function batchAction(ExecuteAssetBatchPayload $payload, ExecuteAssetBatchHandler $handler): 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) { + $handler($payload); } - return $this->adminJson(['success' => false, 'message' => 'something went wrong.']); + return $this->apiOk(); } } diff --git a/src/Controller/Admin/Asset/AssetMediaController.php b/src/Controller/Admin/Asset/AssetMediaController.php new file mode 100644 index 00000000..5bfa0880 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetMediaController.php @@ -0,0 +1,156 @@ +value)] +class AssetMediaController extends AdminAbstractController +{ + #[Route('/get-asset', name: 'opendxp_admin_asset_getasset', methods: ['GET'])] + public function getAssetAction(DownloadAssetPayload $payload, DownloadAssetHandler $handler): StreamedResponse + { + $result = $handler($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 $handler, + ): Response { + $result = $handler($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 $handler, + ): StreamedResponse { + $result = $handler($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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + 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..6395b1c5 --- /dev/null +++ b/src/Controller/Admin/Asset/AssetThumbnailController.php @@ -0,0 +1,162 @@ +thumbnailResult === null) { + $response = new BinaryFileResponse(OPENDXP_WEB_ROOT . '/bundles/opendxpadmin/img/video-loading.gif'); + $response->headers->set('Cache-Control', 'no-store'); + + return $response; + } + + $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-image-thumbnail/fileinfo', name: 'opendxp_admin_asset_getimagethumbnail_fileinfo', methods: ['GET'])] + public function getImageThumbnailFileinfoAction( + GetImageThumbnailPayload $payload, + GetImageThumbnailFileinfoHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[Route('/get-folder-thumbnail', name: 'opendxp_admin_asset_getfolderthumbnail', methods: ['GET'])] + #[IsGranted(CorePermission::Assets->value)] + public function getFolderThumbnailAction(GetFolderThumbnailPayload $payload, GetFolderThumbnailHandler $handler): StreamedResponse + { + $result = $handler($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 $handler, + ): StreamedResponse { + $result = $handler($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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + 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..1050bd5c --- /dev/null +++ b/src/Controller/Admin/Asset/AssetUploadController.php @@ -0,0 +1,96 @@ +value)] +class AssetUploadController extends AdminAbstractController +{ + #[Route('/add-asset', name: 'opendxp_admin_asset_addasset', methods: ['POST'])] + public function addAssetAction(AddAssetPayload $payload, AddAssetHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[AsHtmlContentTypeResponse] + #[Route('/add-asset-compatibility', name: 'opendxp_admin_asset_addassetcompatibility', methods: ['POST'])] + public function addAssetCompatibilityAction(AddAssetPayload $payload, AddAssetCompatibilityHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/exists', name: 'opendxp_admin_asset_exists', methods: ['GET'])] + public function existsAction( + CheckAssetExistsPayload $payload, + CheckAssetExistsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); + } + + #[AsHtmlContentTypeResponse] + #[Route('/replace-asset', name: 'opendxp_admin_asset_replaceasset', methods: ['POST', 'PUT'])] + public function replaceAssetAction(ReplaceAssetPayload $payload, ReplaceAssetHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/import-zip', name: 'opendxp_admin_asset_importzip', methods: ['POST'])] + public function importZipAction( + ImportZipPayload $payload, + ImportZipHandler $handler, + ): Response { + // encodeJson(), not adminJson(): this POST isn't XHR, so a JSON content-type + // would trigger a download dialog in most browsers instead of being read by the iframe + return new Response( + $this->encodeJson(['success' => true, ...get_object_vars($handler($payload))]) + ); + } + + #[Route('/import-zip-files', name: 'opendxp_admin_asset_importzipfiles', methods: ['POST'])] + public function importZipFilesAction( + ImportZipFilesPayload $payload, + ImportZipFilesHandler $handler + ): JsonResponse { + + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/Asset/AssetVersionController.php b/src/Controller/Admin/Asset/AssetVersionController.php new file mode 100644 index 00000000..232f19ad --- /dev/null +++ b/src/Controller/Admin/Asset/AssetVersionController.php @@ -0,0 +1,84 @@ +value)] +class AssetVersionController extends AdminAbstractController +{ + #[Route('/publish-version', name: 'opendxp_admin_asset_publishversion', methods: ['POST'])] + public function publishVersionAction( + PublishVersionPayload $payload, + PublishVersionHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[Route('/show-version', name: 'opendxp_admin_asset_showversion', methods: ['GET'])] + public function showVersionAction( + Environment $twig, + ShowVersionPayload $payload, + ShowVersionHandler $handler, + ): Response { + $result = $handler($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 c5fa7a0f..2a89299b 100644 --- a/src/Controller/Admin/DataObject/ClassController.php +++ b/src/Controller/Admin/DataObject/ClassController.php @@ -16,2060 +16,267 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Exception; +use OpenDxp\Bundle\AdminBundle\Attribute\AsHtmlContentTypeResponse; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionGatewayAware; 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\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 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\BulkExportPrepareHandler; +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\Bundle\AdminBundle\Session\Gateway\BulkOperationSessionGateway; 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->apiJson($handler(), rootProperty: '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->apiJson($handler(), rootProperty: '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->apiJson($handler($payload), rootProperty: '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->apiJson($handler($payload), rootProperty: '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->apiJson($handler($payload)); } + #[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->apiJson($handler($payload)); } + #[IsGranted(CorePermission::Classes->value)] + #[AsHtmlContentTypeResponse] #[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, - ]); - - // 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('/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]); + return $this->apiOk(); } + #[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); - - throw $this->createNotFoundException($errorMessage); - } + $result = $handler($payload); - $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"'); - - 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'); + $response->headers->set('Content-Disposition', 'attachment; filename="class_' . $result->className . '_export.json"'); 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->apiJson($handler($payload), rootProperty: 'config'); } /** * Add option to export/import all class definitions/brick definitions etc. at once */ + #[IsGranted(CorePermission::Classes->value)] + #[AsHtmlContentTypeResponse] + #[SessionGatewayAware(BulkOperationSessionGateway::class)] #[Route('/bulk-import', name: 'bulkimport', methods: ['POST'])] - public function bulkImportAction(Request $request): JsonResponse + public function bulkImportAction(BulkImportPayload $payload, BulkImportHandler $handler): 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); - - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($tmpName): void { - $session->set('class_bulk_import_file', $tmpName); - }, '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->headers->set('Content-Type', 'text/html'); - - return $response; + return $this->apiJson($handler($payload)); } /** * Add option to export/import all class definitions/brick definitions etc. at once - * - * @throws Exception */ + #[SessionGatewayAware(BulkOperationSessionGateway::class)] #[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); + $handler($payload); - $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]); - } - - 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->apiOk(); } /** * Add option to export/import all class definitions/brick definitions etc. at once */ + #[IsGranted(CorePermission::Classes->value)] + #[SessionGatewayAware(BulkOperationSessionGateway::class)] #[Route('/bulk-export-prepare', name: 'bulkexportprepare', methods: ['POST'])] - public function bulkExportPrepareAction(Request $request): Response + public function bulkExportPrepareAction(BulkExportPreparePayload $payload, BulkExportPrepareHandler $handler): JsonResponse { - $data = $request->request->get('data'); + $handler($payload); - Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($data): void { - $session->set('class_bulk_export_settings', $data); - }, 'opendxp_objects'); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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->apiJson($handler()); } + #[SessionGatewayAware(BulkOperationSessionGateway::class)] #[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 = []; + $result = $handler(); - 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 = 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->apiJson($handler($payload), rootProperty: '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); - if ($content === false) { - continue; - } - $result[] = [ - 'text' => sprintf( - '', - $style, - mime_content_type(OPENDXP_WEB_ROOT . $icon), - base64_encode($content) - ), - 'value' => $icon, - ]; - } - - return $this->adminJson($result); + return $this->apiJson($handler($payload), rootProperty: '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): JsonResponse { - $classId = $request->query->get('classId'); - - $identifier = DataObject\ClassDefinition\CustomLayout::getIdentifier($classId); - - $list = new DataObject\ClassDefinition\CustomLayout\Listing(); - - $list = $list->load(); - $existingIds = []; - $existingNames = []; - - 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->apiJson($handler(), envelope: false); } + #[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 + public function videoAllowedTypesAction(GetVideoAllowedTypesHandler $handler): 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); + return $this->apiJson($handler(), rootProperty: 'types'); } - #[Route('/select-options-get', name: 'selectoptionsget', methods: [Request::METHOD_GET])] - public function selectOptionsGetAction(Request $request): JsonResponse + #[IsGranted(CorePermission::Selectoptions->value)] + #[Route('/select-options-get', name: 'selectoptionsget', methods: ['GET'])] + public function selectOptionsGetAction(GetSelectOptionsPayload $payload, GetSelectOptionsHandler $handler): JsonResponse { - $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->apiJson($handler($payload), rootProperty: 'data'); } - #[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-update', name: 'selectoptionsupdate', methods: ['PUT', 'POST'])] + public function selectOptionsUpdateAction(SaveSelectOptionsPayload $payload, SaveSelectOptionsHandler $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->apiJson($handler($payload)); } - #[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-tree', name: 'selectoptionstree', methods: ['GET', 'POST'])] + public function selectOptionsTreeAction(GetSelectOptionsTreePayload $payload, GetSelectOptionsTreeHandler $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); - } - - #[Route('/select-options-delete', name: 'selectoptionsdelete', methods: [Request::METHOD_DELETE])] - public function selectOptionsDeleteAction(Request $request): 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->apiJson($handler($payload), rootProperty: '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->apiOk(); } } diff --git a/src/Controller/Admin/DataObject/ClassificationstoreController.php b/src/Controller/Admin/DataObject/ClassificationstoreController.php index b092e927..5aec1c8a 100644 --- a/src/Controller/Admin/DataObject/ClassificationstoreController.php +++ b/src/Controller/Admin/DataObject/ClassificationstoreController.php @@ -16,1481 +16,257 @@ 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\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\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->apiOk(); } + #[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->apiOk(); } + #[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); - - $config->delete(); + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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'); + $handler($payload); - $id = $request->request->getInt('id'); - - $config = Classificationstore\GroupConfig::getById($id); - $config->delete(); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } - /** - * @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(); - - return $this->adminJson(['success' => true, 'id' => $config->getName()]); - } - - return $this->adminJson(['success' => false, 'id' => $config->getName(), 'message' => 'classificationstore_error_group_exists_msg']); + return $this->apiJson($handler($payload)); } - /** - * @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->apiJson($handler($payload)); } - /** - * @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->apiJson($handler($payload)); } + #[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 = []; - - $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->apiJson($handler($payload)); } + #[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)]); - } - - return $this->adminJson(['success' => false]); + return $this->apiJson($handler($payload)); } + #[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(); - } - - 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->apiJson($handler($payload)); } + #[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)]); - } - - return $this->adminJson(['success' => false]); + return $this->apiJson($handler($payload)); } + #[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(); - - return $this->adminJson($rootElement); + return $this->apiJson($handler($payload)); } + #[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]); - } - - return $this->adminJson(['success' => false]); + return $this->apiJson($handler($payload)); } + #[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->apiJson($handler(), rootProperty: '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'); - } - - $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->apiJson($handler($payload)); } #[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); - } - - 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->apiJson($handler($payload)); } + #[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]); - } - - return $this->adminJson(['success' => false]); + return $this->apiJson($handler($payload)); } - /** - * @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->apiJson($handler($payload), rootProperty: '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->apiJson($handler($payload), rootProperty: '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 = []; - - $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->apiJson($handler($payload)); } + #[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 - { - $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 + public function propertiesAction(UpdatePropertyPayload $payload, UpdatePropertyHandler $handler): JsonResponse { - $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']; - } - } - - return $item; + return $this->apiJson($handler($payload)); } + #[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->apiJson($handler($payload)); } + #[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->apiOk(); } - /** - * @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); + $handler($payload); - if (!$config) { - throw new Exception('Configuration does not exist'); - } - - $config->setName($name); - $config->setDescription($description); - $config->save(); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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->apiJson($handler(), rootProperty: 'items'); } + #[IsGranted(CorePermission::Classificationstore->value)] #[Route('/get-page', name: 'getpage', methods: ['GET'])] - public function getPageAction(Request $request): JsonResponse + public function getPageAction(GetPagePayload $payload, GetPageHandler $handler): 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 - { - $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->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/DataObject/CustomLayoutController.php b/src/Controller/Admin/DataObject/CustomLayoutController.php new file mode 100644 index 00000000..ac48e525 --- /dev/null +++ b/src/Controller/Admin/DataObject/CustomLayoutController.php @@ -0,0 +1,118 @@ +value)] +class CustomLayoutController extends AdminAbstractController +{ + #[Route('/get-custom-layout', name: 'getcustomlayout', methods: ['GET'])] + public function getCustomLayoutAction(GetCustomLayoutHandler $handler, GetCustomLayoutPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/add-custom-layout', name: 'addcustomlayout', methods: ['POST'])] + public function addCustomLayoutAction(AddCustomLayoutHandler $handler, AddCustomLayoutPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/save-custom-layout', name: 'savecustomlayout', methods: ['PUT'])] + public function saveCustomLayoutAction(SaveCustomLayoutHandler $handler, SaveCustomLayoutPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/delete-custom-layout', name: 'deletecustomlayout', methods: ['DELETE'])] + public function deleteCustomLayoutAction(DeleteCustomLayoutHandler $handler, StringIdBodyPayload $payload): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[AsHtmlContentTypeResponse] + #[Route('/import-custom-layout-definition', name: 'importcustomlayoutdefinition', methods: ['POST', 'PUT'])] + public function importCustomLayoutDefinitionAction( + ImportCustomLayoutHandler $handler, + ImportCustomLayoutPayload $payload, + ): Response { + $handler($payload); + + return $this->apiOk(); + } + + #[Route('/export-custom-layout-definition', name: 'exportcustomlayoutdefinition', methods: ['GET'])] + public function exportCustomLayoutDefinitionAction(ExportCustomLayoutHandler $handler, ExportCustomLayoutPayload $payload): Response + { + $result = $handler($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 $handler, GetCustomLayoutDefinitionsPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/get-all-layouts', name: 'getalllayouts', methods: ['GET'])] + public function getAllLayoutsAction(GetAllLayoutsHandler $handler): JsonResponse + { + return $this->apiJson($handler(), envelope: false); + } + + #[Route('/suggest-custom-layout-identifier', name: 'suggestcustomlayoutidentifier', methods: ['GET'])] + public function suggestCustomLayoutIdentifierAction(SuggestCustomLayoutIdentifierHandler $handler, SuggestCustomLayoutIdentifierPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload), envelope: false); + } +} diff --git a/src/Controller/Admin/DataObject/DataObjectController.php b/src/Controller/Admin/DataObject/DataObjectController.php index 313c2749..70d1fcb0 100644 --- a/src/Controller/Admin/DataObject/DataObjectController.php +++ b/src/Controller/Admin/DataObject/DataObjectController.php @@ -13,61 +13,59 @@ * @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\Attribute\SessionGatewayAware; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionIdentityAware; 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\TreeGetDataObjectChildren\TreeGetDataObjectChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetDataObjectChildren\TreeGetDataObjectChildrenPayload; +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\Security\CsrfProtectionHandler; -use OpenDxp\Controller\KernelControllerEventInterface; -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 OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; +use OpenDxp\Bundle\AdminBundle\Session\Gateway\GridColumnConfigSessionGateway; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Http\Attribute\IsGranted; 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; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; /** * @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,1922 +75,162 @@ 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 = []; - #[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'); - - if ($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'), - ]); - } - - return $this->adminJson($objects); + public function treeGetChildrenByIdAction( + Request $request, + TreeGetDataObjectChildrenPayload $payload, + ): Response { + return match ($payload->hasPagination()) { + true => $this->forward(self::class . '::treeGetChildrenByIdPaginatedAction', [], $request->query->all()), + false => $this->forward(self::class . '::treeGetChildrenByIdListAction', [], $request->query->all()), + }; } - 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; + #[Route('/tree-get-children-by-id-paginated', name: 'treegetchildrenbyidpaginated', methods: ['GET'])] + public function treeGetChildrenByIdPaginatedAction( + TreeGetDataObjectChildrenPayload $payload, + TreeGetDataObjectChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } - /** - * @throws Exception - */ - #[Override] - protected function getTreeNodeConfig(ElementInterface $element): array - { - return $this->elementService->getElementTreeNodeConfig($element); + #[Route('/tree-get-children-by-id-list', name: 'treegetchildrenbyidlist', methods: ['GET'])] + public function treeGetChildrenByIdListAction( + TreeGetDataObjectChildrenPayload $payload, + TreeGetDataObjectChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'nodes'); } #[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; - } - - $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, - ]; - - 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->apiJson($handler($payload), rootProperty: 'data'); } - /** - * @throws Exception - */ + #[SessionIdentityAware] #[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); - } - - throw $this->createAccessDeniedHttpException(); - } - - 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; - } - } - } - } + public function getAction( + GetDataObjectPayload $payload, + GetDataObjectHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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; - - if ($isInheritedValue && !$fielddefinition->isEmpty($fieldData) && !$fielddefinition->supportsInheritance()) { - $this->objectData[$key] = null; - $this->metaData[$key]['inherited'] = false; - $this->metaData[$key]['hasParentValue'] = true; - } - } - } + public function getSelectOptions( + GetSelectOptionsPayload $payload, + GetSelectOptionsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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(), - ]; - } - - return $reduced; + public function getFolderAction( + IdQueryPayload $payload, + GetDataObjectFolderHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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(), - ]; - } - - return $this->adminJson($return); + public function addAction( + AddObjectPayload $payload, + AddObjectHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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->apiOk(); } - /** - * @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]); - } - 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(); - } + public function deleteAction( + DeleteDataObjectPayload $payload, + DeleteDataObjectHandler $handler, + ): JsonResponse { + $result = $handler($payload); - // 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]); + if ($payload->type === 'children') { + return $this->apiJson($result); } - return $this->adminJson(['success' => false]); + // return ok even when the object doesn't exist — valid for batch delete incl. children + return $this->apiOk(); } - /** - * @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->apiOk(); } - /** - * @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(); - } - } - - 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); + public function updateAction( + UpdateDataObjectPayload $payload, + UpdateDataObjectHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @throws Exception - */ + #[SessionIdentityAware] #[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')) { + $result = $handler($payload); - $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(), ''); - - return $this->adminJson(['success' => true]); + if ($payload->task === 'session' || $payload->task === 'scheduler') { + return $this->apiOk(); } - 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(), - ], - 'draft' => $draftData, - 'treeData' => $treeData, - ]); - } - - throw $this->createAccessDeniedHttpException(); + return $this->apiJson($result, context: [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); } #[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->apiOk(); } + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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']); + $result = $handler($payload); + if ($result->requestedLanguage && $result->requestedLanguage !== 'default') { + $request->setLocale($result->requestedLanguage); } - $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']); - } - - throw $this->createAccessDeniedHttpException(); + return $this->apiJson($result, rootProperty: 'data'); } + #[SessionIdentityAware] #[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..94da10ac --- /dev/null +++ b/src/Controller/Admin/DataObject/DataObjectCopyController.php @@ -0,0 +1,69 @@ +value)] +class DataObjectCopyController extends AdminAbstractController +{ + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy-info', name: 'copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); + } + + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy-rewrite-ids', name: 'copyrewriteids', methods: ['PUT'])] + public function copyRewriteIdsAction( + RewriteDataObjectIdsPayload $payload, + RewriteDataObjectIdsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy', name: 'copy', methods: ['POST'])] + public function copyAction( + CopyDataObjectPayload $payload, + CopyDataObjectHandler $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/DataObject/DataObjectHelperController.php b/src/Controller/Admin/DataObject/DataObjectHelperController.php index bb10862c..120e9e9c 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\Attribute\AsHtmlContentTypeResponse; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionGatewayAware; 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\Session\Gateway\GridColumnConfigSessionGateway; 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,1651 +62,172 @@ #[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) {} + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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->apiJson($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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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->apiJson($handler($payload), rootProperty: 'data'); } + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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->apiJson($handler($payload), rootProperty: 'data'); } + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[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; - } - } - - Tool\Session::useBag($request->getSession(), static function (AttributeBagInterface $session) use ($helperColumns): void { - $existingColumns = $session->get('helpercolumns', []); - $helperColumns = [...$helperColumns, ...$existingColumns]; - $session->set('helpercolumns', $helperColumns); - }, 'opendxp_gridconfig'); - - return $this->adminJson(['success' => true, 'columns' => $newData]); + public function prepareHelperColumnConfigs( + PrepareHelperColumnConfigsPayload $payload, + PrepareHelperColumnConfigsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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 $handler, + ): JsonResponse { + $handler($payload); - throw $this->createAccessDeniedHttpException(); + return $this->apiOk(); } #[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; - } - - return $result; + public function gridMarkFavouriteColumnConfigAction( + MarkDataObjectGridConfigFavouritePayload $payload, + MarkDataObjectGridConfigFavouriteHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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; - } - - return null; + public function gridSaveColumnConfigAction( + SaveDataObjectGridColumnConfigPayload $payload, + SaveDataObjectGridColumnConfigHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } /** * IMPORTER */ + #[AsHtmlContentTypeResponse] #[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); - - $response = $this->adminJson([ - 'success' => true, - ]); - - // 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; - } - - 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; - } + public function importUploadAction( + ImportUploadPayload $payload, + ImportUploadHandler $handler, + ): JsonResponse { + $handler($payload); - protected function getCsvFile(string $fileHandle): string - { - return $fileHandle . '.csv'; + return $this->apiOk(); } #[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'); - - $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->apiJson($handler($payload)); } - /** - * @throws Exception|FilesystemException - */ #[Route('/do-export', name: 'doexport', methods: ['POST'])] public function doExportAction( + DoDataObjectExportPayload $payload, + DoDataObjectExportHandler $handler, 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'); + if ($payload->requestedLanguage !== $request->getLocale()) { + $request->setLocale($payload->requestedLanguage); } - $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); + $handler($payload); - $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); - } - } - - return $this->adminJson(['success' => true]); - } - - public function encodeFunc(string $value): string - { - $value = str_replace('"', '""', $value); - - //force wrap value in quotes and return - return '"' . $value . '"'; + return $this->apiOk(); } #[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()); - - $jobs = $list->loadIdList(); - - return $this->adminJson(['success' => true, 'jobs' => $jobs]); + return $this->apiJson($handler($payload)); } #[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) { + $handler($payload); } - return $this->adminJson(['success' => $success]); + return $this->apiOk(); } #[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]); - } - - /** - * @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; - } - } + public function getAvailableVisibleFieldsAction( + GetAvailableVisibleFieldsPayload $payload, + GetAvailableVisibleFieldsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } } diff --git a/src/Controller/Admin/DataObject/DataObjectVersionController.php b/src/Controller/Admin/DataObject/DataObjectVersionController.php new file mode 100644 index 00000000..66ef07a5 --- /dev/null +++ b/src/Controller/Admin/DataObject/DataObjectVersionController.php @@ -0,0 +1,92 @@ +value)] +class DataObjectVersionController extends AdminAbstractController +{ + #[Route('/publish-version', name: 'publishversion', methods: ['POST'])] + public function publishVersionAction(IdBodyPayload $payload, PublishVersionHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[Route('/preview-version', name: 'previewversion', methods: ['GET'])] + public function previewVersionAction( + Environment $twig, + PreviewVersionHandler $handler, + PreviewVersionPayload $payload, + ): Response + { + $result = $handler($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 $handler, + DiffVersionsPayload $payload, + ): Response + { + $result = $handler($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..d60df64e --- /dev/null +++ b/src/Controller/Admin/DataObject/FieldCollectionController.php @@ -0,0 +1,120 @@ +apiJson($handler($payload), rootProperty: 'data'); + } + + #[IsGranted(CorePermission::Classes->value)] + #[Route('/fieldcollection-update', name: 'fieldcollectionupdate', methods: ['PUT', 'POST'])] + public function fieldcollectionUpdateAction(UpdateFieldCollectionHandler $handler, UpdateFieldCollectionPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::Classes->value)] + #[IsGranted(CorePermission::Fieldcollections->value)] + #[Route('/fieldcollection-delete', name: 'fieldcollectiondelete', methods: ['DELETE'])] + public function fieldcollectionDeleteAction(DeleteFieldCollectionHandler $handler, StringIdBodyPayload $payload): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[Route('/fieldcollection-tree', name: 'fieldcollectiontree', methods: ['GET', 'POST'])] + public function fieldcollectionTreeAction(GetFieldCollectionTreeHandler $handler, GetFieldCollectionTreePayload $payload): JsonResponse + { + $result = $handler($payload); + + if ($payload->forObjectEditor) { + return $this->apiJson($result, envelope: false); + } + + return $this->apiJson($result, rootProperty: 'definitions'); + } + + #[Route('/fieldcollection-list', name: 'fieldcollectionlist', methods: ['GET'])] + public function fieldcollectionListAction(GetFieldCollectionListHandler $handler, GetFieldCollectionListPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload), envelope: false); + } + + #[IsGranted(CorePermission::Fieldcollections->value)] + #[AsHtmlContentTypeResponse] + #[Route('/import-fieldcollection', name: 'importfieldcollection', methods: ['POST'])] + public function importFieldcollectionAction(ImportFieldCollectionHandler $handler, ImportFieldCollectionPayload $payload): Response + { + $handler($payload); + + return $this->apiOk(); + } + + #[IsGranted(CorePermission::Fieldcollections->value)] + #[Route('/export-fieldcollection', name: 'exportfieldcollection', methods: ['GET'])] + public function exportFieldcollectionAction(ExportFieldCollectionHandler $handler, ExportFieldCollectionPayload $payload): Response + { + $result = $handler($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; + } + + #[IsGranted(CorePermission::Classes->value)] + #[Route('/get-fieldcollection-usages', name: 'getfieldcollectionusages', methods: ['GET'])] + public function getFieldcollectionUsagesAction(GetFieldCollectionUsagesHandler $handler, GetFieldCollectionUsagesPayload $payload): Response + { + return $this->apiJson($handler($payload), rootProperty: 'usages'); + } +} diff --git a/src/Controller/Admin/DataObject/ObjectBrickController.php b/src/Controller/Admin/DataObject/ObjectBrickController.php new file mode 100644 index 00000000..1bba3539 --- /dev/null +++ b/src/Controller/Admin/DataObject/ObjectBrickController.php @@ -0,0 +1,118 @@ +apiJson($handler($payload), rootProperty: 'data'); + } + + #[Route('/objectbrick-update', name: 'objectbrickupdate', methods: ['PUT', 'POST'])] + public function objectbrickUpdateAction(UpdateObjectBrickHandler $handler, UpdateObjectBrickPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::Objectbricks->value)] + #[Route('/objectbrick-delete', name: 'objectbrickdelete', methods: ['DELETE'])] + public function objectbrickDeleteAction(DeleteObjectBrickHandler $handler, StringIdBodyPayload $payload): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[Route('/objectbrick-tree', name: 'objectbricktree', methods: ['GET', 'POST'])] + public function objectbrickTreeAction(GetObjectBrickTreeHandler $handler, GetObjectBrickTreePayload $payload): JsonResponse + { + $result = $handler($payload); + + if ($payload->forObjectEditor) { + return $this->apiJson($result, envelope: false); + } + + return $this->apiJson($result, rootProperty: 'definitions'); + } + + #[Route('/objectbrick-list', name: 'objectbricklist', methods: ['GET'])] + public function objectbrickListAction(GetObjectBrickListHandler $handler, GetObjectBrickListPayload $payload): JsonResponse + { + return $this->apiJson($handler($payload), envelope: false); + } + + #[IsGranted(CorePermission::Objectbricks->value)] + #[AsHtmlContentTypeResponse] + #[Route('/import-objectbrick', name: 'importobjectbrick', methods: ['POST'])] + public function importObjectbrickAction(ImportObjectBrickHandler $handler, ImportObjectBrickPayload $payload): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[IsGranted(CorePermission::Objectbricks->value)] + #[Route('/export-objectbrick', name: 'exportobjectbrick', methods: ['GET'])] + public function exportObjectbrickAction(ExportObjectBrickHandler $handler, ExportObjectBrickPayload $payload): Response + { + $result = $handler($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; + } + + #[IsGranted(CorePermission::Classes->value)] + #[Route('/get-bricks-usages', name: 'getbrickusages', methods: ['GET'])] + public function getBrickUsagesAction(GetBrickUsagesHandler $handler, GetBrickUsagesPayload $payload): Response + { + return $this->apiJson($handler($payload), rootProperty: 'usages'); + } +} diff --git a/src/Controller/Admin/DataObject/QuantityValueController.php b/src/Controller/Admin/DataObject/QuantityValueController.php index 354cc1c4..6eb413c2 100644 --- a/src/Controller/Admin/DataObject/QuantityValueController.php +++ b/src/Controller/Admin/DataObject/QuantityValueController.php @@ -16,19 +16,31 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject; -use Exception; +use OpenDxp\Bundle\AdminBundle\Attribute\AsHtmlContentTypeResponse; 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\Exception\AdminOperationFailedException; +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\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal @@ -36,254 +48,90 @@ #[Route('/quantity-value', name: 'opendxp_admin_dataobject_quantityvalue_')] class QuantityValueController extends AdminAbstractController { - public function __construct(protected QuantityValueService $service) - { - } - + #[AsHtmlContentTypeResponse] #[Route('/unit-import', name: 'unitimport', methods: ['POST', 'PUT'])] - public function unitImportAction(Request $request): JsonResponse + public function unitImportAction(ImportQuantityValueUnitsPayload $payload, ImportQuantityValueUnitsHandler $handler): JsonResponse { - /** @var UploadedFile $uploadFile */ - $uploadFile = $request->files->get('Filedata'); + $handler($payload); - $json = file_get_contents($uploadFile->getPathname()); - if ($json === false) { - throw new BadRequestHttpException('Failed to read file'); - } - $success = $this->service->importDefinitionFromJson($json); - $response = $this->adminJson(['success' => $success]); - $response->headers->set('Content-Type', 'text/html'); - - return $response; + return $this->apiOk(); } #[Route('/unit-export', name: 'unitexport', methods: ['GET'])] - public function unitExportAction(Request $request): Response + public function unitExportAction(ExportQuantityValueUnitsHandler $handler): Response { - $result = $this->service->generateDefinitionJson(); - $response = new Response($result); + $response = new Response($handler()); $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 $handler, 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']); - } - - $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->apiJson($handler($payload)); } - /** - * @throws Exception - */ #[Route('/unit-proxy', name: 'unitproxy', methods: ['POST', 'PUT'])] - public function unitProxyAction(Request $request): JsonResponse - { - $this->checkPermission('quantityValueUnits'); - - if ($request->request->has('data')) { - $data = json_decode($request->request->get('data'), true); - if (!is_array($data)) { - throw new BadRequestHttpException('Invalid data format'); - } - $id = $data['id']; - $unit = Unit::getById($id); - - if ($request->query->get('xaction') === 'destroy') { - 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') { - 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 ' . $id . ' not found.'); - } - if ($request->query->get('xaction') === 'create') { - if (isset($data['baseunit']) && $data['baseunit'] === -1) { - $data['baseunit'] = null; - } - - if ($unit instanceof Unit) { - 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 AdminOperationFailedException(), + }; + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } - return $mapper[$comparison]; + #[Route('/unit-proxy-create', name: 'unitproxy_create', methods: ['POST', 'PUT'])] + #[IsGranted(CorePermission::QuantityValueUnits->value)] + public function unitProxyCreateAction( + QuantityValueUnitPayload $payload, + CreateQuantityValueUnitHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[Route('/unit-list', name: 'unitlist', methods: ['GET'])] - public function unitListAction(Request $request): JsonResponse + public function unitListAction(GetQuantityValueUnitListHandler $handler, 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 = []; - $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->apiJson($handler($payload)); } #[Route('/convert', name: 'convert', methods: ['GET'])] - public function convertAction(Request $request, UnitConversionService $conversionService): JsonResponse + #[IsGranted(CorePermission::Objects->value)] + public function convertAction(ConvertQuantityValueHandler $handler, ConvertQuantityValuePayload $payload): JsonResponse { - $this->checkPermission('objects'); - - $fromUnitId = $request->query->get('fromUnit'); - $toUnitId = $request->query->get('toUnit'); - - $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->apiJson($handler($payload)); } #[Route('/convert-all', name: 'convertall', methods: ['GET'])] - public function convertAllAction(Request $request, UnitConversionService $conversionService): JsonResponse + #[IsGranted(CorePermission::Objects->value)] + public function convertAllAction(ConvertAllQuantityValuesHandler $handler, 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()]); - } - } - - return $this->adminJson([ - 'value' => $request->query->get('value'), - 'fromUnit' => $fromUnit->getAbbreviation(), - 'values' => $convertedValues, 'success' => true] - ); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/DataObject/VariantsController.php b/src/Controller/Admin/DataObject/VariantsController.php index 3942b2d3..78898440 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); - - return $this->adminJson($this->renameObject($object, $key)); + return $this->apiJson($handler($payload), rootProperty: 'data'); } - /** - * @throws Exception - */ + #[SessionGatewayAware(GridColumnConfigSessionGateway::class)] #[Route('/get-variants', name: 'getvariants', methods: ['POST'])] public function getVariantsAction( + GetVariantsHandler $handler, + GetVariantsPayload $payload, Request $request, - EventDispatcherInterface $eventDispatcher, - GridHelperService $gridHelperService, - LocaleServiceInterface $localeService, - CsrfProtectionHandler $csrfProtection + CsrfProtectionHandler $csrfProtection, ): JsonResponse { - - $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'); - } - - $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 - ); + if ($payload->requestedLanguage !== $request->getLocale()) { + $request->setLocale($payload->requestedLanguage); + } - return $this->adminJson($result); + return $this->apiJson($handler($payload), rootProperty: 'data'); } } diff --git a/src/Controller/Admin/Document/DocumentController.php b/src/Controller/Admin/Document/DocumentController.php index f5c53d6a..646916fd 100644 --- a/src/Controller/Admin/Document/DocumentController.php +++ b/src/Controller/Admin/Document/DocumentController.php @@ -15,1360 +15,351 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\Document; -use Exception; -use Imagick; -use OpenDxp; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionIdentityAware; 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\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 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\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\Handler\Element\GetTreeRoot\GetTreeRootHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetTreeRoot\GetTreeRootPayload; 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\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; 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; - - protected Document\Service $_documentService; + #[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( + GetTreeRootPayload $payload, + GetTreeRootHandler $handler, + ): JsonResponse { + return parent::treeGetRootAction($payload, $handler); } + #[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)] + #[SessionIdentityAware] #[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); - } - - throw $this->createAccessDeniedHttpException(); + return $this->apiJson($handler($payload), rootProperty: '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( + Request $request, + TreeGetDocumentChildrenPayload $payload, + ): Response { - $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; - } - } - } - - //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'); + return match ($payload->hasPagination()) { + true => $this->forward(self::class . '::treeGetChildrenByIdPaginatedAction', [], $request->query->all()), + false => $this->forward(self::class . '::treeGetChildrenByIdListAction', [], $request->query->all()), + }; + } - if ($allParams['limit']) { - 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'), - ]); - } + #[IsGranted(CorePermission::Documents->value)] + #[Route('/tree-get-children-by-id-paginated', name: 'opendxp_admin_document_document_treegetchildrenbyidpaginated', methods: ['GET'])] + public function treeGetChildrenByIdPaginatedAction( + TreeGetDocumentChildrenPayload $payload, + TreeGetDocumentChildrenHandler $handler, + ): JsonResponse + { + return $this->apiJson($handler($payload), envelope: false); + } - return $this->adminJson($documents); + #[IsGranted(CorePermission::Documents->value)] + #[Route('/tree-get-children-by-id-list', name: 'opendxp_admin_document_document_treegetchildrenbyidlist', methods: ['GET'])] + public function treeGetChildrenByIdListAction( + TreeGetDocumentChildrenPayload $payload, + TreeGetDocumentChildrenHandler $handler, + ): JsonResponse + { + return $this->apiJson($handler($payload), rootProperty: 'nodes'); } + #[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()]); - } - - 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->apiJson($handler($payload)); } + #[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')); - - $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'); + $result = $handler($payload); - $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->apiJson($result); } - 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->apiOk(); } - /** - * @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(); - - 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); + return $this->apiJson($handler($payload)); } - private function firePostMoveEvent(Document $document, Document $oldDocument, string $oldPath): void + #[Route('/doc-types', name: 'opendxp_admin_document_document_doctypesget', methods: ['GET'])] + public function docTypesGetAction( + GetDocTypesListHandler $handler, + ): JsonResponse { - $arguments = [ - 'oldPath' => $oldPath, - 'oldDocument' => $oldDocument, - ]; - $documentEvent = new OpenDxp\Event\Model\DocumentEvent($document, $arguments); - $this->dispatchEvent($documentEvent, OpenDxp\Event\DocumentEvents::POST_MOVE_ACTION); + return $this->apiJson($handler()); } - 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(); - } + #[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, + #[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 => throw new BadRequestHttpException(), }; - - // 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++; - } } - #[Route('/doc-types', name: 'opendxp_admin_document_document_doctypesget', methods: ['GET'])] - public function docTypesGetAction(Request $request): 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; - } - } - - return $this->adminJson(['data' => $docTypes, 'success' => true, 'total' => count($docTypes)]); - } - - #[Route('/doc-types', name: 'opendxp_admin_document_document_doctypes', methods: ['PUT', 'POST', 'DELETE'])] - public function docTypesAction(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 $handler, + ): 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); + return $this->apiJson($handler($payload)); } - /** - * @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-update', name: 'opendxp_admin_document_document_doctypes_update', methods: ['PUT', 'POST', 'DELETE'])] + public function docTypesUpdateAction( + DocTypePayload $payload, + UpdateDocTypeHandler $handler, + ): 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->apiJson($handler($payload)); } - #[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-create', name: 'opendxp_admin_document_document_doctypes_create', methods: ['PUT', 'POST', 'DELETE'])] + public function docTypesCreateAction( + DocTypePayload $payload, + CreateDocTypeHandler $handler, + ): 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->apiJson($handler($payload)); } - #[Route('/publish-version', name: 'opendxp_admin_document_document_publishversion', methods: ['POST'])] - public function publishVersionAction(Request $request): JsonResponse + #[IsGranted(CorePermission::Documents->value)] + #[Route('/get-doc-types', name: 'opendxp_admin_document_document_getdoctypes', methods: ['GET'])] + public function getDocTypesAction( + GetDocTypesByTypePayload $payload, + GetDocTypesByTypeHandler $handler, + ): 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()]); - } - } - - $treeData = []; - $this->addAdminStyle($document, ElementAdminStyleEvent::CONTEXT_EDITOR, $treeData); - - return $this->adminJson(['success' => true, 'treeData' => $treeData]); + return $this->apiJson($handler($payload), envelope: false); } + #[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 $handler, + ): JsonResponse { - $site = Site::getById($request->request->getInt('id')); - - $event = new SiteCustomSettingsEvent($site); - $eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS); - - $customSettings = $event->getConfigNodes(); - - return $this->adminJson([ - 'data' => $customSettings, - ]); + return $this->apiJson($handler($payload), envelope: false); } + #[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) : []; - - 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->apiJson($handler($payload), rootProperty: '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 $handler, + ): JsonResponse { - $file = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/' . basename($request->query->get('id')); - if (file_exists($file)) { - return new BinaryFileResponse($file); - } + $handler($payload); - throw $this->createNotFoundException('Version diff file not found'); + return $this->apiOk(); } + #[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 $handler, + ): JsonResponse { - if ($doc = Document::getByPath($request->query->get('path'))) { - return $this->adminJson([ - 'id' => $doc->getId(), - 'type' => $doc->getType(), - ]); - } - - return $this->adminJson(false); + return $this->apiJson($handler($payload)); } + #[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')); - - $languages = explode(',', $request->query->get('languages')); - - $result = []; - foreach ($document->getChildren() as $child) { - $result[] = $this->getTranslationTreeNodeConfig($child, $languages); - } - - return $this->adminJson($result); + return $this->apiJson($handler($payload), rootProperty: '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 - { - $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, - ]; - } - } - - return $this->adminJson([ - 'root' => $this->getTranslationTreeNodeConfig($document, array_keys($translations), $translations), - 'columns' => $columns, - 'languages' => array_keys($translations), - ]); - } - - private function getTranslationTreeNodeConfig(Document $document, array $languages, ?array $translations = null): array + public function languageTreeRootAction( + GetLanguageTreeRootPayload $payload, + GetLanguageTreeRootHandler $handler, + ): JsonResponse { - $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; + return $this->apiJson($handler($payload), envelope: false); } + #[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; + $handler($payload); - // 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); - } - } - - $new->setType($type); - $new->save(); - } - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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; - } - } - - return $this->adminJson([ - 'success' => $success, - 'targetPath' => $targetDocument?->getRealFullPath(), - 'targetId' => $targetDocument?->getid(), - ]); + return $this->apiJson($handler($payload)); } + #[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')); + $handler($payload); - 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())); - } - - $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->apiOk(); } + #[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->apiOk(); } + #[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']); - - $this->_documentService = new Document\Service($this->getAdminUser()); + return $this->apiJson($handler($payload)); } - #[Override] - public function getTreeNodeConfig(ElementInterface $element): array - { - return $this->elementService->getElementTreeNodeConfig($element); - } } diff --git a/src/Controller/Admin/Document/DocumentControllerBase.php b/src/Controller/Admin/Document/DocumentControllerBase.php index 7e1d79be..4c99f646 100644 --- a/src/Controller/Admin/Document/DocumentControllerBase.php +++ b/src/Controller/Admin/Document/DocumentControllerBase.php @@ -15,42 +15,27 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin\Document; -use Exception; -use OpenDxp; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionIdentityAware; 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\Event\AdminEvents; -use OpenDxp\Bundle\AdminBundle\Event\ElementAdminStyleEvent; -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\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\RemoveFromSession\RemoveFromSessionPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Document\SaveToSession\SaveToSessionHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Document\SaveToSession\SaveToSessionPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\CorePermission; +use OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface; 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'; public const string TASK_UNPUBLISH = 'unpublish'; @@ -65,361 +50,44 @@ 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, + ) { } + #[SessionIdentityAware] #[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); + public function saveToSessionAction( + SaveToSessionPayload $payload, + SaveToSessionHandler $handler, + ): JsonResponse { + $handler($payload); - 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'); - } - - return $sessionDocument; + return $this->apiOk(); } + #[SessionIdentityAware] #[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( + RemoveFromSessionPayload $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->apiOk(); } /** * 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(); - } - - return $this->adminJson(['success' => true]); - } - - 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 - { - // 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; - } + public function changeMainDocumentAction( + ChangeMainDocumentPayload $payload, + ChangeMainDocumentHandler $handler, + ): JsonResponse { + $handler($payload); - /** - * @throws Element\ValidationException - * @throws Exception - */ - protected function saveDocument(Model\Document $document, Request $request, bool $latestVersion = false, ?string $task = null): array - { - 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(); - } - - break; - default: - throw $this->createAccessDeniedHttpException(); - } - - if ($document instanceof Model\Document\PageSnippet) { - $this->handleTask($task, $document); - } - - 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']; + return $this->apiOk(); } public function getTreeNodeConfig(ElementInterface $element): array diff --git a/src/Controller/Admin/Document/DocumentCopyController.php b/src/Controller/Admin/Document/DocumentCopyController.php new file mode 100644 index 00000000..58169822 --- /dev/null +++ b/src/Controller/Admin/Document/DocumentCopyController.php @@ -0,0 +1,69 @@ +value)] +class DocumentCopyController extends AdminAbstractController +{ + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy-info', name: 'opendxp_admin_document_document_copyinfo', methods: ['GET'])] + public function copyInfoAction( + CopyInfoPayload $payload, + CopyInfoHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy-rewrite-ids', name: 'opendxp_admin_document_document_copyrewriteids', methods: ['PUT'])] + public function copyRewriteIdsAction( + RewriteDocumentIdsPayload $payload, + RewriteDocumentIdsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[SessionGatewayAware(CopySessionGateway::class)] + #[Route('/copy', name: 'opendxp_admin_document_document_copy', methods: ['POST'])] + public function copyAction( + CopyDocumentPayload $payload, + CopyDocumentHandler $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/Document/DocumentVersionController.php b/src/Controller/Admin/Document/DocumentVersionController.php new file mode 100644 index 00000000..21867622 --- /dev/null +++ b/src/Controller/Admin/Document/DocumentVersionController.php @@ -0,0 +1,92 @@ +value)] + #[SessionIdentityAware] + #[Route('/version-to-session', name: 'opendxp_admin_document_document_versiontosession', methods: ['POST'])] + public function versionToSessionAction( + IdBodyPayload $payload, + SaveVersionToSessionHandler $handler, + ): Response { + $handler($payload); + + return new Response(); + } + + #[IsGranted(CorePermission::Documents->value)] + #[SessionIdentityAware] + #[Route('/publish-version', name: 'opendxp_admin_document_document_publishversion', methods: ['POST'])] + public function publishVersionAction( + IdBodyPayload $payload, + PublishVersionHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[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 $handler, + ): Response { + $result = $handler($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..66595d42 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'); - } - - 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->apiJson($handler($payload), rootProperty: 'data'); } - /** - * @throws Exception - */ + #[SessionIdentityAware] #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request): JsonResponse - { - $page = Document\Email::getById((int) $request->request->get('id')); - if (!$page) { - throw $this->createNotFoundException('Email not found'); - } - - [$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, - 'data' => [ - 'versionDate' => $page->getModificationDate(), - 'versionCount' => $page->getVersionCount(), - ], - 'treeData' => $treeData, - ]); - } - $draftData = []; - if ($version) { - $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), - ]; - } - - return $this->adminJson(['success' => true, 'draft' => $draftData]); - } - - protected function setValuesToDocument(Request $request, Document $document): void + public function saveAction(SaveEmailPayload $payload, SaveEmailHandler $handler): JsonResponse { - $this->addSettingsToDocument($request, $document); - $this->addDataToDocument($request, $document); - $this->addPropertiesToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/Document/FolderController.php b/src/Controller/Admin/Document/FolderController.php index cb073f52..86160524 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); - - return $this->preSendDataActions($data, $folder); + return $this->apiJson($handler($payload), rootProperty: 'data'); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request): JsonResponse - { - $folder = Document\Folder::getById((int) $request->request->get('id')); - if (!$folder) { - throw $this->createNotFoundException('Folder not found'); - } - - $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 + public function saveAction(SaveFolderPayload $payload, SaveFolderHandler $handler): JsonResponse { - $this->addPropertiesToDocument($request, $document); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/Document/HardlinkController.php b/src/Controller/Admin/Document/HardlinkController.php index e67e03cf..e1af66fd 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(); - } - - return $this->preSendDataActions($data, $link); + return $this->apiJson($handler($payload), rootProperty: 'data'); } /** * @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 = $this->saveDocument($link, $request); - /** @var Document\Hardlink $link */ - $link = $result[1]; - $treeData = $this->getTreeNodeConfig($link); - - return $this->adminJson([ - 'success' => true, - 'data' => [ - 'versionDate' => $link->getModificationDate(), - 'versionCount' => $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()); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/Document/LinkController.php b/src/Controller/Admin/Document/LinkController.php index 1b19a20f..63b07996 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; - } - - $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->apiJson($handler($payload), rootProperty: 'data'); } /** * @throws Exception */ #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): JsonResponse - { - $link = Document\Link::getById((int) $request->request->get('id')); - if (!$link) { - throw $this->createNotFoundException('Link not found'); - } - - $result = $this->saveDocument($link, $request); - /** @var Document\Link $link */ - $link = $result[1]; - $treeData = $this->getTreeNodeConfig($link); - - return $this->adminJson([ - 'success' => true, - 'data' => [ - 'versionDate' => $link->getModificationDate(), - 'versionCount' => $link->getVersionCount(), - ], - 'treeData' => $treeData, - ]); - } - - /** - * @param Document\Link $document - */ - protected function setValuesToDocument(Request $request, Document $document): void + public function saveAction(SaveLinkPayload $payload, SaveLinkHandler $handler): JsonResponse { - 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()); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/Document/PageController.php b/src/Controller/Admin/Document/PageController.php index fc8a6c05..3b990411 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); - } - - $data['url'] = $page->getUrl(); - $data['scheduledTasks'] = array_map( - static fn (Task $task) => $task->getObjectVars(), - $page->getScheduledTasks() - ); - - return $this->preSendDataActions($data, $page, $draftVersion); + public function getDataByIdAction( + GetPageDataHandler $handler, + GetPageDataPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); } - /** - * @throws Exception - */ + #[SessionIdentityAware] #[Route('/save', name: 'save', methods: ['PUT', 'POST'])] - public function saveAction(Request $request, StaticPageGenerator $staticPageGenerator): JsonResponse + public function saveAction(SavePagePayload $payload, 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'); - } - - 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); - - $data = [ - 'versionDate' => $page->getModificationDate(), - 'versionCount' => $page->getVersionCount(), - ]; - - if ($staticGeneratorEnabled = $page->getStaticGeneratorEnabled()) { - $data['staticGeneratorEnabled'] = $staticGeneratorEnabled; - $data['staticLastGenerated'] = $staticPageGenerator->getLastModified($page); - } - - return $this->adminJson([ - 'success' => true, - 'treeData' => $treeData, - 'data' => $data, - ]); - } - $this->saveToSession($page, $request->getSession()); - $draftData = []; - if ($version) { - $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), - ]; - } - $treeData = $this->getTreeNodeConfig($page); - - return $this->adminJson(['success' => true, 'treeData' => $treeData, 'draft' => $draftData]); + return $this->apiJson($handler($payload)); } #[Route('/generate-previews', name: 'generatepreviews', methods: ['GET'])] - public function generatePreviewsAction(Request $request, MessageBusInterface $messengerBusOpendxpCore): JsonResponse + public function generatePreviewsAction(GeneratePagePreviewsHandler $handler): JsonResponse { - $list = new Document\Listing(); - $list->setCondition('`type` = ?', ['page']); - - // @todo: this seems completely wrong. - foreach ($list->loadIdList() as $docId) { - $messengerBusOpendxpCore->dispatch( - new GeneratePagePreviewMessage($docId, \OpenDxp\Tool::getHostUrl()) - ); + $handler(); - break; - } - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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 { + $result = $handler($payload); + + return new BinaryFileResponse($result->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', '')); + $handler($payload); - $success = true; - - if ($path === '') { - return $this->adminJson([ - 'success' => $success, - ]); - } - - $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->apiOk(); } + #[SessionIdentityAware] #[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->apiOk(); } - /** - * @throws Exception - */ #[Route('/qr-code', name: 'qrcode', methods: ['GET'])] - public function qrCodeAction(Request $request): BinaryFileResponse - { - $page = Document\Page::getById((int) $request->query->get('id')); + public function qrCodeAction( + GenerateQrCodeHandler $handler, + GenerateQrCodePayload $payload, + ): BinaryFileResponse { + $result = $handler($payload); - 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); - - $response = new BinaryFileResponse($tmpFile); + $response = new BinaryFileResponse($result->filePath); $response->headers->set('Content-Type', 'image/png'); - if ($request->query->get('download')) { + if ($payload->download) { $response->setContentDisposition('attachment', 'qrcode-preview.png'); } @@ -328,60 +117,13 @@ 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, - DocumentResolver $documentResolver, - LocaleServiceInterface $localeService + RenderAreabrickIndexEditmodePayload $payload, + RenderAreabrickIndexEditmodeHandler $handler, ): 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')); - - $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)); - - return new JsonResponse([ - 'editableDefinitions' => $definitionCollector->getDefinitions(), - 'htmlCode' => $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()); + return $this->apiJson($handler($payload), envelope: false); } } 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..6eb7cff7 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(); - } - - return $this->preSendDataActions($data, $snippet, $draftVersion); + return $this->apiJson($handler($payload), rootProperty: 'data'); } /** * @throws Exception */ + #[SessionIdentityAware] #[Route('/save', name: 'save', methods: ['POST', 'PUT'])] - public function saveAction(Request $request): 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'); - } - - [$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, - 'data' => [ - 'versionDate' => $snippet->getModificationDate(), - 'versionCount' => $snippet->getVersionCount(), - ], - 'treeData' => $treeData, - ]); - } - - $this->saveToSession($snippet, $request->getSession()); - - $draftData = []; - if ($version) { - $draftData = [ - 'id' => $version->getId(), - 'modificationDate' => $version->getDate(), - 'isAutoSave' => $version->isAutoSave(), - ]; - } - - return $this->adminJson(['success' => true, 'draft' => $draftData]); - } - - protected function setValuesToDocument(Request $request, Document $document): void + public function saveAction(SaveSnippetPayload $payload, SaveSnippetHandler $handler): JsonResponse { - $this->addSettingsToDocument($request, $document); - $this->addDataToDocument($request, $document); - $this->applySchedulerDataToElement($request, $document, $this->getAdminUser()); - $this->addPropertiesToDocument($request, $document); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/ElementController.php b/src/Controller/Admin/ElementController.php index ab67ef2a..77065dd6 100644 --- a/src/Controller/Admin/ElementController.php +++ b/src/Controller/Admin/ElementController.php @@ -16,856 +16,282 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Exception; -use OpenDxp; +use OpenDxp\Bundle\AdminBundle\Attribute\SessionIdentityAware; 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\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\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal */ class ElementController extends AdminAbstractController { + #[SessionIdentityAware] #[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()); + public function lockElementAction( + LockElementHandler $handler, + LockElementPayload $payload, + ): Response { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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')); + public function unlockElementAction( + UnlockElementHandler $handler, + UnlockElementPayload $payload, + ): Response { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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']); - } + public function unlockElementsAction( + UnlockElementsHandler $handler, + UnlockElementsPayload $payload, + ): Response { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } - /** - * 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, - ]); - } - - protected function processNoteTypesFromParameters(string $parameterName): JsonResponse - { - $config = $this->getParameter($parameterName); - $result = []; - foreach ($config as $configEntry) { - $result[] = [ - 'name' => $configEntry, - ]; - } - - return $this->adminJson(['noteTypes' => $result]); + public function getSubtypeAction( + GetSubtypeHandler $handler, + GetSubtypePayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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' => []]), - }; + public function noteTypesAction( + GetNoteTypesPayload $payload, + GetNoteTypesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } #[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(); - - $notes = []; + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteListAction( + Request $request, + NoteListPayload $payload, + GetNoteListHandler $handler, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::noteListDestroyAction', [], $request->query->all()), + default => throw new BadRequestHttpException(), + }; + } + + return $this->apiJson($handler($payload)); + } - foreach ($list->getNotes() as $note) { - $e = Element\Service::getNoteData($note); - $notes[] = $e; - } + #[Route('/element/note-list-destroy', name: 'opendxp_admin_element_notelist_destroy', methods: ['POST'])] + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteListDestroyAction( + NoteListPayload $payload, + DeleteNoteHandler $handler, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'data' => $notes, - 'success' => true, - 'total' => $list->getTotalCount(), - ]); + return $this->apiOk(); } #[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, - ]); + #[IsGranted(CorePermission::NotesEvents->value)] + public function noteAddAction( + AddNoteHandler $handler, + AddNotePayload $payload, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); } #[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; - } - - return $this->adminJson([ - 'data' => $results, - 'total' => $total, - 'hasHidden' => $hasHidden, - 'success' => $success, - ]); + public function findUsagesAction( + FindUsagesHandler $handler, + FindUsagesPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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); + public function getReplaceAssignmentsBatchJobsAction( + GetReplaceAssignmentsBatchJobsHandler $handler, + GetReplaceAssignmentsBatchJobsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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'; - } + public function replaceAssignmentsAction( + ReplaceAssignmentsHandler $handler, + ReplaceAssignmentsPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'success' => $success, - 'message' => $message, - ]); + return $this->apiOk(); } #[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; - } + public function unlockPropagateAction( + UnlockPropagateHandler $handler, + UnlockPropagatePayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'success' => $success, - ]); + return $this->apiOk(); } #[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; - } - - return $this->adminJson($data); + public function typePathAction( + TypePathHandler $handler, + TypePathPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), context: [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); } #[Route('/element/version-update', name: 'opendxp_admin_element_versionupdate', methods: ['PUT'])] - public function versionUpdateAction(Request $request): JsonResponse - { - $data = $this->decodeJson($request->request->get('data')); - - $version = Version::getById($data['id']); + public function versionUpdateAction( + VersionUpdateHandler $handler, + VersionUpdatePayload $payload, + ): JsonResponse { + $handler($payload); - if ($data['public'] != $version->getPublic() || $data['note'] != $version->getNote()) { - $version->setPublic($data['public']); - $version->setNote($data['note']); - $version->save(); - } - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } - /** - * @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')); - - 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]); + public function getNicePathAction( + GetNicePathHandler $handler, + GetNicePathPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @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"); - } - - throw $this->createNotFoundException('Element type not found'); + public function getVersionsAction( + GetVersionsHandler $handler, + GetVersionsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } #[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')); + public function deleteDraftAction( + DeleteDraftHandler $handler, + IdBodyPayload $payload, + ): JsonResponse { + $handler($payload); - if ($version) { - $version->delete(); - } - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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(); + public function deleteVersionAction( + DeleteVersionHandler $handler, + IdBodyPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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(); - } + public function deleteAllVersionAction( + DeleteAllVersionsHandler $handler, + DeleteAllVersionsPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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']??''); - } - - $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); + public function getRequiresDependenciesAction( + GetRequiresDependenciesHandler $handler, + GetDependenciesPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); } #[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); - - 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); + public function getRequiredByDependenciesAction( + GetRequiredByDependenciesHandler $handler, + GetDependenciesPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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(); - } - } - - return $this->adminJson(['properties' => $properties]); + public function getPredefinedPropertiesAction( + GetPredefinedPropertiesHandler $handler, + GetPredefinedPropertiesPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } #[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); - - return $this->adminJson( - [ - 'data' => $result, - 'success' => true, - ] - ); - } - - /** - * @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, - ]); - } - } - } - - return $result; + public function analyzePermissionsAction( + AnalyzePermissionsHandler $handler, + AnalyzePermissionsPayload $payload, + ): Response { + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/ElementControllerBase.php b/src/Controller/Admin/ElementControllerBase.php index 08e4e5cb..11376762 100644 --- a/src/Controller/Admin/ElementControllerBase.php +++ b/src/Controller/Admin/ElementControllerBase.php @@ -16,217 +16,34 @@ 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\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\Model\Element\ElementInterface; -use OpenDxp\Model\Element\Service; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetDeleteInfo\GetDeleteInfoPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetTreeRoot\GetTreeRootHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Element\GetTreeRoot\GetTreeRootPayload; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal */ abstract class ElementControllerBase extends AdminAbstractController { - public function __construct(protected ElementServiceInterface $elementService) - { - } - - /** - * @return array - */ - protected function getTreeNodeConfig(ElementInterface $element): array - { - return []; - } - #[Route('/tree-get-root', name: 'treegetroot', methods: ['GET'])] - public function treeGetRootAction(Request $request): JsonResponse + public function treeGetRootAction( + GetTreeRootPayload $payload, + GetTreeRootHandler $handler, + ): JsonResponse { - $type = $request->query->get('elementType'); - $allowedTypes = ['asset', 'document', 'object']; - - $id = 1; - if ($request->query->get('id')) { - $id = (int)$request->query->get('id'); - } - - if (in_array($type, $allowedTypes)) { - $root = Service::getElementById($type, $id); - if ($root?->isAllowed('list')) { - return $this->adminJson($this->getTreeNodeConfig($root)); - } - - return $this->adminJson(['success' => false, 'id' => $id]); - } - - return $this->adminJson(['success' => false, 'message' => 'missing_permission']); + return $this->apiJson($handler($payload), rootProperty: 'treeNodeConfig'); } - /** - * @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->apiJson($handler($payload), envelope: false); } } diff --git a/src/Controller/Admin/EmailController.php b/src/Controller/Admin/EmailController.php index 1fe9b431..f667433d 100644 --- a/src/Controller/Admin/EmailController.php +++ b/src/Controller/Admin/EmailController.php @@ -16,20 +16,37 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Admin; -use Exception; use OpenDxp\Bundle\AdminBundle\Controller\AdminAbstractController; +use OpenDxp\Bundle\AdminBundle\Exception\AdminOperationFailedException; +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\GetEmailLogDetails\GetEmailLogDetailsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\GetEmailLogHtml\GetEmailLogHtmlHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\GetEmailLogParams\GetEmailLogParamsHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Email\ShowEmailLog\GetEmailLogText\GetEmailLogTextHandler; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdBodyPayload; +use OpenDxp\Bundle\AdminBundle\Payload\Common\IdQueryPayload; +use OpenDxp\Bundle\AdminBundle\Handler\Email\BlocklistPayload; +use OpenDxp\Bundle\AdminBundle\Security\Permission\AdminPermission; +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\Profiler\Profiler; -use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @internal @@ -37,478 +54,154 @@ #[Route('/email')] class EmailController extends AdminAbstractController { - /** - * @throws Exception - */ + #[IsGranted(new Expression( + 'is_granted("' . CorePermission::Emails->value . '") or is_granted("' . AdminPermission::GdprDataExtractor->value . '")' + ))] #[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 $handler, + GetEmailLogsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @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 - { - 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, [ - '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'); - + public function showEmailLogAction( + Request $request, + ?Profiler $profiler, + #[MapQueryParameter] ?string $type = null, + ): Response { + $profiler?->disable(); + + return match ($type) { + 'params' => $this->forward(self::class . '::showEmailLogParamsAction', [], $request->query->all()), + 'text' => $this->forward(self::class . '::showEmailLogTextAction', [], $request->query->all()), + 'html' => $this->forward(self::class . '::showEmailLogHtmlAction', [], $request->query->all()), + 'details' => $this->forward(self::class . '::showEmailLogDetailsAction', [], $request->query->all()), + default => throw new AdminOperationFailedException(sprintf('Invalid email log type "%s"', $type ?? '')), + }; } - 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); - } + #[IsGranted(CorePermission::Emails->value)] + #[Route('/show-email-log/params', name: 'opendxp_admin_email_showemaillog_params', methods: ['GET'])] + public function showEmailLogParamsAction( + GetEmailLogParamsHandler $handler, + IdQueryPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'params'); + } - unset($value); + #[IsGranted(CorePermission::Emails->value)] + #[Route('/show-email-log/text', name: 'opendxp_admin_email_showemaillog_text', methods: ['GET'])] + public function showEmailLogTextAction( + GetEmailLogTextHandler $handler, + IdQueryPayload $payload, + ): Response { + return $this->render('@OpenDxpAdmin/admin/email/text.html.twig', ['log' => $handler($payload)->textLog]); + } - 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'; - } - } + #[IsGranted(CorePermission::Emails->value)] + #[Route('/show-email-log/html', name: 'opendxp_admin_email_showemaillog_html', methods: ['GET'])] + public function showEmailLogHtmlAction( + GetEmailLogHtmlHandler $handler, + IdQueryPayload $payload, + ): Response { + return new Response($handler($payload)->htmlLog, 200, [ + 'Content-Security-Policy' => "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:", + ]); + } - $data['leaf'] = true; - } + #[IsGranted(CorePermission::Emails->value)] + #[Route('/show-email-log/details', name: 'opendxp_admin_email_showemaillog_details', methods: ['GET'])] + public function showEmailLogDetailsAction( + GetEmailLogDetailsHandler $handler, + IdQueryPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'objectVars'); } - /** - * @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 $handler, + IdBodyPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'success' => $success, - ]); + return $this->apiOk(); } - /** - * @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 = []; - } - - foreach ($params as $entry) { - $data = null; - $hasChildren = isset($entry['children']) && is_array($entry['children']); + public function resendEmailAction( + ResendEmailHandler $handler, + ResendEmailPayload $payload, + ): JsonResponse { + $handler($payload); - 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->apiOk(); } - /** - * @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 $handler, + SendTestEmailPayload $payload, + ): JsonResponse { // Simulate a frontend request to prefix assets $request->attributes->set(RequestHelper::ATTRIBUTE_FRONTEND_REQUEST, true); - $mail = new Mail(); + $handler($payload); - 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(); - - return $this->adminJson([ - 'success' => true, - ]); + return $this->apiOk(); } - /** - * @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(); - } - - return $this->adminJson([ - 'success' => true, - 'data' => $jsonData, - 'total' => $list->getTotalCount(), - ]); - } - - return $this->adminJson(['success' => false]); + public function blocklistAction( + Request $request, + BlocklistPayload $payload, + GetBlocklistHandler $handler, + #[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 AdminOperationFailedException(''), + }; + } + + return $this->apiJson($handler($payload)); } - protected function parseLoggingParamObject(array $params): mixed - { - $data = null; - if ($params['data']['type'] === 'object') { - $class = '\\' . ltrim($params['data']['objectClass'], '\\'); - $reflection = new ReflectionClass($class); + #[IsGranted(CorePermission::Emails->value)] + #[Route('/blocklist-destroy', name: 'opendxp_admin_email_blocklist_destroy', methods: ['POST'])] + public function blocklistDestroyAction( + BlocklistPayload $payload, + DeleteBlocklistEntryHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } - return $data; + #[IsGranted(CorePermission::Emails->value)] + #[Route('/blocklist-create', name: 'opendxp_admin_email_blocklist_create', methods: ['POST'])] + public function blocklistCreateAction( + BlocklistPayload $payload, + CreateBlocklistEntryHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/IndexController.php b/src/Controller/Admin/IndexController.php index ca2b7ccd..56366f38 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 = $handler($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, + StatisticsHandler $handler, + ): 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; + $handler(); - return $this; + return $this->apiOk(); } 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..f619916e 100644 --- a/src/Controller/Admin/LoginController.php +++ b/src/Controller/Admin/LoginController.php @@ -17,42 +17,36 @@ 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\Attribute\SessionGatewayAware; 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\Exception\Login\TwoFactorCodeInvalidException; +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\Security\CsrfProtectionHandler; -use OpenDxp\Bundle\AdminBundle\System\AdminConfig; -use OpenDxp\Config; +use OpenDxp\Bundle\AdminBundle\Service\Login\LoginPageService; +use OpenDxp\Bundle\AdminBundle\Session\Gateway\TwoFactorSetupSessionGateway; 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 +60,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 LoginPageService $loginPageService, ) { } 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 +93,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 +108,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->loginPageService->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 +137,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 +158,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 $handler, ): Response { - $params = $this->buildLoginPageViewParams($config); - $error = null; + $params = $this->loginPageService->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 = $handler($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 +175,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->loginPageService->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 +209,54 @@ 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')); + #[SessionGatewayAware(TwoFactorSetupSessionGateway::class)] + #[Route('/login/2fa-setup-save', name: 'opendxp_admin_2fa_setup_save')] + public function twoFactorSetupSaveAction( + SaveTwoFactorSetupPayload $payload, + SaveTwoFactorSetupHandler $handler, + ): RedirectResponse { + try { + $handler($payload); + } catch (TwoFactorCodeInvalidException) { + return new RedirectResponse($this->generateUrl('opendxp_admin_2fa_setup', ['error' => '2fa_wrong'])); } - $newSecret = $twoFactor->generateSecret(); - - $request->getSession()->set('2fa_secret', $newSecret); + return new RedirectResponse($this->generateUrl('opendxp_admin_login')); + } - $user->setTwoFactorAuthentication('enabled', true); - $user->setTwoFactorAuthentication('type', 'google'); - $user->setTwoFactorAuthentication('secret', $newSecret); + #[SessionGatewayAware(TwoFactorSetupSessionGateway::class)] + #[Route('/login/2fa-setup-generate', name: 'opendxp_admin_2fa_setup_generate')] + public function twoFactorSetupGenerateAction( + GenerateTwoFactorSetupPayload $payload, + GenerateTwoFactorSetupHandler $handler, + ): Response { - $url = $twoFactor->getQRContent($proxyUser); + $params = $this->loginPageService->base(); + $params['setup'] = true; - $result = Builder::create() - ->writer(new PngWriter()) - ->data($url) - ->size(200) - ->build(); + if ($payload->error) { + $params['error'] = $payload->error; + } - $params['image'] = $result->getDataUri(); + $result = $handler($payload); + $params['image'] = $result->qrDataUri; 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..e159a141 100644 --- a/src/Controller/Admin/MiscController.php +++ b/src/Controller/Admin/MiscController.php @@ -1,5 +1,4 @@ getControllerReferences(); - - $result = array_map(fn ($controller) => [ - 'name' => $controller, - ], $controllerReferences); - - return $this->adminJson([ - 'success' => true, - 'data' => $result, - 'total' => count($result), - ]); + public function getAvailableControllerReferencesAction( + GetAvailableControllerReferencesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler()); } #[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); - - $result = array_map(static fn ($template) => [ - 'path' => $template, - ], $templates); - - return $this->adminJson([ - 'data' => $result, - ]); + public function getAvailableTemplatesAction( + GetAvailableTemplatesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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 +82,132 @@ 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( + AdminCssHandler $handler, + ): Response { + $result = $handler(); $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( + GetAvailableLanguagesHandler $handler, + ): Response { + $result = $handler(); + + $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 { + return $this->apiJson($handler($payload), envelope: false); } + #[IsGranted(CorePermission::MaintenanceMode->value)] + #[SessionIdentityAware] #[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->apiOk(); } #[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, - ]; - } - } - - return $this->adminJson(['data' => $options]); + public function countryListAction( + GetCountryListHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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, - ]; - } - - return $this->adminJson(['data' => $options]); + public function languageListAction( + GetLanguageListHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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->apiOk(); } #[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..3a863e06 100644 --- a/src/Controller/Admin/NotificationController.php +++ b/src/Controller/Admin/NotificationController.php @@ -18,16 +18,22 @@ 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\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\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 +41,81 @@ #[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'); - - $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); + public function recipientsAction( + GetRecipientsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), rootProperty: '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); - } - - return $this->adminJson(['success' => true]); + public function sendAction( + SendNotificationHandler $handler, + SendNotificationPayload $payload, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); } + #[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'); - - try { - $notification = $service->findAndMarkAsRead($id, $this->getAdminUser()->getId()); - } catch (UnexpectedValueException) { - return $this->adminJson( - [ - 'success' => false, - ] - ); - } - - $data = $service->format($notification); - - return $this->adminJson([ - 'success' => true, - 'data' => $data, - ]); + public function findAction( + FindNotificationHandler $handler, + IdQueryPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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); - - 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, - ]); + public function findAllAction( + FindAllNotificationsHandler $handler, + FindAllNotificationsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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()); - - $data = []; - - foreach ($result['data'] as $notification) { - $data[] = $service->format($notification); - } - - return $this->adminJson([ - 'success' => true, - 'total' => $result['total'], - 'data' => $data, - 'unread' => $unread, - ]); + public function findLastUnreadAction( + FindLastUnreadNotificationsHandler $handler, + FindLastUnreadNotificationsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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->apiOk(); } + #[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->apiOk(); } + #[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 $handler, + ): JsonResponse { + $handler(); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } } diff --git a/src/Controller/Admin/PortalController.php b/src/Controller/Admin/PortalController.php index 78a6ede9..f9313db1 100644 --- a/src/Controller/Admin/PortalController.php +++ b/src/Controller/Admin/PortalController.php @@ -1,5 +1,4 @@ dashboardHelper->getAllDashboards(); - - $data = []; - foreach (array_keys($dashboards) as $key) { - if ($key !== 'welcome') { - $data[] = $key; - } - } - - return $this->adminJson($data); + public function dashboardListAction( + GetDashboardListHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), rootProperty: '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 $handler, + CreateDashboardPayload $payload, + ): JsonResponse { + $handler($payload); - if (!empty($key)) { - $this->dashboardHelper->saveDashboard($key); - - return $this->adminJson(['success' => true]); - } - - return $this->adminJson(['success' => false, 'message' => 'empty']); + return $this->apiOk(); } #[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 $handler, + DeleteDashboardPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[Route('/get-configuration', name: 'opendxp_admin_portal_getconfiguration', methods: ['GET'])] - public function getConfigurationAction(Request $request): JsonResponse - { - $config = $this->dashboardHelper->getDashboard($request->query->get('key')); - - return $this->adminJson($config); + public function getConfigurationAction( + GetDashboardConfigurationHandler $handler, + GetDashboardConfigurationPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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 $handler, + RemoveWidgetPayload $payload, + ): JsonResponse { + $handler($payload); - $config['positions'] = $newConfig; - - $this->dashboardHelper->saveDashboard($dashboardId, $config); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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); - } - } - - $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]); + public function addWidgetAction( + AddWidgetHandler $handler, + AddWidgetPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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 $handler, + ReorderWidgetPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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 $handler, + UpdatePortletConfigPayload $payload, + ): JsonResponse { + $handler($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->apiOk(); } #[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'] = []; - - 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); + public function portletModifiedDocumentsAction( + GetModifiedDocumentsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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(), - ]; - } - } - - return $this->adminJson($response); + public function portletModifiedAssetsAction( + GetModifiedAssetsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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(), - ]; - } - } - - return $this->adminJson($response); + public function portletModifiedObjectsAction( + GetModifiedObjectsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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; - } - - $this->dashboardHelper = new Dashboard($this->getAdminUser()); + public function portletModificationStatisticsAction( + GetModificationStatisticsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } } diff --git a/src/Controller/Admin/RecyclebinController.php b/src/Controller/Admin/RecyclebinController.php index 82c1d57c..9a87fe8d 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' => []]); - } - - $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')) .'%'); - } - - $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); - } - } + public function listAction( + Request $request, + RecyclebinPayload $payload, + ListRecyclebinHandler $handler, + #[MapQueryParameter] ?string $xaction = null, + ): Response { + if ($payload->hasData) { + return match ($xaction) { + 'destroy' => $this->forward(self::class . '::listDestroyAction', [], $request->query->all()), + default => throw new AdminOperationFailedException(), + }; } - if ($conditionFilters !== []) { - $condition = implode(' AND ', $conditionFilters); - $list->setCondition($condition); - } - - $items = $list->load(); - $data = []; - foreach ($items as $item) { - $data[] = $item->getObjectVars(); - } - - return $this->adminJson(['data' => $data, 'success' => true, 'total' => $list->getTotalCount()]); + return $this->apiJson($handler($payload)); + } + #[IsGranted(CorePermission::Recyclebin->value)] + #[Route('/recyclebin/list-destroy', name: 'opendxp_admin_recyclebin_list_destroy', methods: ['POST'])] + public function listDestroyAction( + RecyclebinPayload $payload, + DeleteRecyclebinItemHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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 $handler, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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( + FlushRecyclebinHandler $handler, + ): JsonResponse { + $handler(); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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')); + public function addAction( + AddToRecyclebinPayload $payload, + AddToRecyclebinHandler $handler, + ): JsonResponse { + $handler($payload); - 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()]); - } - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } public function onKernelControllerEvent(ControllerEvent $event): void @@ -185,12 +106,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..4099be78 --- /dev/null +++ b/src/Controller/Admin/Settings/ThumbnailController.php @@ -0,0 +1,86 @@ +value)] + #[Route('/settings/thumbnail-tree', name: 'opendxp_admin_settings_thumbnailtree', methods: ['GET', 'POST'])] + public function thumbnailTreeAction(GetThumbnailTreeHandler $handler): JsonResponse + { + return $this->apiJson($handler(), rootProperty: 'nodes'); + } + + #[Route('/settings/thumbnail-downloadable', name: 'opendxp_admin_settings_thumbnaildownloadable', methods: ['GET'])] + public function thumbnailDownloadableAction(GetDownloadableThumbnailsHandler $handler): JsonResponse + { + return $this->apiJson($handler(), rootProperty: 'thumbnails'); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/thumbnail-add', name: 'opendxp_admin_settings_thumbnailadd', methods: ['POST'])] + public function thumbnailAddAction(AddThumbnailPayload $payload, AddThumbnailHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/thumbnail-delete', name: 'opendxp_admin_settings_thumbnaildelete', methods: ['DELETE'])] + public function thumbnailDeleteAction(DeleteThumbnailPayload $payload, DeleteThumbnailHandler $handler): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/thumbnail-get', name: 'opendxp_admin_settings_thumbnailget', methods: ['GET'])] + public function thumbnailGetAction( + GetThumbnailPayload $payload, + GetThumbnailHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/thumbnail-update', name: 'opendxp_admin_settings_thumbnailupdate', methods: ['PUT'])] + public function thumbnailUpdateAction(UpdateThumbnailPayload $payload, UpdateThumbnailHandler $handler): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/Settings/VideoThumbnailController.php b/src/Controller/Admin/Settings/VideoThumbnailController.php new file mode 100644 index 00000000..731039f3 --- /dev/null +++ b/src/Controller/Admin/Settings/VideoThumbnailController.php @@ -0,0 +1,102 @@ +' . + $translator->trans('php_cli_binary_and_or_ffmpeg_binary_setting_is_missing', [], 'admin') . + ''; + } + + return new Response($content); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/video-thumbnail-tree', name: 'opendxp_admin_settings_videothumbnailtree', methods: ['GET', 'POST'])] + public function videoThumbnailTreeAction(GetVideoThumbnailTreeHandler $handler): JsonResponse + { + return $this->apiJson($handler(), rootProperty: 'nodes'); + } + + #[Route('/settings/video-thumbnail-list', name: 'opendxp_admin_settings_videothumbnail_list', methods: ['GET'])] + public function videoThumbnailListAction(GetVideoThumbnailListHandler $handler): JsonResponse + { + return $this->apiJson($handler(), rootProperty: 'thumbnails'); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/video-thumbnail-add', name: 'opendxp_admin_settings_videothumbnailadd', methods: ['POST'])] + public function videoThumbnailAddAction(AddVideoThumbnailPayload $payload, AddVideoThumbnailHandler $handler): JsonResponse + { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/video-thumbnail-delete', name: 'opendxp_admin_settings_videothumbnaildelete', methods: ['DELETE'])] + public function videoThumbnailDeleteAction(DeleteVideoThumbnailPayload $payload, DeleteVideoThumbnailHandler $handler): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/video-thumbnail-get', name: 'opendxp_admin_settings_videothumbnailget', methods: ['GET'])] + public function videoThumbnailGetAction( + GetVideoThumbnailPayload $payload, + GetVideoThumbnailHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'data'); + } + + #[IsGranted(CorePermission::Thumbnails->value)] + #[Route('/settings/video-thumbnail-update', name: 'opendxp_admin_settings_videothumbnailupdate', methods: ['PUT'])] + public function videoThumbnailUpdateAction(UpdateVideoThumbnailPayload $payload, UpdateVideoThumbnailHandler $handler): JsonResponse + { + $handler($payload); + + return $this->apiOk(); + } +} diff --git a/src/Controller/Admin/SettingsController.php b/src/Controller/Admin/SettingsController.php index 1195d12a..fea625ee 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'; - } - - $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 - } - } + $result = $handler($payload); - 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 - */ + #[AsHtmlContentTypeResponse] #[Route('/upload-custom-logo', name: 'opendxp_admin_settings_uploadcustomlogo', methods: ['POST'])] - public function uploadCustomLogoAction(Request $request): JsonResponse + public function uploadCustomLogoAction(UploadCustomLogoPayload $payload, 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.'); - } - - $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($payload); - $response = $this->adminJson(['success' => true]); - $response->headers->set('Content-Type', 'text/html'); - - return $response; + return $this->apiOk(); } #[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->apiOk(); } - /** - * 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(); - - return $this->adminJson(['data' => $responseData, 'success' => true]); - } - } else { - // get list of types - $list = new Metadata\Predefined\Listing(); - - 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 false; - }); - } + public function metadataAction( + Request $request, + PredefinedMetadataPayload $payload, + GetPredefinedMetadataListHandler $handler, + #[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 AdminOperationFailedException(''), + }; + } + + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::AssetMetadata->value)] + #[Route('/predefined-metadata-destroy', name: 'opendxp_admin_settings_metadata_destroy', methods: ['POST'])] + public function metadataDestroyAction( + PredefinedMetadataPayload $payload, + DeletePredefinedMetadataHandler $handler, + ): JsonResponse { + $handler($payload); - $properties = []; - foreach ($list->getDefinitions() as $metadata) { - $metadata->expand(); - $data = $metadata->getObjectVars(); - $data['writeable'] = $metadata->isWriteable(); - $properties[] = $data; - } + return $this->apiOk(); + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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( + GetFilteredPredefinedMetadataPayload $payload, + GetFilteredPredefinedMetadataHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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(); - - 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]; - - foreach ($cellValues as $cellValue) { - if (stripos((string)$cellValue, (string) $filter) !== false) { - return true; - } - } - } - } - - return false; - }); - } + public function propertiesAction( + Request $request, + PredefinedPropertyPayload $payload, + GetPredefinedPropertiesListHandler $handler, + #[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 AdminOperationFailedException(''), + }; + } + + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::PredefinedProperties->value)] + #[Route('/properties-destroy', name: 'opendxp_admin_settings_properties_destroy', methods: ['POST'])] + public function propertiesDestroyAction( + PredefinedPropertyPayload $payload, + DeletePredefinedPropertyHandler $handler, + ): JsonResponse { + $handler($payload); - $properties = []; - foreach ($list->getProperties() as $property) { - $data = $property->getObjectVars(); - $data['writeable'] = $property->isWriteable(); - $properties[] = $data; - } + return $this->apiOk(); + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } - 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 $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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->apiJson($handler(), envelope: false); } + #[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(); - - // 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->apiJson($handler(), envelope: false); } + #[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(); - - $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); - }); + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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); - - // 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); - }); + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[IsGranted(new Expression( + 'is_granted("' . CorePermission::ClearCache->value . '") or is_granted("' . CorePermission::SystemSettings->value . '")' + ))] #[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 $handler, ): 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); - } + $handler($payload); - $response = new JsonResponse($result); + $response = $this->apiOk(); - 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 +268,102 @@ 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'); + $handler(); - // empty document cache - Cache::clearTags(['output', 'output_lifetime']); - - $eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_FULLPAGE_CACHE); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } + #[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('/'); + $handler(); - // system files - FileSystemHelper::recursiveDelete(OPENDXP_SYSTEM_TEMP_DIRECTORY, false); - - $eventDispatcher->dispatch(new GenericEvent(), SystemEvents::CACHE_CLEAR_TEMPORARY_FILES); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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->apiJson($handler(), rootProperty: 'langs'); } #[Route('/get-available-sites', name: 'opendxp_admin_settings_getavailablesites', methods: ['GET'])] - public function getAvailableSitesAction(Request $request): 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); + public function getAvailableSitesAction( + GetAvailableSitesPayload $payload, + GetAvailableSitesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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 = []; - - 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->apiJson($handler()); } #[Route('/thumbnail-adapter-check', name: 'opendxp_admin_settings_thumbnailadaptercheck', methods: ['GET'])] - public function thumbnailAdapterCheckAction(Request $request, TranslatorInterface $translator): 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); - } - - #[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(); - } - - $pipe->delete(); - - return $this->adminJson(['success' => true]); - } - - #[Route('/video-thumbnail-get', name: 'opendxp_admin_settings_videothumbnailget', methods: ['GET'])] - public function videoThumbnailGetAction(Request $request): JsonResponse + public function thumbnailAdapterCheckAction(ThumbnailAdapterCheckHandler $handler): Response { - $this->checkPermission('thumbnails'); - - $pipe = Asset\Video\Thumbnail\Config::getByName($request->query->get('name')); - - $data = $pipe->getObjectVars(); - $data['writeable'] = $pipe->isWriteable(); - - return $this->adminJson($data); + return new Response($handler()->content); } - #[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(); - - return $this->adminJson(['success' => true]); - } - - /** - * @throws Exception - */ + #[IsGranted(CorePermission::WebsiteSettings->value)] #[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]); - } + public function websiteSettingsAction( + Request $request, + WebsiteSettingPayload $payload, + GetWebsiteSettingsListHandler $handler, + #[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 AdminOperationFailedException(''), + }; + } + + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings-destroy', name: 'opendxp_admin_settings_websitesettings_destroy', methods: ['POST'])] + public function websiteSettingsDestroyAction( + WebsiteSettingPayload $payload, + DeleteWebsiteSettingHandler $handler, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => false]); + return $this->apiOk(); } - /** - * @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-update', name: 'opendxp_admin_settings_websitesettings_update', methods: ['POST'])] + public function websiteSettingsUpdateAction( + WebsiteSettingPayload $payload, + UpdateWebsiteSettingHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - #[Route('/get-available-algorithms', name: 'opendxp_admin_settings_getavailablealgorithms', methods: ['GET'])] - public function getAvailableAlgorithmsAction(Request $request): JsonResponse - { - $options = [ - [ - 'key' => 'password_hash', - 'value' => 'password_hash', - ], - ]; - - $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); + #[IsGranted(CorePermission::WebsiteSettings->value)] + #[Route('/website-settings-create', name: 'opendxp_admin_settings_websitesettings_create', methods: ['POST'])] + public function websiteSettingsCreateAction( + WebsiteSettingPayload $payload, + CreateWebsiteSettingHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * deleteViews - * delete views for localized fields when languages are removed to - * prevent mysql errors - */ - protected function deleteViews(string $language, string $dbName): void + #[Route('/get-available-algorithms', name: 'opendxp_admin_settings_getavailablealgorithms', methods: ['GET'])] + public function getAvailableAlgorithmsAction(GetAvailableAlgorithmsHandler $handler): JsonResponse { - $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->apiJson($handler()); } } diff --git a/src/Controller/Admin/TagsController.php b/src/Controller/Admin/TagsController.php index 60ea39c9..f128a444 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'); - - 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()]); - } + public function addAction( + AddTagPayload $payload, + AddTagHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @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'); - - $tag = Tag::getById((int) $request->request->get('id')); - if ($tag) { - $tag->delete(); - - return $this->adminJson(['success' => true]); - } + public function deleteAction( + DeleteTagPayload $payload, + DeleteTagHandler $handler, + ): JsonResponse { + $handler($payload); - throw $this->createNotFoundException('Tag with ID ' . $request->request->get('id') . ' not found.'); + return $this->apiOk(); } - /** - * @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'); + public function updateAction( + UpdateTagPayload $payload, + UpdateTagHandler $handler, + ): JsonResponse { + $handler($payload); - $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', ''))); - } - - $tag->save(); - - return $this->adminJson(['success' => true]); - } - - throw $this->createNotFoundException('Tag with ID ' . $request->request->get('id') . ' not found.'); + return $this->apiOk(); } #[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'); - - 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; + public function treeGetChildrenByIdAction( + GetTagTreeChildrenPayload $payload, + GetTagTreeChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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, []); - } - } - - return $this->adminJson($assignedTagArray); + public function loadTagsForElementAction( + LoadTagsForElementPayload $payload, + GetTagsForElementHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: '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'); - - $tag = Tag::getById($tagId); - if ($tag) { - Tag::addTagToElement($assignmentElementType, $assignmentElementId, $tag); - - return $this->adminJson(['success' => true, 'id' => $tag->getId()]); - } - - return $this->adminJson(['success' => false]); + public function addTagToElementAction( + AddTagToElementPayload $payload, + AddTagToElementHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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'); - - $tag = Tag::getById($tagId); - if ($tag) { - Tag::removeTagFromElement($assignmentElementType, $assignmentElementId, $tag); - - return $this->adminJson(['success' => true, 'id' => $tag->getId()]); - } - - return $this->adminJson(['success' => false]); + public function removeTagFromElementAction( + RemoveTagFromElementPayload $payload, + RemoveTagFromElementHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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); - } - - 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(); + public function getBatchAssignmentJobsAction( + GetBatchAssignmentJobsPayload $payload, + GetBatchAssignmentJobsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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 $handler, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } } diff --git a/src/Controller/Admin/TranslationController.php b/src/Controller/Admin/TranslationController.php index bf5fc12d..fccbbfce 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'); - - $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 = [ - '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)); - } - - $response = $this->adminJson($result); - // 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'); + public function importAction( + ImportTranslationsHandler $handler, + ImportTranslationsPayload $payload, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - return $response; + return $this->apiJson($handler($payload), context: [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); } + #[SessionGatewayAware(TranslationImportSessionGateway::class)] #[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); - }, '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, - ], - ]); + public function uploadImportFileAction( + UploadTranslationImportFileHandler $handler, + UploadTranslationImportFilePayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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'); + public function exportAction( + ExportTranslationsHandler $handler, + ExportTranslationsPayload $payload, + ): Response { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - $translation = new Translation(); - $translation->setDomain($domain); - $tableName = $translation->getDao()->getDatabaseTableName(); - - $list = new Translation\Listing(); - $list->setDomain($domain); - - $joins = []; - - $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']]; - } + $result = $handler($payload); - $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 $handler, + AddAdminTranslationKeysPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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'), - ]; - - $conditions = $this->getGridFilterCondition($filterParameters, $tableName, false, $admin); - $filters = $this->getGridFilterCondition($filterParameters, $tableName, true, $admin); - - if ($filters) { - $joins = [...$joins, ...$filters['joins']]; - } - - if ($conditions !== []) { - $list->setCondition($conditions['condition'], $conditions['params']); - } - - $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(), - ]; - } - - return $this->adminJson([ - 'success' => true, - 'data' => $translations, - 'total' => $list->getTotalCount(), - ]); - } - - protected function prefixTranslations(array $translations): array - { - $prefixedTranslations = []; - foreach ($translations as $lang => $trans) { - $prefixedTranslations['_' . $lang] = $trans; - } - - return $prefixedTranslations; + public function translationsAction( + Request $request, + TranslationPayload $payload, + GetTranslationsHandler $handler, + #[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(), + }; + } + + return $this->apiJson($handler($payload)); } - 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']; + #[Route('/translations-destroy', name: 'opendxp_admin_translation_translations_destroy', methods: ['POST'])] + public function translationsDestroyAction( + TranslationPayload $payload, + DeleteTranslationHandler $handler, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - if (isset($alreadyJoined[$fieldName])) { - continue; - } - $alreadyJoined[$fieldName] = 1; + $handler($payload); - $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); - } - } - ); - } + return $this->apiOk(); } - 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, '_'); - } + #[Route('/translations-update', name: 'opendxp_admin_translation_translations_update', methods: ['POST'])] + public function translationsUpdateAction( + TranslationPayload $payload, + UpdateTranslationHandler $handler, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - $fieldName = str_replace('--', '', $fieldName); - if (!$languageMode && in_array($fieldName, $validLanguages)) { - continue; - } - - if ($languageMode && !in_array($fieldName, $validLanguages)) { - continue; - } - - $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate']; - if (!$languageMode && !in_array($fieldName, $allowedNonLanguageFields)) { - 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']; - } + return $this->apiJson($handler($payload)); + } - $conditionFilters = [ - 'condition' => implode(' AND ', $conditions), - 'params' => $params, - ]; - } + #[Route('/translations-create', name: 'opendxp_admin_translation_translations_create', methods: ['POST'])] + public function translationsCreateAction( + TranslationPayload $payload, + CreateTranslationHandler $handler, + ): JsonResponse { + $this->checkPermission(($payload->domain === Translation::DOMAIN_ADMIN ? 'admin_' : '') . 'translations'); - return $conditionFilters; + return $this->apiJson($handler($payload)); } #[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(); + public function cleanupAction( + CleanupTranslationsHandler $handler, + CleanupTranslationsPayload $payload, + ): JsonResponse { + $handler($payload); - \OpenDxp\Cache::clearTags(['translator', 'translate']); - - return $this->adminJson(['success' => true]); + return $this->apiOk(); } /** @@ -667,154 +176,34 @@ 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), - ], - ]]; - } - - return $this->adminJson([ - 'success' => true, - 'jobs' => $jobs, - 'id' => $exportId, - ]); + public function contentExportJobsAction( + BuildContentExportJobsHandler $handler, + BuildContentExportJobsPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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); + public function mergeItemAction( + MergeTranslationItemsHandler $handler, + MergeTranslationItemsPayload $payload, + ): JsonResponse { + $handler($payload); - 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(); - } - - return $this->adminJson([ - 'success' => true, - ]); + return $this->apiOk(); } #[Route('/get-website-translation-languages', name: 'opendxp_admin_translation_getwebsitetranslationlanguages', methods: ['GET'])] - public function getWebsiteTranslationLanguagesAction(Request $request): JsonResponse - { - return $this->adminJson([ - 'view' => $this->getAdminUser()->getAllowedLanguagesForViewingWebsiteTranslations(), - //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(), - ]); + public function getWebsiteTranslationLanguagesAction( + GetWebsiteTranslationLanguagesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } #[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(), - ); - - return $this->adminJson(['domains' => $domains]); + public function getTranslationDomainsAction( + GetTranslationDomainsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler(), envelope: false); } } diff --git a/src/Controller/Admin/User/RoleController.php b/src/Controller/Admin/User/RoleController.php new file mode 100644 index 00000000..30d56e9e --- /dev/null +++ b/src/Controller/Admin/User/RoleController.php @@ -0,0 +1,72 @@ +value)] + #[Route('/user/role-tree-get-children-by-id', name: 'opendxp_admin_user_roletreegetchildrenbyid', methods: ['GET'])] + public function roleTreeGetChildrenByIdAction( + GetRoleTreeChildrenPayload $payload, + GetRoleTreeChildrenHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'roles'); + } + + #[IsGranted(CorePermission::Users->value)] + #[Route('/user/role-get', name: 'opendxp_admin_user_roleget', methods: ['GET'])] + public function roleGetAction( + GetRolePayload $payload, + GetRoleHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::Users->value)] + #[Route('/user/get-roles', name: 'opendxp_admin_user_getroles', methods: ['GET'])] + public function getRolesAction( + GetRolesPayload $payload, + GetRolesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } + + #[IsGranted(CorePermission::ShareConfigurations->value)] + #[Route('/user/get-roles-for-sharing', name: 'opendxp_admin_user_getrolesforsharing', methods: ['GET'])] + public function getRolesForSharingAction( + GetRolesPayload $payload, + GetRolesHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); + } +} diff --git a/src/Controller/Admin/User/UserProfileController.php b/src/Controller/Admin/User/UserProfileController.php new file mode 100644 index 00000000..961d54fc --- /dev/null +++ b/src/Controller/Admin/User/UserProfileController.php @@ -0,0 +1,98 @@ +apiOk(); + } + + #[IsGranted(OwnUserVoter::OWN_USER, subject: 'payload')] + #[SessionGatewayAware(PasswordResetSessionGateway::class)] + #[Route('/user/update-current-user', name: 'opendxp_admin_user_updatecurrentuser', methods: ['PUT'])] + public function updateCurrentUserAction( + UpdateCurrentUserPayload $payload, + UpdateCurrentUserHandler $handler, + ): JsonResponse { + $handler($payload); + + return $this->apiOk(); + } + + #[SessionGatewayAware(PasswordResetSessionGateway::class)] + #[Route('/user/get-current-user', name: 'opendxp_admin_user_getcurrentuser', methods: ['GET'])] + public function getCurrentUserAction( + GetCurrentUserPayload $payload, + GetCurrentUserHandler $handler, + ): Response { + $result = $handler($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( + ResetMy2FaSecretHandler $handler, + ): JsonResponse { + + $handler(); + + return $this->apiOk(); + } + + #[IsGranted(CorePermission::Users->value)] + #[Route('/user/get-default-key-bindings', name: 'opendxp_admin_user_getdefaultkeybindings', methods: ['GET'])] + public function getDefaultKeyBindingsAction(GetDefaultKeyBindingsHandler $handler): JsonResponse + { + return $this->apiJson($handler()); + } +} diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index bdee9899..b487d0fd 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 $handler, + GetUserTreeChildrenPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), rootProperty: 'users'); } + #[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); - } - } - - return $currentList; + public function addAction( + AddUserHandler $handler, + AddUserPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @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 $handler, + DeleteUserPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } - /** - * @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 $handler, + UpdateUserPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } - /** - * @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 $handler, + GetUserPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[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); - - return $this->adminJson([ - 'success' => true, - 'role' => $role->getObjectVars(), - 'permissions' => $role->generatePermissionList(), - 'classes' => $role->getClasses(), - 'docTypes' => $role->getDocTypes(), - 'availablePermissions' => $availableUserPermissions, - 'availablePerspectives' => $availablePerspectives, - 'validLanguages' => Tool::getValidLanguages(), - ]); + public function getMinimalAction( + GetMinimalUserHandler $handler, + GetMinimalUserPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload), envelope: false); } - /** - * @throws Exception - */ + #[AsHtmlContentTypeResponse] + #[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()); - - // 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->headers->set('Content-Type', 'text/html'); + public function uploadImageAction( + UploadUserImageHandler $handler, + UploadUserImagePayload $payload, + ): JsonResponse { + $handler($payload); - return $response; + return $this->apiOk(); } - /** - * @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 $handler, + DeleteUserImagePayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson(['success' => true]); + return $this->apiOk(); } #[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 $handler, + Disable2FaPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'success' => $success, - ]); + return $this->apiOk(); } + #[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 $handler, + Reset2FaSecretPayload $payload, + ): JsonResponse { + $handler($payload); - return $this->adminJson([ - 'success' => true, - ]); + return $this->apiOk(); } #[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); - } - - $token = Tool\Authentication::generateTokenByUser($user); - $link = $this->generateCustomUrl([ - 'token' => $token, - ]); - - return $this->adminJson([ - 'success' => true, - 'link' => $link, - ]); + public function getTokenLoginLinkAction( + GetTokenLoginLinkHandler $handler, + GetTokenLoginLinkPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } + #[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 $handler, + SearchUsersPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($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 $handler, + GetUsersPayload $payload, + ): JsonResponse { + return $this->getUsersAction($handler, $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]); - } - - #[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()]); + public function getUsersAction( + GetUsersHandler $handler, + GetUsersPayload $payload, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * @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 $handler, + SendInvitationLinkPayload $payload, ): JsonResponse { - - $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->apiJson($handler($payload)); } } diff --git a/src/Controller/Admin/WorkflowController.php b/src/Controller/Admin/WorkflowController.php index ec898955..91dc359d 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; - } - } - - 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'] = []; - } - } - } catch (Exception $e) { - $wfConfig['message'] = $e->getMessage(); - } - - return $this->adminJson($wfConfig); + return $this->apiJson($handler($payload), envelope: false); } #[Route('/submit-workflow-transition', name: 'opendxp_admin_workflow_submitworkflowtransition', methods: ['POST'])] - public function submitWorkflowTransitionAction(Request $request, Registry $workflowRegistry, Manager $workflowManager): JsonResponse - { - $workflowOptions = $request->request->all('workflow'); - $workflow = $workflowRegistry->get($this->element, $request->request->get('workflowName')); - - if ($workflow->can($this->element, $request->request->get('transition'))) { - try { - $workflowManager->applyWithAdditionalData($workflow, $this->element, $request->request->get('transition'), $workflowOptions, true); - - $data = [ - 'success' => true, - 'callback' => 'reloadObject', - ]; - } catch (ValidationException $e) { - $reason = ''; - if (count($e->getSubItems()) > 0) { - $reason = ''; - } - - $data = [ - 'success' => false, - 'message' => $e->getMessage(), - 'reasons' => [$reason], - - ]; - } catch (Exception $e) { - $data = [ - 'success' => false, - 'message' => 'error performing action on this element', - 'reasons' => [$e->getMessage()], - ]; - } - } else { - $blockTransitionList = $workflow->buildTransitionBlockerList($this->element, $request->request->get('transition')); - - $reasons = array_map(static fn ($blockTransitionItem) => $blockTransitionItem->getMessage(), iterator_to_array($blockTransitionList->getIterator(), true)); - - $data = [ - 'success' => false, - 'message' => 'transition failed', - 'reasons' => $reasons, - ]; - } - - return $this->adminJson($data); + public function submitWorkflowTransitionAction( + SubmitWorkflowTransitionPayload $payload, + SubmitWorkflowTransitionHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } #[Route('/submit-global-action', name: 'opendxp_admin_workflow_submitglobal', methods: ['POST'])] public function submitGlobalAction( - Request $request, - Registry $workflowRegistry, - Manager $workflowManager + SubmitGlobalActionPayload $payload, + SubmitGlobalActionHandler $handler, ): 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 - ); - - $data = [ - 'success' => true, - 'callback' => 'reloadObject', - ]; - } catch (ValidationException $e) { - $reason = ''; - if (count($e->getSubItems()) > 0) { - $reason = ''; - } - - $data = [ - 'success' => false, - 'message' => $e->getMessage(), - 'reasons' => [$reason], - - ]; - } catch (Exception $e) { - $data = [ - 'success' => false, - 'message' => 'error performing action on this element', - 'reasons' => [$e->getMessage()], - ]; - } - - return $this->adminJson($data); + return $this->apiJson($handler($payload)); } - /** - * 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, - ]; - } - - return $this->adminJson([ - 'data' => $data, - 'success' => true, - 'total' => count($data), - ]); + public function getWorkflowDetailsStore( + GetWorkflowDetailsPayload $payload, + GetWorkflowDetailsHandler $handler, + ): JsonResponse { + return $this->apiJson($handler($payload)); } - /** - * 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 $handler, + ): Response { + $result = $handler($payload); - $response = new Response($this->getWorkflowSvg($workflow)); + $response = new Response($result->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 - { - $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 + public function getModalCustomHtml( + GetModalCustomHtmlPayload $payload, + GetModalCustomHtmlHandler $handler, + ): JsonResponse { - 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, - }; - - if ($this->element === null) { - throw new Exception('Cannot load element' . $cid . ' of type \'' . $ctype . '\''); - } - - //get the latest available version of the element - - $this->element = $this->getLatestVersion($this->element); - $this->element->setUserModification($this->getAdminUser()->getId()); + return $this->apiJson($handler($payload)); } } diff --git a/src/Controller/AdminAbstractController.php b/src/Controller/AdminAbstractController.php index 2808a29f..04132ee7 100644 --- a/src/Controller/AdminAbstractController.php +++ b/src/Controller/AdminAbstractController.php @@ -15,11 +15,15 @@ namespace OpenDxp\Bundle\AdminBundle\Controller; +use JsonSerializable; +use OpenDxp\Bundle\AdminBundle\Handler\ConditionalResultInterface; +use OpenDxp\Bundle\AdminBundle\Handler\ResultInterface; use OpenDxp\Controller\Traits\JsonHelperTrait; use OpenDxp\Controller\UserAwareController; use OpenDxp\Model\User; use OpenDxp\Security\User\User as UserProxy; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; /** * @internal @@ -33,9 +37,68 @@ 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); } + /** + * Serializes a Handler Result into the admin wire response: `success` plus the + * Result's own typed properties. + * `success` is true unless the Result implements ConditionalResultInterface. + * + * TODO: + * Once no Result ever holds a raw domain object as a property, every value handled + * here is already a plain scalar/array (at that point this method (and adminJson()'s + * detour through the opendxp serializer) can be replaced with plain symfony normalizer. + * + * $rootProperty is for endpoints whose established wire contract is the bare value of a + * single Result property (a raw array/list, no envelope) rather than an enveloped + * `{success, ...}` object. + * + * $envelope = false is for endpoints whose established wire contract has multiple + * top-level properties but no `success` key at all. Mutually exclusive with $rootProperty. + * + * $context recognizes AbstractObjectNormalizer::SKIP_NULL_VALUES, reused here only as a + * familiar name: the admin serializer never reaches Symfony's AbstractObjectNormalizer, + * so the flag is interpreted directly below rather than delegated to it. + * Pass it only for the specific action whose Result has a genuinely optional/conditional. + */ + protected function apiJson(ResultInterface $result, int $status = 200, ?string $rootProperty = null, array $context = [], bool $envelope = true): JsonResponse + { + if ($rootProperty !== null) { + if (!property_exists($result, $rootProperty)) { + throw new \LogicException(sprintf('%s has no property "%s" to use as apiJson() rootProperty.', $result::class, $rootProperty)); + } + + return $this->adminJson($result->$rootProperty, $status); + } + + $data = get_object_vars($result); + + if ($context[AbstractObjectNormalizer::SKIP_NULL_VALUES] ?? false) { + $data = array_filter($data, static fn (mixed $value): bool => $value !== null); + } + + if (!$envelope) { + return $this->adminJson($data, $status); + } + + $success = $result instanceof ConditionalResultInterface ? $result->isSuccessful() : true; + + return $this->adminJson(['success' => $success, ...$data], $status); + } + + /** + * Wire response for void Command handlers: no data to report, only that it succeeded. + */ + protected function apiOk(int $status = 200): JsonResponse + { + return $this->adminJson(['success' => true], $status); + } + /** * Get user from user proxy object which is registered on security component */ diff --git a/src/Controller/GDPR/AdminController.php b/src/Controller/GDPR/AdminController.php index 7b87f545..96818275 100644 --- a/src/Controller/GDPR/AdminController.php +++ b/src/Controller/GDPR/AdminController.php @@ -18,37 +18,21 @@ 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\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): 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->apiJson($handler(), rootProperty: 'providers'); } } diff --git a/src/Controller/GDPR/AssetController.php b/src/Controller/GDPR/AssetController.php index b3f84d01..f5d3a282 100644 --- a/src/Controller/GDPR/AssetController.php +++ b/src/Controller/GDPR/AssetController.php @@ -16,64 +16,40 @@ 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->apiJson($handler($payload), rootProperty: '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'); - } + $result = $handler($payload); + + $response = new Response($result->zipContent); + $response->headers->set('Content-Type', 'application/zip'); + $response->headers->set('Content-Length', (string) strlen($result->zipContent)); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $result->suggestedFilename . '.zip"'); - return $service->doExportData($asset); + return $response; } } diff --git a/src/Controller/GDPR/DataObjectController.php b/src/Controller/GDPR/DataObjectController.php index 8a32dc2d..9c63dad7 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->apiJson($handler($payload), rootProperty: '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..e3e956fb 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->apiJson($handler($payload), rootProperty: '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..56e43724 100644 --- a/src/Controller/Traits/DocumentTreeConfigTrait.php +++ b/src/Controller/Traits/DocumentTreeConfigTrait.php @@ -16,8 +16,7 @@ namespace OpenDxp\Bundle\AdminBundle\Controller\Traits; -use Exception; -use OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface; +use OpenDxp\Bundle\AdminBundle\Service\Element\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/Coordinator/Asset/AssetPersistenceCoordinator.php b/src/Coordinator/Asset/AssetPersistenceCoordinator.php new file mode 100644 index 00000000..41a9fbb4 --- /dev/null +++ b/src/Coordinator/Asset/AssetPersistenceCoordinator.php @@ -0,0 +1,51 @@ +setUserModification($this->userContext->getAdminUser()->getId()); + + if ($task === 'session') { + $this->elementDraftService->saveAsset($asset); + } else { + $asset->save(); + } + + return new AssetPersistenceDto( + data: [ + 'versionDate' => $asset->getModificationDate() ?? 0, + 'versionCount' => $asset->getVersionCount(), + ], + treeData: $this->elementService->getElementTreeNodeConfig($asset), + ); + } +} diff --git a/src/Coordinator/DataObject/DataObjectPersistenceCoordinator.php b/src/Coordinator/DataObject/DataObjectPersistenceCoordinator.php new file mode 100644 index 00000000..d67286c3 --- /dev/null +++ b/src/Coordinator/DataObject/DataObjectPersistenceCoordinator.php @@ -0,0 +1,133 @@ +elementDraftService->saveObject($object); + + return new DataObjectPersistenceDto( + general: [ + 'modificationDate' => 0, + 'versionDate' => 0, + 'versionCount' => 0 + ], + treeData: [], + ); + } + + $adminUser = $this->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 DataObjectPersistenceDto( + general: [ + 'modificationDate' => $object->getModificationDate() ?? 0, + 'versionDate' => $newObject->getModificationDate() ?? 0, + 'versionCount' => $newObject->getVersionCount(), + ], + treeData: $treeData, + ); + } + + if ($task === 'scheduler' && $object->isAllowed('settings')) { + $object->saveScheduledTasks(); + + return new DataObjectPersistenceDto( + general: [ + 'modificationDate' => $object->getModificationDate() ?? 0, + 'versionDate' => $object->getModificationDate() ?? 0, + 'versionCount' => $object->getVersionCount(), + ], + treeData: [], + ); + } + + 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 DataObjectPersistenceDto( + general: [ + 'modificationDate' => $object->getModificationDate() ?? 0, + 'versionDate' => $newObject->getModificationDate() ?? 0, + 'versionCount' => $newObject->getVersionCount(), + ], + treeData: $treeData, + draft: $draftData, + ); + } + + throw new AccessDeniedHttpException('Missing permission to save object'); + } +} diff --git a/src/Coordinator/Document/DocumentPersistenceCoordinator.php b/src/Coordinator/Document/DocumentPersistenceCoordinator.php new file mode 100644 index 00000000..82004080 --- /dev/null +++ b/src/Coordinator/Document/DocumentPersistenceCoordinator.php @@ -0,0 +1,81 @@ +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($this->userContext->getAdminUser()->getId()); + } + + return new DocumentPersistenceDto( + data: [ + 'versionDate' => $document->getModificationDate(), + 'versionCount' => $document->getVersionCount(), + ], + treeData: $this->elementService->getElementTreeNodeConfig($document), + draft: $version ? [ + 'id' => $version->getId(), + 'modificationDate' => $version->getDate(), + 'isAutoSave' => $version->isAutoSave(), + ] : null, + ); + } +} diff --git a/src/DependencyInjection/OpenDxpAdminExtension.php b/src/DependencyInjection/OpenDxpAdminExtension.php index a9490a1e..b086ee4d 100644 --- a/src/DependencyInjection/OpenDxpAdminExtension.php +++ b/src/DependencyInjection/OpenDxpAdminExtension.php @@ -53,6 +53,7 @@ public function load(array $configs, ContainerBuilder $container): void ); $loader->load('services.yaml'); + $loader->load('legacy_services.yaml'); $loader->load('security_services.yaml'); $loader->load('event_listeners.yaml'); $loader->load('export.yaml'); diff --git a/src/Dto/Admin/AdminSettingsDto.php b/src/Dto/Admin/AdminSettingsDto.php new file mode 100644 index 00000000..e9f2a449 --- /dev/null +++ b/src/Dto/Admin/AdminSettingsDto.php @@ -0,0 +1,195 @@ + $this->instanceId, + 'version' => $this->version, + 'build' => $this->build, + 'debug' => $this->debug, + 'devmode' => $this->devMode, + 'disableMinifyJs' => $this->disableMinifyJs, + 'environment' => $this->environment, + 'sessionId' => $this->sessionId, + + 'language' => $this->language, + 'websiteLanguages' => $this->websiteLanguages, + 'requiredLanguages' => $this->requiredLanguages, + + 'showCloseConfirmation' => true, + 'debug_admin_translations' => $this->debugAdminTranslations, + 'document_generatepreviews' => $this->generateDocumentPreviews, + 'asset_disable_tree_preview' => $this->disableAssetTreePreview, + 'asset_hide_edit' => $this->hideEditImage, + 'asset_tree_paging_limit' => $this->assetTreePagingLimit, + 'asset_default_upload_path' => $this->assetDefaultUploadPath, + 'chromium' => $this->chromiumAvailable, + 'videoconverter' => $this->videoConverterAvailable, + 'main_domain' => $this->mainDomain, + 'custom_admin_entrypoint_url' => $this->customAdminEntrypointUrl, + 'timezone' => $this->timezone, + 'tile_layer_url_template' => $this->tileLayerUrlTemplate, + 'geocoding_url_template' => $this->geocodingUrlTemplate, + 'reverse_geocoding_url_template' => $this->reverseGeocodingUrlTemplate, + 'document_tree_paging_limit' => $this->documentTreePagingLimit, + 'object_tree_paging_limit' => $this->objectTreePagingLimit, + 'hostname' => $this->hostname, + 'dependency' => $this->dependencyEnabled, + 'document_auto_save_interval' => $this->documentAutoSaveInterval, + 'object_auto_save_interval' => $this->objectAutoSaveInterval, + + 'perspective' => $this->perspective, + 'availablePerspectives' => $this->availablePerspectives, + 'disabledPortlets' => $this->disabledPortlets, + + 'image-thumbnails-writeable' => $this->imageThumbnailsWriteable, + 'video-thumbnails-writeable' => $this->videoThumbnailsWriteable, + 'document-types-writeable' => $this->documentTypesWriteable, + 'predefined-properties-writeable' => $this->predefinedPropertiesWriteable, + 'predefined-asset-metadata-writeable' => $this->predefinedAssetMetadataWriteable, + 'perspectives-writeable' => $this->perspectivesWriteable, + 'custom-views-writeable' => $this->customViewsWriteable, + 'class-definition-writeable' => $this->classDefinitionWriteable, + 'object-custom-layout-writeable' => $this->objectCustomLayoutWriteable, + 'select-options-writeable' => $this->selectOptionsWriteable, + + 'asset_search_types' => $this->assetSearchTypes, + 'document_types_configuration' => $this->documentTypesConfiguration, + 'document_search_types' => $this->documentSearchTypes, + 'document_valid_types' => $this->documentValidTypes, + 'document_email_search_types' => $this->documentEmailSearchTypes, + 'select_options_provider_class' => $this->selectOptionsProviderClass, + + 'upload_max_filesize' => $this->uploadMaxFilesize, + 'session_gc_maxlifetime' => $this->sessionGcMaxlifetime, + + 'maintenance_active' => $this->maintenanceActive, + 'maintenance_mode' => $this->maintenanceMode, + + 'mail' => $this->mailConfigured, + 'mailDefaultAddress' => $this->mailDefaultAddress, + + 'customviews' => $this->customViews, + + 'notifications_enabled' => $this->notificationsEnabled, + 'checknewnotification_enabled' => $this->checkNewNotificationEnabled, + 'checknewnotification_interval' => $this->checkNewNotificationInterval, + + 'csrfToken' => $this->csrfToken, + ]; + } +} diff --git a/src/Dto/Admin/StatisticsDto.php b/src/Dto/Admin/StatisticsDto.php new file mode 100644 index 00000000..f0d657ca --- /dev/null +++ b/src/Dto/Admin/StatisticsDto.php @@ -0,0 +1,43 @@ + $this->instanceId, + 'revision' => $this->revision, + 'version' => $this->version, + 'major_version' => $this->majorVersion, + 'php_version' => $this->phpVersion, + 'db_version' => $this->dbVersion, + 'bundles' => $this->bundles, + ]; + } +} diff --git a/src/Dto/Asset/AssetPersistenceDto.php b/src/Dto/Asset/AssetPersistenceDto.php new file mode 100644 index 00000000..794a7ed3 --- /dev/null +++ b/src/Dto/Asset/AssetPersistenceDto.php @@ -0,0 +1,28 @@ + $this->onlyUnreferenced]; + } +} diff --git a/src/Dto/Grid/DataObjectGridColumnConfig.php b/src/Dto/Grid/DataObjectGridColumnConfig.php new file mode 100644 index 00000000..be172aa3 --- /dev/null +++ b/src/Dto/Grid/DataObjectGridColumnConfig.php @@ -0,0 +1,55 @@ + $this->language, + 'searchFilter' => $this->searchFilter, + 'filter' => $this->filter, + ]; + } +} diff --git a/src/Dto/Grid/GridColumnConfig.php b/src/Dto/Grid/GridColumnConfig.php new file mode 100644 index 00000000..fa9ff524 --- /dev/null +++ b/src/Dto/Grid/GridColumnConfig.php @@ -0,0 +1,51 @@ + $this->sortinfo, + 'availableFields' => $this->availableFields, + 'settings' => $this->settings, + 'onlyDirectChildren' => $this->onlyDirectChildren, + 'pageSize' => $this->pageSize, + 'availableConfigs' => $this->availableConfigs, + 'sharedConfigs' => $this->sharedConfigs, + 'context' => $this->context, + ]; + } +} diff --git a/src/Dto/Grid/GridConfigData.php b/src/Dto/Grid/GridConfigData.php new file mode 100644 index 00000000..a1c1025c --- /dev/null +++ b/src/Dto/Grid/GridConfigData.php @@ -0,0 +1,38 @@ +id === 0; + } +} diff --git a/src/Enricher/DataObject/CustomLayoutEnricher.php b/src/Enricher/DataObject/CustomLayoutEnricher.php new file mode 100644 index 00000000..21c01f60 --- /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..dab11d32 --- /dev/null +++ b/src/Enricher/DataObject/DraftEnricher.php @@ -0,0 +1,40 @@ +getId() ?? 0, ['force' => true]) ?? $object; + 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..33024c78 --- /dev/null +++ b/src/Enricher/Document/DocumentMetaEnricher.php @@ -0,0 +1,32 @@ +getId() ?? 0, ['force' => true]) ?? $document; + $data['versionDate'] = $fresh->getModificationDate(); + $data['userPermissions'] = $document->getUserPermissions(); + $data['idPath'] = ElementService::getIdPath($document); + } +} diff --git a/src/Enricher/Document/DraftEnricher.php b/src/Enricher/Document/DraftEnricher.php new file mode 100644 index 00000000..f5fd2e3e --- /dev/null +++ b/src/Enricher/Document/DraftEnricher.php @@ -0,0 +1,40 @@ +getId() ?? 0, ['force' => true]) ?? $document; + 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/PhpMetaEnricher.php b/src/Enricher/Element/PhpMetaEnricher.php new file mode 100644 index 00000000..867fa84e --- /dev/null +++ b/src/Enricher/Element/PhpMetaEnricher.php @@ -0,0 +1,34 @@ + [ + $element::class, + ...array_values(class_parents($element)) + ], + 'interfaces' => array_values(class_implements($element)), + ]; + } +} \ No newline at end of file diff --git a/src/Enricher/Element/PreSendDataEventEnricher.php b/src/Enricher/Element/PreSendDataEventEnricher.php new file mode 100644 index 00000000..a83a382a --- /dev/null +++ b/src/Enricher/Element/PreSendDataEventEnricher.php @@ -0,0 +1,57 @@ + AdminEvents::DOCUMENT_GET_PRE_SEND_DATA, + 'asset' => AdminEvents::ASSET_GET_PRE_SEND_DATA, + 'object' => AdminEvents::OBJECT_GET_PRE_SEND_DATA, + default => throw new LogicException(sprintf('No GET_PRE_SEND_DATA event known for element class "%s".', $element::class)), + }; + + $event = new GenericEvent(null, [ + 'data' => $data, + $elementType => $element, + ]); + + $this->eventDispatcher->dispatch($event, $eventName); + $eventData = $event->getArgument('data'); + + $data = is_array($eventData) ? $eventData : $data; + } +} \ No newline at end of file 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/AdminExceptionListener.php b/src/EventListener/AdminExceptionListener.php index f5b12841..c93cdf38 100644 --- a/src/EventListener/AdminExceptionListener.php +++ b/src/EventListener/AdminExceptionListener.php @@ -21,6 +21,8 @@ use Exception; use OpenDxp; use OpenDxp\Bundle\CoreBundle\EventListener\Traits\OpenDxpContextAwareTrait; +use OpenDxp\Bundle\AdminBundle\Exception\AdminOperationFailedException; +use OpenDxp\Bundle\AdminBundle\Exception\ElementLockedException; use OpenDxp\Http\Request\Resolver\OpenDxpContextResolver; use OpenDxp\Model\Element\ValidationException; use OpenDxp\Model\Exception\ConfigWriteException; @@ -51,46 +53,76 @@ public function onKernelException(ExceptionEvent $event): void $request = $event->getRequest(); $ex = $event->getThrowable(); - if ($this->matchesOpenDxpContext($request, OpenDxpContextResolver::CONTEXT_ADMIN)) { - // only return JSON error for XHR requests - if (!$request->isXmlHttpRequest()) { - return; - } + if (!$this->matchesOpenDxpContext($request, OpenDxpContextResolver::CONTEXT_ADMIN)) { + return; + } - [$code, $headers, $message] = $this->getResponseData($ex); + // only return JSON error for XHR requests + if (!$request->isXmlHttpRequest()) { + return; + } - $data = [ - 'success' => false, - 'traceString' => '', - ]; + [$code, $headers, $message] = $this->getResponseData($ex); - // DBAL exceptions do include SQL statements, we don't want to expose them - if (!OpenDxp::inDebugMode() && $ex instanceof DBALException) { - $message = 'Database error, see logs for details'; - } + $data = [ + 'success' => false, + 'traceString' => '', + ]; - if (OpenDxp::inDebugMode()) { - $data['trace'] = $ex->getTrace(); - $data['traceString'] = 'in ' . $ex->getFile() . ':' . $ex->getLine() . "\n" . $ex->getTraceAsString(); - } + // DBAL exceptions do include SQL statements, we don't want to expose them + if (!OpenDxp::inDebugMode() && $ex instanceof DBALException) { + $message = 'Database error, see logs for details'; + } - if ($ex instanceof ValidationException) { - $data['type'] = 'ValidationException'; - $code = 422; + if (OpenDxp::inDebugMode()) { + $data['trace'] = $ex->getTrace(); + $data['traceString'] = 'in ' . $ex->getFile() . ':' . $ex->getLine() . "\n" . $ex->getTraceAsString(); + } - $this->recursiveAddValidationExceptionSubItems($ex->getSubItems(), $message, $data['traceString']); - } + if ($ex instanceof ValidationException) { + $data['type'] = 'ValidationException'; + $code = 422; - if ($ex instanceof ConfigWriteException) { - $data['type'] = 'ConfigWriteException'; - $code = 422; + $this->recursiveAddValidationExceptionSubItems($ex->getSubItems(), $message, $data['traceString']); + } + + if ($ex instanceof ConfigWriteException) { + $data['type'] = 'ConfigWriteException'; + $code = 422; + } + + // expected, recoverable business-rule failure: the ExtJS admin UI's success/failure + // handling for this call site expects a 200 with success:false, not a real HTTP error + if ($ex instanceof AdminOperationFailedException) { + $code = 200; + $data = array_merge($data, $ex->getExtra()); + + // without this, HttpKernel::handleThrowable() forces non-error response codes + // from an exception listener back to 500, since it assumes that's a mistake + $event->allowCustomResponseCode(); + } + + // element is locked for editing: the ExtJS admin UI expects a 200 with an + // "editlock" payload to show the lock dialog, not a real HTTP error + if ($ex instanceof ElementLockedException) { + $editLock = $ex->getEditLock(); + $editLockData = $editLock->getObjectVars(); + unset($editLockData['sessionId']); + + if ($user = $editLock->getUser()) { + $editLockData['user'] = ['name' => $user->getName()]; } - $data['message'] = $message; + $data['editlock'] = $editLockData; + $code = 200; - $response = new JsonResponse($data, $code, $headers); - $event->setResponse($response); + $event->allowCustomResponseCode(); } + + $data['message'] = $message; + + $response = new JsonResponse($data, $code, $headers); + $event->setResponse($response); } private function getResponseData(Throwable $ex, int $defaultStatusCode = 500): array diff --git a/src/EventListener/HtmlContentTypeResponseListener.php b/src/EventListener/HtmlContentTypeResponseListener.php new file mode 100644 index 00000000..1faf29bb --- /dev/null +++ b/src/EventListener/HtmlContentTypeResponseListener.php @@ -0,0 +1,58 @@ + 'onKernelControllerArguments', + KernelEvents::RESPONSE => 'onKernelResponse', + ]; + } + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + if ($event->getAttributes(AsHtmlContentTypeResponse::class) !== []) { + $event->getRequest()->attributes->set(self::REQUEST_ATTRIBUTE, true); + } + } + + public function onKernelResponse(ResponseEvent $event): void + { + if ($event->getRequest()->attributes->get(self::REQUEST_ATTRIBUTE) === true) { + $event->getResponse()->headers->set('Content-Type', 'text/html'); + } + } +} 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/AdminOperationFailedException.php b/src/Exception/AdminOperationFailedException.php new file mode 100644 index 00000000..ff314fdc --- /dev/null +++ b/src/Exception/AdminOperationFailedException.php @@ -0,0 +1,42 @@ + $extra additional keys merged into the JSON response body + */ + public function __construct(string $message = '', private readonly array $extra = []) + { + parent::__construct($message); + } + + /** + * @return array + */ + public function getExtra(): array + { + return $this->extra; + } +} 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/Login/TwoFactorCodeInvalidException.php b/src/Exception/Login/TwoFactorCodeInvalidException.php new file mode 100644 index 00000000..c92fb867 --- /dev/null +++ b/src/Exception/Login/TwoFactorCodeInvalidException.php @@ -0,0 +1,19 @@ +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/GDPR/DataProvider/Assets.php b/src/GDPR/DataProvider/Assets.php index 9b62ecda..aee1158c 100644 --- a/src/GDPR/DataProvider/Assets.php +++ b/src/GDPR/DataProvider/Assets.php @@ -19,10 +19,9 @@ use Doctrine\DBAL\Exception; use OpenDxp\Bundle\AdminBundle\Helper\QueryParams; -use OpenDxp\Bundle\AdminBundle\Service\GridData; +use OpenDxp\Bundle\AdminBundle\Mapper\GridData; use OpenDxp\Model\Asset; use OpenDxp\Model\Element; -use Symfony\Component\HttpFoundation\Response; use ZipArchive; /** @@ -52,7 +51,7 @@ public function getJsClassName(): string /** * Exports data of given asset as json */ - public function doExportData(Asset $asset): Response + public function doExportData(Asset $asset): string { $this->exportIds = []; $this->exportIds[$asset->getId()] = true; @@ -80,16 +79,10 @@ public function doExportData(Asset $asset): Response $zip->close(); - $size = filesize($file); $content = file_get_contents($file); unlink($file); - $response = new Response($content); - $response->headers->set('Content-Type', 'application/zip'); - $response->headers->set('Content-Length', (string) $size); - $response->headers->set('Content-Disposition', 'attachment; filename="' . $asset->getFilename() . '.zip"'); - - return $response; + return $content; } /** diff --git a/src/GDPR/DataProvider/DataObjects.php b/src/GDPR/DataProvider/DataObjects.php index 12c21771..19532f07 100644 --- a/src/GDPR/DataProvider/DataObjects.php +++ b/src/GDPR/DataProvider/DataObjects.php @@ -18,7 +18,7 @@ namespace OpenDxp\Bundle\AdminBundle\GDPR\DataProvider; use OpenDxp\Bundle\AdminBundle\Helper\QueryParams; -use OpenDxp\Bundle\AdminBundle\Service\GridData; +use OpenDxp\Bundle\AdminBundle\Mapper\GridData; use OpenDxp\Model\Asset; use OpenDxp\Model\DataObject\AbstractObject; use OpenDxp\Model\DataObject\Concrete; diff --git a/src/Generator/CustomLoginUrlGenerator.php b/src/Generator/CustomLoginUrlGenerator.php new file mode 100644 index 00000000..9c5cf9e5 --- /dev/null +++ b/src/Generator/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/Generator/PreviewGenerator.php b/src/Generator/PreviewGenerator.php new file mode 100644 index 00000000..92ac3647 --- /dev/null +++ b/src/Generator/PreviewGenerator.php @@ -0,0 +1,141 @@ +getClass()->getLinkGenerator(); + + if ($linkGenerator instanceof LinkGeneratorInterface) { + $filteredParameters = $this->filterParameters($object, $params); + + $locale = $filteredParameters[PreviewGeneratorInterface::PARAMETER_LOCALE] ?? Tool::getDefaultLanguage(); + $site = array_key_exists(PreviewGeneratorInterface::PARAMETER_SITE, $filteredParameters) ? Site::getById($filteredParameters[PreviewGeneratorInterface::PARAMETER_SITE]) : (new Site\Listing())->current(); + + return $linkGenerator->generate($object, [ + PreviewGeneratorInterface::PARAMETER_LOCALE => $locale, + PreviewGeneratorInterface::PARAMETER_SITE => $site, + ]); + } + + throw new LogicException("No link generator given for element of type {$object->getClassName()}"); + } + + /** + * @return array only parameters that are part of the preview generator config and are not empty + */ + protected function filterParameters(Concrete $object, array $parameters): array + { + $previewConfig = $this->getPreviewConfig($object); + + $filteredParameters = []; + foreach ($previewConfig as $config) { + $name = $config['name']; + $selectedValue = $parameters[$name] ?? $config['defaultValue']; + + if (!empty($selectedValue)) { + $filteredParameters[$name] = $selectedValue; + } + } + + return $filteredParameters; + } + + public function getPreviewConfig(Concrete $object): array + { + return array_filter([ + $this->getLocalePreviewConfig($object), + $this->getSitePreviewConfig($object), + ]); + } + + protected function getLocalePreviewConfig(Concrete $object): array + { + $user = Tool\Authentication::authenticateSession(); + $userLocale = $user->getLanguage(); + + $locales = []; + foreach (Tool::getValidLanguages() as $locale) { + $label = sprintf('%s (%s)', Locale::getDisplayLanguage($locale, $userLocale), $locale); + $locales[$label] = $locale; + } + + return [ + 'name' => PreviewGeneratorInterface::PARAMETER_LOCALE, + 'label' => $this->translator->trans('preview_generator_locale', [], Translation::DOMAIN_ADMIN), + 'values' => $locales, + 'defaultValue' => in_array($userLocale, Tool::getValidLanguages()) ? $userLocale : Tool::getDefaultLanguage(), + ]; + } + + protected function getSitePreviewConfig(Concrete $object): array + { + $sites = new Site\Listing(); + $sites->setOrderKey('mainDomain')->setOrder('ASC'); + + if ($sites->count() === 0) { + return []; + } + + $sitesOptions = [ + $this->translator->trans('main_site', [], Translation::DOMAIN_ADMIN) => '0', + ]; + + $preSelectedSite = null; + foreach ($sites as $site) { + $label = $site->getRootDocument()?->getKey(); + $sitesOptions[$label] = $site->getId(); + + $domains = $site->getDomains(); + array_unshift($domains, $site->getMainDomain()); + + if (is_null($preSelectedSite) && in_array(Tool::getHostname(), $domains)) { + $preSelectedSite = $sitesOptions[$label]; + } + } + + return [ + 'name' => PreviewGeneratorInterface::PARAMETER_SITE, + 'label' => $this->translator->trans('preview_generator_site', [], Translation::DOMAIN_ADMIN), + 'values' => $sitesOptions, + 'defaultValue' => $preSelectedSite ?? reset($sitesOptions), + ]; + } + + /** + * @internal + */ + #[Required] + public function setTranslator(Translator $translator): void + { + $this->translator = $translator; + } +} diff --git a/src/Handler/Admin/Settings/SettingsHandler.php b/src/Handler/Admin/Settings/SettingsHandler.php new file mode 100644 index 00000000..f4b81518 --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsHandler.php @@ -0,0 +1,63 @@ +userContext->getAdminUser(); + $dto = $this->adminSettingsService->createSettings($payload->locale, $user); + $settings = $dto->asSettingsArray(); + + $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..61bb9ae9 --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsPayload.php @@ -0,0 +1,34 @@ +getLocale(), + ); + } +} diff --git a/src/Handler/Admin/Settings/SettingsResult.php b/src/Handler/Admin/Settings/SettingsResult.php new file mode 100644 index 00000000..f4aca8fd --- /dev/null +++ b/src/Handler/Admin/Settings/SettingsResult.php @@ -0,0 +1,27 @@ +adminStatisticsService->createStatistics()->asStatisticsArray(); + } 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..8a12547c --- /dev/null +++ b/src/Handler/Asset/AssetResult.php @@ -0,0 +1,28 @@ +assetId); + + if (!$asset) { + throw new AdminOperationFailedException('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..d2d9a5da --- /dev/null +++ b/src/Handler/Asset/ClearAssetThumbnail/ClearAssetThumbnailPayload.php @@ -0,0 +1,35 @@ +request->getString('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..d1d77dc3 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetHandler.php @@ -0,0 +1,87 @@ +sourceId; + $targetId = $payload->targetId; + $type = $payload->type; + $sourceParentId = $payload->sourceParentId; + $targetParentId = $payload->targetParentId; + $sessionParentId = $this->copySession->getParentId($payload->transactionId); + $source = Asset::getById($sourceId); + + if ($source === null) { + throw new AdminOperationFailedException('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); + + if ($payload->saveParentId) { + $this->copySession->rememberParentId($payload->transactionId, $newAsset->getId()); + } + + 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..5d15fa25 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetPayload.php @@ -0,0 +1,47 @@ +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, + 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..0baeba06 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyAsset/CopyAssetResult.php @@ -0,0 +1,28 @@ +copySession->startTransaction((string) $transactionId); + $pasteJobs = []; + + if ($payload->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($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..eb9299f6 --- /dev/null +++ b/src/Handler/Asset/Copy/CopyInfo/CopyInfoPayload.php @@ -0,0 +1,39 @@ +query->getString('type') ?: null, + sourceId: (int) $request->query->getString('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..543838de --- /dev/null +++ b/src/Handler/Asset/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,27 @@ +parentId; + $name = $payload->name; + $adminUser = $this->userContext->getAdminUser(); + $parentAsset = Asset::getById($parentId); + + if (!$parentAsset->isAllowed('create')) { + throw new AdminOperationFailedException('prevented creating asset because of missing permissions'); + } + + if (Asset::getByPath($parentAsset->getRealFullPath() . '/' . $name)) { + throw new AdminOperationFailedException('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..a70e9b3a --- /dev/null +++ b/src/Handler/Asset/CreateAssetFolder/CreateAssetFolderPayload.php @@ -0,0 +1,37 @@ +request->getString('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..11827067 --- /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 AdminOperationFailedException('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..2608d330 --- /dev/null +++ b/src/Handler/Asset/DeleteAsset/DeleteAssetPayload.php @@ -0,0 +1,39 @@ +request->getString('type'), + id: (int) $request->request->getString('id'), + amount: (int) $request->request->getString('amount'), + ); + } +} diff --git a/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php b/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php new file mode 100644 index 00000000..5b543787 --- /dev/null +++ b/src/Handler/Asset/DeleteAsset/DeleteAssetResult.php @@ -0,0 +1,32 @@ + $deleted asset id => real full path of each deleted asset; + * null unless this was a batch ("children") delete + */ + public function __construct( + public ?array $deleted = null, + ) {} +} + diff --git a/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipHandler.php b/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipHandler.php new file mode 100644 index 00000000..3ef94722 --- /dev/null +++ b/src/Handler/Asset/Download/AddFilesToZip/AddFilesToZipHandler.php @@ -0,0 +1,100 @@ +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 AdminOperationFailedException(''); + } + + $zip = new ZipArchive(); + $zipState = is_file($zipFile) ? $zip->open($zipFile) : $zip->open($zipFile, ZipArchive::CREATE); + + if ($zipState !== true) { + throw new AdminOperationFailedException('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..81f09463 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailPayload.php @@ -0,0 +1,58 @@ +query->getString('thumbnail') ?: null; + $config = $request->query->getString('config') ?: null; + $type = $request->query->getString('type') ?: null; + + $configData = null; + if ($config !== null) { + $decoded = json_decode($config, true); + $configData = is_array($decoded) ? $decoded : null; + } 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..4b3e8f48 --- /dev/null +++ b/src/Handler/Asset/Download/DownloadImageThumbnail/DownloadImageThumbnailResult.php @@ -0,0 +1,31 @@ +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..0af62dfd --- /dev/null +++ b/src/Handler/Asset/Download/DownloadZip/DownloadZipResult.php @@ -0,0 +1,28 @@ +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..a4cd4043 --- /dev/null +++ b/src/Handler/Asset/Download/GetDownloadZipJobs/GetDownloadZipJobsResult.php @@ -0,0 +1,28 @@ +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..7a7f2eed --- /dev/null +++ b/src/Handler/Asset/Editor/LoadAssetForEditor/LoadAssetForEditorPayload.php @@ -0,0 +1,35 @@ +query->getString('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..9b77e42b --- /dev/null +++ b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorHandler.php @@ -0,0 +1,53 @@ +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'); + } + + $commaPosition = strpos($dataUri, ','); + $data = $commaPosition !== false ? substr($dataUri, $commaPosition) : $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..773bc472 --- /dev/null +++ b/src/Handler/Asset/Editor/SaveImageEditor/SaveImageEditorPayload.php @@ -0,0 +1,37 @@ +query->getString('id'), + dataUri: $request->request->getString('dataUri'), + ); + } +} diff --git a/src/Handler/Asset/GetAssetData/GetAssetDataHandler.php b/src/Handler/Asset/GetAssetData/GetAssetDataHandler.php new file mode 100644 index 00000000..a621a672 --- /dev/null +++ b/src/Handler/Asset/GetAssetData/GetAssetDataHandler.php @@ -0,0 +1,196 @@ +id; + $requestSchemeAndHost = $payload->requestSchemeAndHost; + $asset = Asset::getById($id); + if (!$asset instanceof Asset) { + throw new AdminOperationFailedException(sprintf('Asset with id %d not found', $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); + $this->phpMetaEnricher->enrich($asset, $data); + $this->preSendDataEventEnricher->enrich($asset, $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..c2d307bd --- /dev/null +++ b/src/Handler/Asset/GetAssetData/GetAssetDataResult.php @@ -0,0 +1,27 @@ +params; + + $filterPrepareEvent = new GenericEvent(null, ['requestParams' => $params]); + $this->eventDispatcher->dispatch($filterPrepareEvent, AdminEvents::ASSET_LIST_BEFORE_FILTER_PREPARE); + $params = $filterPrepareEvent->getArgument('requestParams'); + + try { + $result = $this->assetGridService->gridProxy($params, $payload->language); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new GridProxyResult($result); + } +} 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..dcabd18a --- /dev/null +++ b/src/Handler/Asset/GridProxy/GridProxyResult.php @@ -0,0 +1,27 @@ +gridConfigId; + $adminUser = $this->userContext->getAdminUser(); + $gridConfig = GridConfig::getById($gridConfigId); + if (!$gridConfig) { + throw new AdminOperationFailedException('Grid config not found: ' . $gridConfigId); + } + + if ($gridConfig->getOwnerId() !== $adminUser->getId()) { + throw new BadRequestHttpException("don't mess with someone elses grid config"); + } + + $gridConfig->delete(); + + $params = [ + 'id' => $payload->id, + 'types' => $payload->types, + 'gridConfigId' => $gridConfigId, + 'searchType' => $payload->searchType, + 'noSystemColumns' => $payload->noSystemColumns, + ]; + + $config = $this->gridConfigResolver->resolve($params, true); + + return new DeleteGridColumnConfigResult([...$config->toArray(), 'deleteSuccess' => true]); + } +} diff --git a/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php new file mode 100644 index 00000000..426db7ab --- /dev/null +++ b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigPayload.php @@ -0,0 +1,43 @@ +request->getInt('gridConfigId'), + noSystemColumns: (bool) $request->query->get('no_system_columns'), + id: $request->request->getString('id'), + types: $request->request->getString('types'), + searchType: $request->request->getString('searchType'), + ); + } +} diff --git a/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php new file mode 100644 index 00000000..ba3ca133 --- /dev/null +++ b/src/Handler/Asset/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php @@ -0,0 +1,27 @@ +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 AdminOperationFailedException(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..a7633a18 --- /dev/null +++ b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchHandler.php @@ -0,0 +1,44 @@ +data ?? []; + $adminUser = $this->userContext->getAdminUser(); + + try { + $this->gridBatchService->executeAssetBatch($data, $adminUser); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } +} diff --git a/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php new file mode 100644 index 00000000..2ef729ba --- /dev/null +++ b/src/Handler/Asset/Helper/ExecuteAssetBatch/ExecuteAssetBatchPayload.php @@ -0,0 +1,38 @@ +request->getString('data'); + $decoded = $raw !== '' ? json_decode($raw, true) : null; + + return new static( + data: is_array($decoded) ? $decoded : 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..27e9b64e --- /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..44c3a7cc --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetBatchJobs/GetAssetBatchJobsResult.php @@ -0,0 +1,27 @@ + $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..53762d47 --- /dev/null +++ b/src/Handler/Asset/Helper/GetAssetMetadataForColumnConfig/GetAssetMetadataForColumnConfigResult.php @@ -0,0 +1,27 @@ +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..3f825576 --- /dev/null +++ b/src/Handler/Asset/Helper/GetExportJobs/GetExportJobsResult.php @@ -0,0 +1,28 @@ + $payload->id, + 'types' => $payload->types, + 'gridConfigId' => $payload->gridConfigId, + 'searchType' => $payload->searchType, + 'noSystemColumns' => $payload->noSystemColumns, + ]; + + $config = $this->gridConfigResolver->resolve($params); + + return new GetGridColumnConfigResult($config->toArray()); + } +} diff --git a/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php b/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php new file mode 100644 index 00000000..72605f23 --- /dev/null +++ b/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php @@ -0,0 +1,42 @@ +query->getString('id') ?: null, + types: $request->query->getString('types') ?: 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'), + ); + } +} diff --git a/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php b/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php new file mode 100644 index 00000000..165aabd9 --- /dev/null +++ b/src/Handler/Asset/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php @@ -0,0 +1,27 @@ +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..37563cac --- /dev/null +++ b/src/Handler/Asset/Helper/MarkGridConfigFavourite/MarkGridConfigFavouriteResult.php @@ -0,0 +1,27 @@ +columns as $item) { + if (!empty($item->isOperator)) { + $itemKey = '#' . uniqid('', false); + + $item->key = $itemKey; + $newData[] = $item; + $helperColumns[$itemKey] = $item; + } else { + $newData[] = $item; + } + } + + $this->gridColumnConfigSession->prependHelperColumns($helperColumns); + + return new PrepareHelperColumnConfigsResult( + columns: $newData, + ); + } +} diff --git a/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php new file mode 100644 index 00000000..98657172 --- /dev/null +++ b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsPayload.php @@ -0,0 +1,36 @@ +request->getString('columns')); + + return new static( + columns: is_array($decoded) ? $decoded : [], + ); + } +} diff --git a/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php new file mode 100644 index 00000000..a155e8ef --- /dev/null +++ b/src/Handler/Asset/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php @@ -0,0 +1,26 @@ +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 AdminOperationFailedException("don't mess around with somebody else's configuration"); + } + + try { + $this->gridColumnConfigService->updateGridConfigShares($gridConfig, $metadata ?? [], $adminUser); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + 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()) { + try { + $this->gridColumnConfigService->updateGridConfigFavourites($gridConfig, $metadata, $adminUser); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } + + $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..3c2013a3 --- /dev/null +++ b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigPayload.php @@ -0,0 +1,53 @@ +request->getString('gridconfig'); + $settings = $request->request->getString('settings'); + + $decodedGridConfig = $gridconfig ? json_decode($gridconfig, true) : []; + $decodedMetadata = $settings ? json_decode($settings, true) : null; + + 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: is_array($decodedGridConfig) ? $decodedGridConfig : [], + metadata: is_array($decodedMetadata) ? $decodedMetadata : 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..7d21f1c3 --- /dev/null +++ b/src/Handler/Asset/Helper/SaveGridColumnConfig/SaveGridColumnConfigResult.php @@ -0,0 +1,29 @@ +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..62a27c31 --- /dev/null +++ b/src/Handler/Asset/Media/GetAssetText/GetAssetTextPayload.php @@ -0,0 +1,39 @@ +query->getString('page'); + + return new static( + id: (int) $request->query->getString('id'), + page: empty($page) ? null : (int) $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..e096ed4a --- /dev/null +++ b/src/Handler/Asset/Media/GetDocumentPreview/GetDocumentPreviewPayload.php @@ -0,0 +1,35 @@ +query->getString('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..af545d32 --- /dev/null +++ b/src/Handler/Asset/Media/GetDocumentPreview/PreviewDocumentResult.php @@ -0,0 +1,37 @@ +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..f1f57e40 --- /dev/null +++ b/src/Handler/Asset/Media/GetVideoPreview/GetVideoPreviewPayload.php @@ -0,0 +1,37 @@ +query->getString('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..0432f45a --- /dev/null +++ b/src/Handler/Asset/Media/GetVideoPreview/PreviewVideoResult.php @@ -0,0 +1,31 @@ +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..078775e7 --- /dev/null +++ b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewPayload.php @@ -0,0 +1,37 @@ +query->getString('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..1d4592be --- /dev/null +++ b/src/Handler/Asset/Media/ServeVideoPreview/ServeVideoPreviewResult.php @@ -0,0 +1,31 @@ +id) ?? throw new NotFoundHttpException('Asset not found'); + + if (!$asset->isAllowed('publish')) { + throw new AccessDeniedHttpException(); + } + + $this->payloadMapper->applyPayload($payload, $asset); + + $persistenceData = $this->coordinator->save($asset, $payload->task); + + return new SaveAssetResult( + data: $persistenceData->data, + treeData: $persistenceData->treeData + ); + } +} diff --git a/src/Handler/Asset/SaveAsset/SaveAssetPayload.php b/src/Handler/Asset/SaveAsset/SaveAssetPayload.php new file mode 100644 index 00000000..b5d49e1d --- /dev/null +++ b/src/Handler/Asset/SaveAsset/SaveAssetPayload.php @@ -0,0 +1,68 @@ +request->has('image'); + + $metadata = $request->request->has('metadata') + ? json_decode($request->request->getString('metadata'), true) + : null; + $propertiesData = $request->request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + $schedulerData = null; + if ($request->request->has('scheduler')) { + $decodedScheduler = json_decode($request->request->getString('scheduler'), true); + // a present-but-non-array value (e.g. JSON null) must still clear existing + // scheduled tasks, matching ApplySchedulerDataTrait's original behavior + $schedulerData = is_array($decodedScheduler) ? $decodedScheduler : []; + } + $imageData = $hasImage + ? json_decode($request->request->getString('image'), true) + : null; + $rawData = $request->request->get('data'); + + return new static( + id: (int) $request->request->getString('id'), + task: $request->request->getString('task'), + metadata: is_array($metadata) ? $metadata : null, + propertiesData: is_array($propertiesData) ? $propertiesData : null, + schedulerData: $schedulerData, + rawData: $rawData !== null ? (string) $rawData : null, + hasImage: $hasImage, + imageData: is_array($imageData) ? $imageData : null, + ); + } +} diff --git a/src/Handler/Asset/SaveAsset/SaveAssetResult.php b/src/Handler/Asset/SaveAsset/SaveAssetResult.php new file mode 100644 index 00000000..0ccea77d --- /dev/null +++ b/src/Handler/Asset/SaveAsset/SaveAssetResult.php @@ -0,0 +1,31 @@ +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..8da1a1cb --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetDocumentThumbnail/GetDocumentThumbnailResult.php @@ -0,0 +1,31 @@ +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); + $eventResult = $afterListLoadEvent->getArgument('list'); + $result = is_array($eventResult) ? $eventResult : $result; + + $resultData = is_array($result['data'] ?? null) ? $result['data'] : $assets; + $resultTotal = is_int($result['total'] ?? null) ? $result['total'] : $list->getTotalCount(); + + return new GetFolderContentPreviewResult($resultData, $resultTotal); + } +} 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..59710438 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetFolderContentPreview/GetFolderContentPreviewResult.php @@ -0,0 +1,28 @@ +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..b0fd932d --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailHandler.php @@ -0,0 +1,32 @@ +resolver->resolve($payload); + + return new GetImageThumbnailResult($resolution->image, $resolution->thumbnailResult); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php new file mode 100644 index 00000000..e7dd2313 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailPayload.php @@ -0,0 +1,59 @@ +query->getString('config') ?: null; + $cropPercent = $request->query->getString('cropPercent') ?: null; + $decodedConfig = $config !== null ? json_decode($config, true) : null; + + return new static( + id: (int) $request->query->getString('id'), + thumbnailParam: $request->query->all()['thumbnail'] ?? null, + configDecoded: is_array($decodedConfig) ? $decodedConfig : null, + queryAll: $request->query->all(), + hasThumbnailPreview: (bool) $request->query->get('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..ecb37969 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnail/GetImageThumbnailResult.php @@ -0,0 +1,30 @@ +resolver->resolve($payload); + + if ($resolution->thumbnailResult === null) { + throw new NotFoundHttpException(sprintf('Tree preview thumbnail not available for asset %d', $payload->id)); + } + + return new GetImageThumbnailFileinfoResult( + width: $resolution->thumbnailResult->getWidth(), + height: $resolution->thumbnailResult->getHeight(), + ); + } +} diff --git a/src/Handler/Asset/Thumbnail/GetImageThumbnailFileinfo/GetImageThumbnailFileinfoResult.php b/src/Handler/Asset/Thumbnail/GetImageThumbnailFileinfo/GetImageThumbnailFileinfoResult.php new file mode 100644 index 00000000..ee38f722 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetImageThumbnailFileinfo/GetImageThumbnailFileinfoResult.php @@ -0,0 +1,27 @@ +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..a22a9bf9 --- /dev/null +++ b/src/Handler/Asset/Thumbnail/GetVideoThumbnail/GetVideoThumbnailResult.php @@ -0,0 +1,31 @@ +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; + + $sqlFilter = $filter; + if ($sqlFilter !== null) { + if (!str_ends_with($sqlFilter, '*')) { + $sqlFilter .= '*'; + } + $sqlFilter = str_replace('*', '%', $sqlFilter); + $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 ($sqlFilter !== null) { + $childrenList->addConditionParam('CAST(assets.filename AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ?', [$sqlFilter]); + } + + $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' => $payload->queryAll, + ]); + $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); + $eventAssets = $event->getArgument('assets'); + $assets = is_array($eventAssets) ? $eventAssets : $assets; + + if (!$payload->hasPagination()) { + return new TreeGetAssetChildrenListResult($assets); + } + + return new TreeGetAssetChildrenPaginatedResult( + nodes: $assets, + offset: $offset, + limit: $limit, + total: $asset->getChildAmount($adminUser), + overflow: $filter !== null && ($filteredTotalCount > $limit), + filter: $filter ?? '', + inSearch: $payload->inSearch, + ); + } +} diff --git a/src/Handler/Asset/TreeGetAssetChildren/TreeGetAssetChildrenListResult.php b/src/Handler/Asset/TreeGetAssetChildren/TreeGetAssetChildrenListResult.php new file mode 100644 index 00000000..499165ed --- /dev/null +++ b/src/Handler/Asset/TreeGetAssetChildren/TreeGetAssetChildrenListResult.php @@ -0,0 +1,27 @@ +hasLimit; + } + + public static function fromRequest(Request $request): static + { + $filter = $request->query->has('filter') ? $request->query->getString('filter') : null; + $limit = (int) $request->query->getString('limit'); + if ($filter !== null) { + $limit = 100; + } elseif (!$limit) { + $limit = 100000000; + } + + return new static( + nodeId: (int) $request->query->getString('node'), + customViewId: ($request->query->getString('view') ?: null), + filter: $filter, + limit: $limit, + offset: (int) $request->query->getString('start'), + hasLimit: !empty($request->query->get('limit')), + inSearch: (int) $request->query->getString('inSearch'), + queryAll: $request->query->all(), + ); + } +} diff --git a/src/Handler/Asset/UpdateAsset/UpdateAssetHandler.php b/src/Handler/Asset/UpdateAsset/UpdateAssetHandler.php new file mode 100644 index 00000000..75ab879f --- /dev/null +++ b/src/Handler/Asset/UpdateAsset/UpdateAssetHandler.php @@ -0,0 +1,105 @@ +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); + + try { + $asset->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + 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 AdminOperationFailedException($msg); + } + + if ($asset->isAllowed('rename') && isset($updateData['filename'])) { + $asset->setFilename($updateData['filename']); + + try { + $asset->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new UpdateAssetResult($this->elementService->getElementTreeNodeConfig($asset)); + } + + Logger::debug('prevented update asset because of missing permissions'); + throw new AdminOperationFailedException('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..73b0b737 --- /dev/null +++ b/src/Handler/Asset/UpdateAsset/UpdateAssetPayload.php @@ -0,0 +1,37 @@ +request->getString('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..5091b587 --- /dev/null +++ b/src/Handler/Asset/UpdateAsset/UpdateAssetResult.php @@ -0,0 +1,27 @@ +assetUploadService->addAsset($payload); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new AddAssetResult([ + 'id' => $asset->getId(), + 'path' => $asset->getFullPath(), + 'type' => $asset->getType(), + ]); + } +} diff --git a/src/Handler/Asset/Upload/AddAsset/AddAssetPayload.php b/src/Handler/Asset/Upload/AddAsset/AddAssetPayload.php new file mode 100644 index 00000000..da3bba6d --- /dev/null +++ b/src/Handler/Asset/Upload/AddAsset/AddAssetPayload.php @@ -0,0 +1,68 @@ +files->has('Filedata')) { + /** @var UploadedFile $file */ + $file = $request->files->get('Filedata'); + $filedataPath = $file->getPathname(); + $filedataOriginalName = $file->getClientOriginalName(); + } + + return new static( + filedataPath: $filedataPath, + filedataOriginalName: $filedataOriginalName, + type: $request->request->getString('type') ?: null, + filename: $request->request->getString('filename') ?: null, + data: $request->request->getString('data') ?: null, + parentId: $request->query->getInt('parentId'), + hasParentId: $request->query->has('parentId'), + parentPath: $request->query->get('parentPath'), + hasDir: $request->query->has('dir'), + dir: $request->query->get('dir'), + context: $request->query->get('context'), + allowOverwrite: (bool) $request->query->get('allowOverwrite'), + uploadAssetType: $request->query->get('uploadAssetType'), + ); + } +} diff --git a/src/Handler/Asset/Upload/AddAsset/AddAssetResult.php b/src/Handler/Asset/Upload/AddAsset/AddAssetResult.php new file mode 100644 index 00000000..50a7c162 --- /dev/null +++ b/src/Handler/Asset/Upload/AddAsset/AddAssetResult.php @@ -0,0 +1,29 @@ +assetUploadService->addAsset($payload); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new AddAssetCompatibilityResult( + msg: 'Success', + id: $asset->getId(), + fullpath: $asset->getRealFullPath(), + type: $asset->getType(), + ); + } +} diff --git a/src/Handler/Asset/Upload/AddAssetCompatibility/AddAssetCompatibilityResult.php b/src/Handler/Asset/Upload/AddAssetCompatibility/AddAssetCompatibilityResult.php new file mode 100644 index 00000000..d46c850a --- /dev/null +++ b/src/Handler/Asset/Upload/AddAssetCompatibility/AddAssetCompatibilityResult.php @@ -0,0 +1,29 @@ +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 new CheckAssetExistsResult(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/CheckAssetExists/CheckAssetExistsResult.php b/src/Handler/Asset/Upload/CheckAssetExists/CheckAssetExistsResult.php new file mode 100644 index 00000000..5fb4f37d --- /dev/null +++ b/src/Handler/Asset/Upload/CheckAssetExists/CheckAssetExistsResult.php @@ -0,0 +1,26 @@ +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 AdminOperationFailedException($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..f8725670 --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZip/ImportZipPayload.php @@ -0,0 +1,47 @@ +files->get('Filedata'); + if (!$file instanceof UploadedFile || !is_file($file->getPathname())) { + throw new AdminOperationFailedException('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'); + } + + 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..3c64936f --- /dev/null +++ b/src/Handler/Asset/Upload/ImportZip/ImportZipResult.php @@ -0,0 +1,28 @@ +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 ($path === false) { + continue; + } + if (str_starts_with($path, '__MACOSX/')) { + continue; + } + if (str_ends_with($path, '/Thumbs.db')) { + continue; + } + if (str_ends_with($path, '/.DS_Store')) { + continue; + } + + if ($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..4c85381e --- /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 AdminOperationFailedException(sprintf( + $this->translator->trans('asset_type_change_not_allowed', [], 'admin'), + $newType, + $asset->getType(), + )); + } + + $stream = fopen($filePath, 'rb+') ?: null; + $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 new ReplaceAssetResult(id: $asset->getId(), path: $asset->getRealFullPath()); + } +} 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/Upload/ReplaceAsset/ReplaceAssetResult.php b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetResult.php new file mode 100644 index 00000000..72035ce4 --- /dev/null +++ b/src/Handler/Asset/Upload/ReplaceAsset/ReplaceAssetResult.php @@ -0,0 +1,27 @@ +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); + + try { + $asset->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new PublishVersionResult($this->elementService->getElementTreeNodeConfig($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..77f61ded --- /dev/null +++ b/src/Handler/Asset/Version/PublishVersion/PublishVersionPayload.php @@ -0,0 +1,35 @@ +request->getString('id'), + ); + } +} diff --git a/src/Handler/Asset/Version/PublishVersion/PublishVersionResult.php b/src/Handler/Asset/Version/PublishVersion/PublishVersionResult.php new file mode 100644 index 00000000..2c39e6e7 --- /dev/null +++ b/src/Handler/Asset/Version/PublishVersion/PublishVersionResult.php @@ -0,0 +1,27 @@ +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(sprintf('Permission denied, version id [%d]', $versionId)); + } + + 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..8da4a62b --- /dev/null +++ b/src/Handler/Asset/Version/ShowVersion/ShowVersionPayload.php @@ -0,0 +1,37 @@ +query->getString('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..fa537272 --- /dev/null +++ b/src/Handler/Asset/Version/ShowVersion/ShowVersionResult.php @@ -0,0 +1,32 @@ +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 AdminOperationFailedException('prevented adding object because of missing permissions'); + } + + if (DataObject\Service::pathExists($parent->getRealFullPath() . '/' . $key)) { + throw new AdminOperationFailedException('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() ?? 0, 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..2564ae6d --- /dev/null +++ b/src/Handler/DataObject/AddObject/AddObjectResult.php @@ -0,0 +1,28 @@ +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 AdminOperationFailedException('prevented creating folder because of missing permissions'); + } + + if (DataObject\Service::pathExists($parent->getRealFullPath() . '/' . $key)) { + throw new AdminOperationFailedException('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..600b8bd5 --- /dev/null +++ b/src/Handler/DataObject/ChangeChildrenSortBy/ChangeChildrenSortByHandler.php @@ -0,0 +1,154 @@ +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 AdminOperationFailedException(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 AdminOperationFailedException('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((int) $child['id'], (int) $child['modificationDate']); + + DataObject::clearDependentCacheByObjectId((int) $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..b1bfefe0 --- /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..0b5b630a --- /dev/null +++ b/src/Handler/DataObject/ClassDef/AddClass/AddClassResult.php @@ -0,0 +1,27 @@ +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); + } + } + + $filename = $this->bulkOperationSession->getImportFile(); + + $json = @file_get_contents($filename); + $json = json_decode($json !== false ? $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 AdminOperationFailedException('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 AdminOperationFailedException('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 AdminOperationFailedException('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); + } + + 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()); + + throw new AdminOperationFailedException($e->getMessage()); + } + } + } + } + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php b/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php new file mode 100644 index 00000000..bbcd97e5 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkCommit/BulkCommitPayload.php @@ -0,0 +1,24 @@ +request->getString('data'), true); + + return new static( + data: is_array($data) ? $data : [], + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPrepareHandler.php b/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPrepareHandler.php new file mode 100644 index 00000000..a7d671bc --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkExportPrepare/BulkExportPrepareHandler.php @@ -0,0 +1,29 @@ +bulkOperationSession->storeExportSettings($payload->data); + } +} 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..e8057372 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportHandler.php @@ -0,0 +1,75 @@ +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, + ]; + } + } + + $this->bulkOperationSession->storeImportFile($tmpName); + + return new BulkImportResult(data: $result); + } +} 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..38eebfc2 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/BulkImport/BulkImportResult.php @@ -0,0 +1,30 @@ + $data + */ + public function __construct( + public array $data, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassHandler.php b/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassHandler.php new file mode 100644 index 00000000..3e5dcb78 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DeleteClass/DeleteClassHandler.php @@ -0,0 +1,31 @@ +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..3dd3a6a0 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DeleteSelectOptions/DeleteSelectOptionsHandler.php @@ -0,0 +1,38 @@ +id); + if (!$selectOptions instanceof DataObject\SelectOptions\Config) { + throw new AdminOperationFailedException('Not Found'); + } + + try { + $selectOptions->delete(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } +} 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..27f26a58 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportHandler.php @@ -0,0 +1,71 @@ +bulkOperationSession->getExportSettings(), 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..6690fde7 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/DoBulkExport/DoBulkExportResult.php @@ -0,0 +1,27 @@ +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..66a75fff --- /dev/null +++ b/src/Handler/DataObject/ClassDef/ExportClass/ExportClassResult.php @@ -0,0 +1,28 @@ + $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..f60b2324 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetAssetTypes/GetAssetTypesResult.php @@ -0,0 +1,30 @@ + $types + */ + public function __construct( + public array $types, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/GetClass/GetClassHandler.php b/src/Handler/DataObject/ClassDef/GetClass/GetClassHandler.php new file mode 100644 index 00000000..c4060e01 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClass/GetClassHandler.php @@ -0,0 +1,39 @@ +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..57c676a8 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClass/GetClassResult.php @@ -0,0 +1,27 @@ +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..e44ea532 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassBulkExportList/GetClassBulkExportListResult.php @@ -0,0 +1,27 @@ +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..9727268f --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassDefinitionForColumnConfig/GetClassDefinitionForColumnConfigResult.php @@ -0,0 +1,27 @@ +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); + if ($content === false) { + continue; + } + $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..decb897f --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsPayload.php @@ -0,0 +1,24 @@ +query->has('type') ? $request->query->getString('type') : null, + classId: $request->query->get('classId'), + ); + } +} diff --git a/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php new file mode 100644 index 00000000..d69b27a4 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassIcons/GetClassIconsResult.php @@ -0,0 +1,30 @@ + $icons + */ + public function __construct( + public array $icons, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeHandler.php b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeHandler.php new file mode 100644 index 00000000..179e52b5 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeHandler.php @@ -0,0 +1,156 @@ +userContext->getAdminUser(); + + if ($adminUser === null || !$adminUser->isAllowed('objects')) { + Logger::log('[Startup] Object types are not loaded as "objects" permission is missing'); + + return new GetClassTreeResult(nodes: []); + } + + $createAllowed = $payload->createAllowed; + $withId = $payload->withId; + $useTitle = $payload->useTitle; + $grouped = $payload->grouped; + $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..5fefbc06 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetClassTree/GetClassTreeResult.php @@ -0,0 +1,27 @@ + $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..2cf85767 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetDocumentTypes/GetDocumentTypesResult.php @@ -0,0 +1,30 @@ + $types + */ + public function __construct( + public array $types, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsHandler.php b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsHandler.php new file mode 100644 index 00000000..62c8f725 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsHandler.php @@ -0,0 +1,38 @@ +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..19fc55a4 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptions/GetSelectOptionsResult.php @@ -0,0 +1,27 @@ +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..4ee3b80b --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsTree/GetSelectOptionsTreeResult.php @@ -0,0 +1,27 @@ +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..e87c8116 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetSelectOptionsUsages/GetSelectOptionsUsagesResult.php @@ -0,0 +1,30 @@ + $usages + */ + public function __construct( + public array $usages, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewHandler.php b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewHandler.php new file mode 100644 index 00000000..cd910215 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewHandler.php @@ -0,0 +1,61 @@ +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..dfbf6117 --- /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'), + 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..f1149a97 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetTextLayoutPreview/GetTextLayoutPreviewResult.php @@ -0,0 +1,27 @@ +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..66c611dc --- /dev/null +++ b/src/Handler/DataObject/ClassDef/GetVideoAllowedTypes/GetVideoAllowedTypesResult.php @@ -0,0 +1,30 @@ + $types + */ + public function __construct( + public array $types, + ) {} +} diff --git a/src/Handler/DataObject/ClassDef/ImportClass/ImportClassHandler.php b/src/Handler/DataObject/ClassDef/ImportClass/ImportClassHandler.php new file mode 100644 index 00000000..cf2d50ef --- /dev/null +++ b/src/Handler/DataObject/ClassDef/ImportClass/ImportClassHandler.php @@ -0,0 +1,37 @@ +id); + if (!$class) { + throw new NotFoundHttpException('Class not found'); + } + + if (!DataObject\ClassDefinition\Service::importClassDefinitionFromJson($class, $payload->json, false, true)) { + throw new AdminOperationFailedException('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..5be2bdd8 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionHandler.php @@ -0,0 +1,109 @@ +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); + + try { + $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(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + $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..6c67084b --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveClassDefinition/SaveClassDefinitionResult.php @@ -0,0 +1,28 @@ +task === 'add' && (new DataObject\SelectOptions\Config\Listing())->hasConfig($payload->id)) { + throw new \Exception('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()); + } catch (\Exception $e) { + Logger::error($e->getMessage()); + + throw new AdminOperationFailedException($e->getMessage()); + } + } +} 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..2363bb1c --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SaveSelectOptions/SaveSelectOptionsResult.php @@ -0,0 +1,27 @@ +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..3842d414 --- /dev/null +++ b/src/Handler/DataObject/ClassDef/SuggestClassIdentifier/SuggestClassIdentifierResult.php @@ -0,0 +1,31 @@ + $existingIds + */ + public function __construct( + public int $suggestedIdentifier, + public array $existingIds, + ) {} +} diff --git a/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsHandler.php b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsHandler.php new file mode 100644 index 00000000..e463e5ef --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsHandler.php @@ -0,0 +1,131 @@ +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..9f897742 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddCollections/AddCollectionsResult.php @@ -0,0 +1,27 @@ +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..c63fd08b --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddGroups/AddGroupsResult.php @@ -0,0 +1,27 @@ + '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(id: $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..63ce877a --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/AddProperty/AddPropertyResult.php @@ -0,0 +1,27 @@ +name, $payload->storeId); + + if (!$config) { + $config = new Classificationstore\CollectionConfig(); + $config->setName($payload->name); + $config->setStoreId($payload->storeId); + $config->save(); + } + + return new CreateCollectionResult(id: $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..4fd6185f --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateCollection/CreateCollectionResult.php @@ -0,0 +1,27 @@ +name, $payload->storeId)) { + throw new AdminOperationFailedException('classificationstore_error_group_exists_msg'); + } + + $config = new Classificationstore\GroupConfig(); + $config->setStoreId($payload->storeId); + $config->setName($payload->name); + $config->save(); + + return new CreateGroupResult(id: $config->getName()); + } +} 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..c88706f6 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateGroup/CreateGroupResult.php @@ -0,0 +1,27 @@ +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() ?? 0); + } +} 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..e6921dd5 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/CreateStore/CreateStoreResult.php @@ -0,0 +1,27 @@ +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..ab3ea114 --- /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: $request->query->getBoolean('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..5bbfafe7 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollectionRelations/GetCollectionRelationsResult.php @@ -0,0 +1,28 @@ +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..51087215 --- /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: $request->query->getBoolean('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..5ef47e8c --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetCollections/GetCollectionsResult.php @@ -0,0 +1,28 @@ +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..0e9c0842 --- /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: $request->query->getBoolean('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..dbc6ede8 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetGroups/GetGroupsResult.php @@ -0,0 +1,28 @@ +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..c22a1c89 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetPage/GetPageResult.php @@ -0,0 +1,27 @@ +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[] = $this->keyConfigService->buildKeyConfigItem($config); + } + + return new GetPropertiesResult(data: $data, total: $list->getTotalCount()); + } +} diff --git a/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesPayload.php b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesPayload.php new file mode 100644 index 00000000..0e080afd --- /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: $request->query->getBoolean('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..651cdf8c --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetProperties/GetPropertiesResult.php @@ -0,0 +1,28 @@ + '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..03cdc017 --- /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: $request->query->getBoolean('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..8d7adecf --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetRelations/GetRelationsResult.php @@ -0,0 +1,28 @@ +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..aac369c8 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/GetStoreTree/GetStoreTreeResult.php @@ -0,0 +1,27 @@ +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..2b23744b --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/ListStores/ListStoresResult.php @@ -0,0 +1,27 @@ +hasData) { + throw new AdminOperationFailedException(); + } + + $data = $payload->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..9c481ed9 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveCollectionRelations/SaveCollectionRelationsResult.php @@ -0,0 +1,27 @@ +hasData) { + throw new AdminOperationFailedException(); + } + + $data = $payload->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..61b978df --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SaveRelation/SaveRelationResult.php @@ -0,0 +1,27 @@ + 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..be0ab16e --- /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: $request->query->getBoolean('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..19295555 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/SearchRelations/SearchRelationsResult.php @@ -0,0 +1,28 @@ +hasData) { + throw new AdminOperationFailedException(); + } + + $data = $payload->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(data: $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..9a9b0551 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateCollection/UpdateCollectionResult.php @@ -0,0 +1,27 @@ +hasData) { + throw new AdminOperationFailedException(); + } + + $data = $payload->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(data: $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..bfe63eb1 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateGroup/UpdateGroupResult.php @@ -0,0 +1,27 @@ +hasData) { + throw new AdminOperationFailedException(); + } + + $data = $payload->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 = $this->keyConfigService->buildKeyConfigItem($config); + + return new UpdatePropertyResult(data: $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..b2ad60a9 --- /dev/null +++ b/src/Handler/DataObject/Classificationstore/UpdateProperty/UpdatePropertyResult.php @@ -0,0 +1,27 @@ +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 = $this->copySession->getParentId($payload->transactionId) ?? $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 AdminOperationFailedException("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); + + $this->copySession->rememberCopiedId($payload->transactionId, $payload->sourceId, $newObject->getId(), $payload->saveParentId); + + 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..4d2d615f --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectPayload.php @@ -0,0 +1,48 @@ +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, + transactionId: $request->request->getString('transactionId'), + saveParentId: (bool) $request->request->getString('saveParentId'), + ); + } +} diff --git a/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php new file mode 100644 index 00000000..9413ba4c --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyDataObject/CopyDataObjectResult.php @@ -0,0 +1,29 @@ +copySession->startTransaction((string) $transactionId); + $pasteJobs = []; + + if ($payload->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($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..58359e06 --- /dev/null +++ b/src/Handler/DataObject/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,27 @@ +userContext->getAdminUser()?->getId() ?? 0; + $objectId = $this->copySession->popRewriteStackId($payload->transactionId); + $idMapping = $this->copySession->getIdMapping($payload->transactionId); + $object = DataObject::getById($objectId) ?? throw new DataObjectNotFoundException($objectId); + + $object = DataObject\Service::rewriteIds($object, ['object' => $idMapping]); + $object->setUserModification($userId); + $object->save(); + + return new RewriteDataObjectIdsResult($objectId); + } +} diff --git a/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php new file mode 100644 index 00000000..627ae01a --- /dev/null +++ b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsPayload.php @@ -0,0 +1,34 @@ +request->getString('transactionId'), + ); + } +} diff --git a/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsResult.php b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsResult.php new file mode 100644 index 00000000..7d9450b4 --- /dev/null +++ b/src/Handler/DataObject/Copy/RewriteDataObjectIds/RewriteDataObjectIdsResult.php @@ -0,0 +1,26 @@ +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(); + + $data = $customLayout->getObjectVars(); + $data['isWriteable'] = $customLayout->isWriteable(); + + return new AddCustomLayoutResult(id: $customLayout->getId(), name: $customLayout->getName(), data: $data); + } +} 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/AddCustomLayout/AddCustomLayoutResult.php b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutResult.php new file mode 100644 index 00000000..cc525f42 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/AddCustomLayout/AddCustomLayoutResult.php @@ -0,0 +1,28 @@ +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..000fd6ba --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutHandler.php @@ -0,0 +1,43 @@ +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..b1a44e51 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ExportCustomLayout/ExportCustomLayoutResult.php @@ -0,0 +1,28 @@ +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(data: $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..37b20e27 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutHandler.php @@ -0,0 +1,65 @@ +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(); + } + } + + $data = $customLayout->getObjectVars(); + $data['isWriteable'] = $customLayout->isWriteable(); + + return new GetCustomLayoutResult($data); + } +} 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..51077c74 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/GetCustomLayout/GetCustomLayoutResult.php @@ -0,0 +1,27 @@ +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..c6214d48 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutHandler.php @@ -0,0 +1,62 @@ +importData; + + if (isset($importData['name']) + && DataObject\ClassDefinition\CustomLayout::getByName($importData['name']) instanceof DataObject\ClassDefinition\CustomLayout + ) { + throw new AdminOperationFailedException('', ['nameAlreadyInUse' => true]); + } + + $customLayout = DataObject\ClassDefinition\CustomLayout::getById($payload->id); + if (!$customLayout) { + throw new AdminOperationFailedException('Custom layout not found'); + } + + 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(); + } catch (\Exception $e) { + Logger::error($e->getMessage()); + + throw new AdminOperationFailedException($e->getMessage()); + } + } +} diff --git a/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php new file mode 100644 index 00000000..19474b99 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/ImportCustomLayout/ImportCustomLayoutPayload.php @@ -0,0 +1,42 @@ +files->get('Filedata'); + $fileContents = $file !== null ? (file_get_contents($file->getPathname()) ?: '') : ''; + $importData = json_decode($fileContents, true) ?? []; + + return new static( + id: $request->query->getString('id') ?: null, + importData: $importData, + ); + } +} diff --git a/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutHandler.php b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutHandler.php new file mode 100644 index 00000000..55de3913 --- /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 AdminOperationFailedException('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 new SaveCustomLayoutResult(id: $customLayout->getId(), data: $customLayout->getObjectVars()); + } +} 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/SaveCustomLayout/SaveCustomLayoutResult.php b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutResult.php new file mode 100644 index 00000000..ddac2c49 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SaveCustomLayout/SaveCustomLayoutResult.php @@ -0,0 +1,27 @@ +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..da598c09 --- /dev/null +++ b/src/Handler/DataObject/CustomLayout/SuggestCustomLayoutIdentifier/SuggestCustomLayoutIdentifierResult.php @@ -0,0 +1,29 @@ +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..fb82ec33 --- /dev/null +++ b/src/Handler/DataObject/DataObjectGridProxy/DataObjectGridProxyResult.php @@ -0,0 +1,28 @@ +type; + $id = $payload->id; + $amount = $payload->amount; + + if ($type !== 'children' && !$id) { + throw new AdminOperationFailedException(); + } + + 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 AdminOperationFailedException('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..8444af0e --- /dev/null +++ b/src/Handler/DataObject/DeleteDataObject/DeleteDataObjectResult.php @@ -0,0 +1,27 @@ +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..5b713355 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/ExportFieldCollection/ExportFieldCollectionResult.php @@ -0,0 +1,28 @@ +id); + + $data = $fc->getObjectVars(); + $data['isWriteable'] = $fc->isWritable(); + + return new GetFieldCollectionResult($data); + } +} 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..2d120c99 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollection/GetFieldCollectionResult.php @@ -0,0 +1,27 @@ +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/FieldCollectionTreeEditorResult.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/FieldCollectionTreeEditorResult.php new file mode 100644 index 00000000..923da7d3 --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionTree/FieldCollectionTreeEditorResult.php @@ -0,0 +1,28 @@ +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); + + $definitions = $event->getArgument('list'); + $layoutDefinitions = $event->getArgument('layoutDefinitions'); + + if ($forObjectEditor) { + return new FieldCollectionTreeEditorResult( + fieldcollections: $definitions, + layoutDefinitions: $layoutDefinitions, + ); + } + + return new FieldCollectionTreeResult(definitions: $definitions); + } +} 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..4fadfb94 --- /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 new GetFieldCollectionUsagesResult(usages: $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/GetFieldCollectionUsages/GetFieldCollectionUsagesResult.php b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesResult.php new file mode 100644 index 00000000..9ef5775b --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/GetFieldCollectionUsages/GetFieldCollectionUsagesResult.php @@ -0,0 +1,27 @@ +id); + + if (!DataObject\ClassDefinition\Service::importFieldCollectionFromJson($fieldCollection, $payload->json)) { + throw new AdminOperationFailedException('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..6d693fdf --- /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 !== null ? (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..cdc1b03f --- /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 AdminOperationFailedException('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 new UpdateFieldCollectionResult(id: $fcDef->getKey()); + } +} 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/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionResult.php b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionResult.php new file mode 100644 index 00000000..5e71e95a --- /dev/null +++ b/src/Handler/DataObject/FieldCollection/UpdateFieldCollection/UpdateFieldCollectionResult.php @@ -0,0 +1,26 @@ +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']['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']); + }); + + if ($currentLayoutId == -1 && $adminUser->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); + + $this->phpMetaEnricher->enrich($objectFromDatabase, $objectData['general']); + $this->preSendDataEventEnricher->enrich($object, $objectData); + + $this->adminStyleEnricher->forEditor($object, $objectData['general']); + $this->userNamesEnricher->enrich($object, $objectData['general']); + $this->customLayoutEnricher->enrich($object, $objectData); + $this->draftEnricher->enrich($object, $objectData, $draftVersion); + + $this->elementDraftService->removeObject('object', $payload->id); + + 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..a7e9db2d --- /dev/null +++ b/src/Handler/DataObject/GetDataObject/GetDataObjectResult.php @@ -0,0 +1,27 @@ +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']); + + $this->preSendDataEventEnricher->enrich($object, $objectData); + + 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..6fba50bb --- /dev/null +++ b/src/Handler/DataObject/GetDataObjectFolder/GetDataObjectFolderResult.php @@ -0,0 +1,27 @@ +elementDraftService->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..73e9ff53 --- /dev/null +++ b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoHandler.php @@ -0,0 +1,64 @@ +path === null) { + throw new AdminOperationFailedException(); + } + + $path = $payload->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..8cfd2177 --- /dev/null +++ b/src/Handler/DataObject/GetIdPathPagingInfo/GetIdPathPagingInfoResult.php @@ -0,0 +1,27 @@ +objectId); + if (!$object instanceof DataObject\Concrete) { + throw new AdminOperationFailedException('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 + ); + + $options = $optionsProvider->getOptions( + [ + 'object' => $object, + 'fieldname' => $fieldDefinition->getName(), + 'class' => $object->getClass(), + 'context' => $payload->context, + ], + $fieldDefinition + ); + + return new GetSelectOptionsResult(options: $options); + } +} 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/GetSelectOptions/GetSelectOptionsResult.php b/src/Handler/DataObject/GetSelectOptions/GetSelectOptionsResult.php new file mode 100644 index 00000000..d4dedb30 --- /dev/null +++ b/src/Handler/DataObject/GetSelectOptions/GetSelectOptionsResult.php @@ -0,0 +1,27 @@ +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..fb058096 --- /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 AdminOperationFailedException('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, + ]; + + $config = $this->gridConfigResolver->resolve($payload->locale, $params, true); + $data = [...$config->toArray(), '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 new DeleteGridColumnConfigResult($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/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php new file mode 100644 index 00000000..9bfc30cc --- /dev/null +++ b/src/Handler/DataObject/Helper/DeleteGridColumnConfig/DeleteGridColumnConfigResult.php @@ -0,0 +1,27 @@ +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 AdminOperationFailedException(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..64b34715 --- /dev/null +++ b/src/Handler/DataObject/Helper/ExecuteBatch/ExecuteBatchHandler.php @@ -0,0 +1,41 @@ +userContext->getAdminUser(); + + try { + $this->gridBatchService->executeObjectBatch($payload->params, $payload->locale, $adminUser); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } +} 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..d70e66f8 --- /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..fe1f53ee --- /dev/null +++ b/src/Handler/DataObject/Helper/GetAvailableVisibleFields/GetAvailableVisibleFieldsResult.php @@ -0,0 +1,27 @@ +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..7216bf33 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetBatchJobs/GetBatchJobsResult.php @@ -0,0 +1,27 @@ +userContext->getAdminUser(); + $userId = $adminUser?->getId() ?? 0; + + $list = $this->gridColumnConfigService->getMyOwnColumnConfigs($userId, $payload->classId); + $list = [...$list, ...$this->gridColumnConfigService->getSharedColumnConfigs($adminUser, $payload->classId)]; + + $data = [['id' => -1, 'name' => '--default--']]; + + foreach ($list as $config) { + $data[] = [ + 'id' => $config['id'], + 'name' => $config['name'], + ]; + } + + return new GetExportConfigsResult(data: $data); + } +} 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/GetExportConfigs/GetExportConfigsResult.php b/src/Handler/DataObject/Helper/GetExportConfigs/GetExportConfigsResult.php new file mode 100644 index 00000000..743e8dc1 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportConfigs/GetExportConfigsResult.php @@ -0,0 +1,27 @@ +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..c75e2f3a --- /dev/null +++ b/src/Handler/DataObject/Helper/GetExportJobs/GetExportJobsResult.php @@ -0,0 +1,28 @@ + $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, + ]; + + $config = $this->gridConfigResolver->resolve($payload->locale, $params); + + $event = new GenericEvent($this, [ + 'data' => $config->toArray(), + 'request' => $this->requestStack->getCurrentRequest(), + 'config' => $this->config, + 'context' => 'get', + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_GRID_GET_COLUMN_CONFIG_PRE_SEND_DATA); + + return new GetGridColumnConfigResult($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..60d4b0da --- /dev/null +++ b/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigPayload.php @@ -0,0 +1,55 @@ +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(), + ); + } +} diff --git a/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php b/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php new file mode 100644 index 00000000..b4c43048 --- /dev/null +++ b/src/Handler/DataObject/Helper/GetGridColumnConfig/GetGridColumnConfigResult.php @@ -0,0 +1,27 @@ +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..9935930a --- /dev/null +++ b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataHandler.php @@ -0,0 +1,44 @@ +id); + if (!$object instanceof DataObject) { + throw new AdminOperationFailedException(sprintf('DataObject with id %d not found', $payload->id)); + } + + return new LoadObjectDataResult(fields: GridData\DataObject::getData( + $object, + $payload->fields, + params: [ + 'helperDefinitions' => $this->gridColumnConfigSession->getHelperColumns(), + ] + ) + ); + } +} 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/LoadObjectData/LoadObjectDataResult.php b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataResult.php new file mode 100644 index 00000000..25a2c15d --- /dev/null +++ b/src/Handler/DataObject/Helper/LoadObjectData/LoadObjectDataResult.php @@ -0,0 +1,27 @@ +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..bd4ec90a --- /dev/null +++ b/src/Handler/DataObject/Helper/MarkDataObjectGridConfigFavourite/MarkDataObjectGridConfigFavouriteResult.php @@ -0,0 +1,27 @@ +columns as $item) { + if (!empty($item->isOperator)) { + $itemKey = '#' . uniqid('', false); + $item->key = $itemKey; + $helperColumns[$itemKey] = $item; + } + $newData[] = $item; + } + + $this->gridColumnConfigSession->prependHelperColumns($helperColumns); + + return new PrepareHelperColumnConfigsResult( + columns: $newData, + ); + } +} 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/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php b/src/Handler/DataObject/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php new file mode 100644 index 00000000..c7b52a65 --- /dev/null +++ b/src/Handler/DataObject/Helper/PrepareHelperColumnConfigs/PrepareHelperColumnConfigsResult.php @@ -0,0 +1,30 @@ +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 AdminOperationFailedException("don't mess around with somebody elses configuration"); + } + + try { + $this->gridColumnConfigService->updateGridConfigShares($gridConfig, $metadata ?? [], $adminUser, adminCanEditAll: true); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + if ($gridConfig && !empty($metadata['setAsFavourite']) && $adminUser->isAdmin()) { + try { + $this->gridColumnConfigService->updateGridConfigFavourites($gridConfig, $metadata, $adminUser, $payload->objectId); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } + + 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..304cd2bc --- /dev/null +++ b/src/Handler/DataObject/Helper/SaveDataObjectGridColumnConfig/SaveDataObjectGridColumnConfigResult.php @@ -0,0 +1,29 @@ +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..288903dd --- /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..502e209c --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/ExportObjectBrick/ExportObjectBrickResult.php @@ -0,0 +1,28 @@ +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..56189761 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickHandler.php @@ -0,0 +1,34 @@ +id); + + $data = $brick->getObjectVars(); + $data['isWriteable'] = $brick->isWritable(); + + return new GetObjectBrickResult($data); + } +} 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..64f1772e --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrick/GetObjectBrickResult.php @@ -0,0 +1,27 @@ +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..50c2e037 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickList/ObjectBrickListResult.php @@ -0,0 +1,27 @@ +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($this, [ + 'list' => $definitions, + 'objectId' => $objectId, + 'forObjectEditor' => $forObjectEditor, + 'layoutDefinitions' => $layoutDefinitions, + 'fieldName' => $fieldName, + 'object' => $object, + ]); + $this->eventDispatcher->dispatch($event, AdminEvents::CLASS_OBJECTBRICK_LIST_PRE_SEND_DATA); + + $definitions = $event->getArgument('list'); + $layoutDefinitions = $event->getArgument('layoutDefinitions'); + + if ($forObjectEditor) { + return new ObjectBrickTreeEditorResult( + objectbricks: $definitions, + layoutDefinitions: $layoutDefinitions, + ); + } + + return new ObjectBrickTreeResult(definitions: $definitions); + } +} 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/ObjectBrickTreeEditorResult.php b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/ObjectBrickTreeEditorResult.php new file mode 100644 index 00000000..f5f93438 --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/GetObjectBrickTree/ObjectBrickTreeEditorResult.php @@ -0,0 +1,28 @@ +id); + + if (!DataObject\ClassDefinition\Service::importObjectBrickFromJson($objectBrick, $payload->json)) { + throw new AdminOperationFailedException('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..1f7e2552 --- /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 !== null ? (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..6a91927b --- /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 AdminOperationFailedException('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 new UpdateObjectBrickResult(id: $brickDef->getKey()); + } +} 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/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickResult.php b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickResult.php new file mode 100644 index 00000000..ce66cdad --- /dev/null +++ b/src/Handler/DataObject/ObjectBrick/UpdateObjectBrick/UpdateObjectBrickResult.php @@ -0,0 +1,26 @@ +unitId); + if (!$fromUnit instanceof Unit) { + throw new AdminOperationFailedException('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..aaf21b1f --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertAllQuantityValues/ConvertAllQuantityValuesResult.php @@ -0,0 +1,29 @@ +fromUnitId); + $toUnit = Unit::getById($payload->toUnitId); + + if (!$fromUnit instanceof Unit || !$toUnit instanceof Unit) { + throw new AdminOperationFailedException('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..518d78c2 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ConvertQuantityValue/ConvertQuantityValueResult.php @@ -0,0 +1,27 @@ +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..f2839856 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnitList/GetQuantityValueUnitListResult.php @@ -0,0 +1,28 @@ +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..b30e214c --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/GetQuantityValueUnits/GetQuantityValueUnitsResult.php @@ -0,0 +1,28 @@ +service->importDefinitionFromJson($payload->json)) { + throw new AdminOperationFailedException('Failed to import quantity value units'); + } + } +} diff --git a/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php b/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php new file mode 100644 index 00000000..7960f63c --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ImportQuantityValueUnits/ImportQuantityValueUnitsPayload.php @@ -0,0 +1,34 @@ +files->get('Filedata'); + + return new static(json: $file !== null ? (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..aa766101 --- /dev/null +++ b/src/Handler/DataObject/QuantityValue/ManageQuantityValueUnitResult.php @@ -0,0 +1,27 @@ +request->getString('data'), true) ?? []; + if (!is_array($data)) { + throw new BadRequestHttpException('Invalid data format'); + } + + return new static(data: $data); + } +} 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..1edfaebe --- /dev/null +++ b/src/Handler/DataObject/SaveDataObject/SaveDataObjectHandler.php @@ -0,0 +1,90 @@ +id); + if (!$objectFromDatabase instanceof DataObject\Concrete) { + throw new AdminOperationFailedException('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); + + $persistenceData = $this->coordinator->save($object, $payload->task); + + return new SaveDataObjectResult( + general: $persistenceData->general, + treeData: $persistenceData->treeData, + draft: $persistenceData->draft + ); + } +} 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..7c7634d4 --- /dev/null +++ b/src/Handler/DataObject/SaveDataObject/SaveDataObjectResult.php @@ -0,0 +1,32 @@ +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'); + } + + try { + $object->setValues($general); + $object->setUserModification($adminUser->getId()); + + $this->applyProperties($object, $propertiesData); + + $object->save(); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } + + 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/TreeGetDataObjectChildren/TreeGetDataObjectChildrenHandler.php b/src/Handler/DataObject/TreeGetDataObjectChildren/TreeGetDataObjectChildrenHandler.php new file mode 100644 index 00000000..332d531e --- /dev/null +++ b/src/Handler/DataObject/TreeGetDataObjectChildren/TreeGetDataObjectChildrenHandler.php @@ -0,0 +1,197 @@ +node; + $filter = $payload->filter; + $start = $payload->start; + $limit = $payload->limit; + $view = $payload->view; + $fromPaging = $payload->fromPaging; + $requestQueryAll = $payload->allParams; + + $object = DataObject::getById($node) ?? throw new DataObjectNotFoundException($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'); + + [$objects, $total, $filteredTotalCount] = $this->loadChildren($object, $childrenList, $view); + $limit = $effectiveLimit; + } + + $event = new GenericEvent($this, ['objects' => $objects]); + $this->eventDispatcher->dispatch($event, AdminEvents::OBJECT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); + $objects = $event->getArgument('objects'); + + if (!$payload->hasPagination()) { + return new TreeGetDataObjectChildrenListResult($objects); + } + + return new TreeGetDataObjectChildrenPaginatedResult( + nodes: $objects, + offset: $offset, + limit: $limit, + total: $total, + overflow: !is_null($filter) && ($filteredTotalCount > $limit), + fromPaging: $fromPaging, + filter: $filter ?? '', + inSearch: $payload->inSearch, + ); + } + + private function loadChildren(DataObject\AbstractObject $object, DataObject\Listing $childrenList, string $view): array + { + $adminUser = $this->userContext->getAdminUser(); + $objects = []; + + $cv = $view ? ($this->elementService->getCustomViewById($view) ?? []) : []; + + $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 [$objects, $total, $filteredTotalCount]; + } + + 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/TreeGetDataObjectChildren/TreeGetDataObjectChildrenListResult.php b/src/Handler/DataObject/TreeGetDataObjectChildren/TreeGetDataObjectChildrenListResult.php new file mode 100644 index 00000000..da4f79a3 --- /dev/null +++ b/src/Handler/DataObject/TreeGetDataObjectChildren/TreeGetDataObjectChildrenListResult.php @@ -0,0 +1,27 @@ +allParams); + } + + public static function fromRequest(Request $request): static + { + return new static( + node: $request->query->getInt('node'), + filter: $request->query->has('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..0071e471 --- /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 AdminOperationFailedException("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 AdminOperationFailedException('prevented creating object because object with same path+key already exists'); + } + + if ($object->isLocked()) { + throw new AdminOperationFailedException('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 AdminOperationFailedException($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((int) $sibling['id'], $index); + $index++; + + DataObject::clearDependentCacheByObjectId((int) $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..aebd683c --- /dev/null +++ b/src/Handler/DataObject/UpdateDataObject/UpdateDataObjectResult.php @@ -0,0 +1,27 @@ +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..8f618791 --- /dev/null +++ b/src/Handler/DataObject/Variants/GetVariants/GetVariantsResult.php @@ -0,0 +1,27 @@ +id; + $key = $payload->key; + + $object = DataObject\Concrete::getById($id); + + if (!$object) { + throw new AdminOperationFailedException('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..8ff7e742 --- /dev/null +++ b/src/Handler/DataObject/Variants/UpdateObjectKey/UpdateObjectKeyResult.php @@ -0,0 +1,27 @@ +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..b2f83cda --- /dev/null +++ b/src/Handler/DataObject/Version/DiffVersions/DiffVersionsResult.php @@ -0,0 +1,32 @@ +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..3b34779b --- /dev/null +++ b/src/Handler/DataObject/Version/PreviewVersion/PreviewVersionResult.php @@ -0,0 +1,30 @@ +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); + + try { + $object->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + $treeData = []; + $this->adminStyleEnricher->forTree($object, $treeData); + + return new PublishVersionResult( + general: ['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..9d8d1026 --- /dev/null +++ b/src/Handler/DataObject/Version/PublishVersion/PublishVersionResult.php @@ -0,0 +1,30 @@ +userContext->getAdminUser(); + $parentDocument = Document::getById($payload->parentId); + + if (!$parentDocument || !$parentDocument->isAllowed('create')) { + throw new AdminOperationFailedException('Prevented adding a document because of missing permissions'); + } + + $intendedPath = $parentDocument->getRealFullPath() . '/' . $payload->key; + + if (Document\Service::pathExists($intendedPath)) { + throw new AdminOperationFailedException( + 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) { + 'folder' => $this->createFolder($parentDocument, $createValues), + '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), + 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->getId(), $document->getType()); + } + + 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); + + try { + $document->save(); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + 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); + + try { + $document->save(); + } catch (Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return $document; + } + + Logger::debug("Unknown document type, can't add [ $type ] "); + + throw new AdminOperationFailedException(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..6790c9b8 --- /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->getString('docTypeId') ?: null, + translationsBaseDocumentId: $request->request->getString('translationsBaseDocument') ?: null, + language: $request->request->getString('language') ?: null, + inheritanceSource: $request->request->has('inheritanceSource') ? $request->request->getString('inheritanceSource') : null, + title: $request->request->getString('title') ?: null, + name: $request->request->getString('name') ?: null, + ); + } +} diff --git a/src/Handler/Document/AddDocument/AddDocumentResult.php b/src/Handler/Document/AddDocument/AddDocumentResult.php new file mode 100644 index 00000000..06b2917a --- /dev/null +++ b/src/Handler/Document/AddDocument/AddDocumentResult.php @@ -0,0 +1,27 @@ +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..90874c32 --- /dev/null +++ b/src/Handler/Document/ChangeMainDocument/ChangeMainDocumentPayload.php @@ -0,0 +1,36 @@ +request->get('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..5191fc75 --- /dev/null +++ b/src/Handler/Document/ConvertDocument/ConvertDocumentPayload.php @@ -0,0 +1,36 @@ +request->getString('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..47391a68 --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentHandler.php @@ -0,0 +1,92 @@ +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 = $this->copySession->getParentId($payload->transactionId) ?? $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 AdminOperationFailedException('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 AdminOperationFailedException('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); + + $this->copySession->rememberCopiedId($payload->transactionId, $payload->sourceId, $newDocument->getId(), $payload->saveParentId); + + 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..26d36888 --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentPayload.php @@ -0,0 +1,54 @@ +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, + enableInheritance: $request->request->getString('enableInheritance') === 'true', + resetIndex: $request->request->getString('resetIndex') === 'true', + language: $request->request->getString('language') ?: null, + transactionId: $request->request->getString('transactionId'), + saveParentId: (bool) $request->request->getString('saveParentId'), + ); + } +} diff --git a/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php b/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php new file mode 100644 index 00000000..d78170fb --- /dev/null +++ b/src/Handler/Document/Copy/CopyDocument/CopyDocumentResult.php @@ -0,0 +1,29 @@ +copySession->startTransaction((string) $transactionId); + $pasteJobs = []; + + if ($payload->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..d58ff5b8 --- /dev/null +++ b/src/Handler/Document/Copy/CopyInfo/CopyInfoResult.php @@ -0,0 +1,28 @@ +userContext->getAdminUser()?->getId() ?? 0; + $documentId = $this->copySession->popRewriteStackId($payload->transactionId); + $idMapping = $this->copySession->getIdMapping($payload->transactionId); + $document = Document::getById($documentId); + + if ($document === null) { + return new RewriteDocumentIdsResult($documentId); + } + + $document = Document\Service::rewriteIds($document, ['document' => $idMapping], [ + 'enableInheritance' => $payload->enableInheritance, + ]); + $document->setUserModification($userId); + $document->save(); + + return new RewriteDocumentIdsResult($documentId); + } +} diff --git a/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php new file mode 100644 index 00000000..9ebf40ba --- /dev/null +++ b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsPayload.php @@ -0,0 +1,36 @@ +request->getString('enableInheritance') === 'true', + transactionId: $request->request->getString('transactionId'), + ); + } +} diff --git a/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsResult.php b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsResult.php new file mode 100644 index 00000000..faf6000b --- /dev/null +++ b/src/Handler/Document/Copy/RewriteDocumentIds/RewriteDocumentIdsResult.php @@ -0,0 +1,26 @@ +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 AdminOperationFailedException('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..9b95f63c --- /dev/null +++ b/src/Handler/Document/DeleteDocument/DeleteDocumentPayload.php @@ -0,0 +1,38 @@ +request->getString('type'), + id: (int) $request->request->getString('id'), + amount: (int) $request->request->getString('amount'), + ); + } +} diff --git a/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php b/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php new file mode 100644 index 00000000..9ee55c39 --- /dev/null +++ b/src/Handler/Document/DeleteDocument/DeleteDocumentResult.php @@ -0,0 +1,26 @@ +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..111ffd54 --- /dev/null +++ b/src/Handler/Document/DocTypes/CreateDocType/CreateDocTypeResult.php @@ -0,0 +1,14 @@ +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..34897643 --- /dev/null +++ b/src/Handler/Document/DocTypes/DeleteDocType/DeleteDocTypeResult.php @@ -0,0 +1,14 @@ +request->getString('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..7d602907 --- /dev/null +++ b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListHandler.php @@ -0,0 +1,45 @@ +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..6d5beba2 --- /dev/null +++ b/src/Handler/Document/DocTypes/GetDocTypesList/GetDocTypesListResult.php @@ -0,0 +1,28 @@ +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..8df9f842 --- /dev/null +++ b/src/Handler/Document/DocTypes/UpdateDocType/UpdateDocTypeResult.php @@ -0,0 +1,14 @@ +id); + if (!$email) { + throw new NotFoundHttpException('Email not found'); + } + + if (!$email->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + 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->phpMetaEnricher->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); + + $this->preSendDataEventEnricher->enrich($email, $data); + + return new GetEmailDataResult(data: $data); + } +} diff --git a/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php b/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php new file mode 100644 index 00000000..246ded44 --- /dev/null +++ b/src/Handler/Document/Email/GetEmailData/GetEmailDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$loadedEmail) { + throw new NotFoundHttpException('Email not found'); + } + + $email = $this->elementDraftService->resolveDraft($loadedEmail); + + $this->mapper->applyPagePayload($payload, $email, $payload->task); + + $persistenceData = $this->coordinator->save($email, $payload->task); + + $this->elementDraftService->saveDocument($email); + + if ($payload->task === 'publish' || $payload->task === 'unpublish') { + return new SaveEmailPublishedResult( + data: $persistenceData->data, + treeData: $persistenceData->treeData, + ); + } + + return new SaveEmailDraftResult(draft: $persistenceData->draft ?? []); + } +} 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'); + } + + if (!$folder->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + $folder = clone $folder; + $folder->setParent(null); + + $data = $folder->getObjectVars(); + $data['locked'] = $folder->isLocked(); + + $this->documentMetaEnricher->enrich($folder, $data); + $this->phpMetaEnricher->enrich($folder, $data); + $this->adminStyleEnricher->forEditor($folder, $data); + $this->userNamesEnricher->enrich($folder, $data); + $this->propertiesEnricher->enrich($folder, $data); + $this->translationEnricher->enrich($folder, $data); + + $this->preSendDataEventEnricher->enrich($folder, $data); + + return new GetFolderDataResult(data: $data); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php b/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php new file mode 100644 index 00000000..0c4aa8b7 --- /dev/null +++ b/src/Handler/Document/Folder/GetFolderData/GetFolderDataPayload.php @@ -0,0 +1,35 @@ +query->getString('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..ed75d111 --- /dev/null +++ b/src/Handler/Document/Folder/GetFolderData/GetFolderDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$folder) { + throw new NotFoundHttpException('Folder not found'); + } + + $this->mapper->applyFolderPayload($payload, $folder); + $persistenceData = $this->coordinator->save($folder, 'publish'); + + return new SaveFolderResult(treeData: $persistenceData->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..a2f37230 --- /dev/null +++ b/src/Handler/Document/Folder/SaveFolder/SaveFolderPayload.php @@ -0,0 +1,41 @@ +request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + + return new static( + id: (int) $request->request->getString('id'), + properties: is_array($properties) ? $properties : 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..230e1a26 --- /dev/null +++ b/src/Handler/Document/Folder/SaveFolder/SaveFolderResult.php @@ -0,0 +1,27 @@ +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..40de338e --- /dev/null +++ b/src/Handler/Document/GetDocTypesByType/GetDocTypesByTypeResult.php @@ -0,0 +1,27 @@ +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->phpMetaEnricher->enrich($document, $data); + $this->adminStyleEnricher->forEditor($document, $data); + $this->userNamesEnricher->enrich($document, $data); + $this->propertiesEnricher->enrich($document, $data); + $this->translationEnricher->enrich($document, $data); + + $this->preSendDataEventEnricher->enrich($document, $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..c3f448ee --- /dev/null +++ b/src/Handler/Document/GetDocumentData/GetDocumentDataPayload.php @@ -0,0 +1,34 @@ +query->getString('id'), + ); + } +} diff --git a/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php b/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php new file mode 100644 index 00000000..2a39df8f --- /dev/null +++ b/src/Handler/Document/GetDocumentData/GetDocumentDataResult.php @@ -0,0 +1,26 @@ +path); + + if (!$document) { + return new GetDocumentIdForPathResult(found: false); + } + + return new GetDocumentIdForPathResult(found: true, id: $document->getId(), type: $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..3fda8551 --- /dev/null +++ b/src/Handler/Document/GetDocumentIdForPath/GetDocumentIdForPathResult.php @@ -0,0 +1,34 @@ +found; + } +} diff --git a/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataHandler.php b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataHandler.php new file mode 100644 index 00000000..0b2f8da3 --- /dev/null +++ b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataHandler.php @@ -0,0 +1,88 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Hardlink not found'); + } + + if (!$link->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + 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->phpMetaEnricher->enrich($cloned, $data); + $this->adminStyleEnricher->forEditor($cloned, $data); + $this->userNamesEnricher->enrich($cloned, $data); + $this->propertiesEnricher->enrich($cloned, $data); + $this->translationEnricher->enrich($cloned, $data); + + $this->preSendDataEventEnricher->enrich($cloned, $data); + + return new GetHardlinkDataResult(data: $data); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php new file mode 100644 index 00000000..ac2f7b4f --- /dev/null +++ b/src/Handler/Document/Hardlink/GetHardlinkData/GetHardlinkDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Hardlink not found'); + } + + $this->mapper->applyHardlinkPayload($payload, $link); + $persistenceData = $this->coordinator->save($link, $payload->task); + + return new SaveHardlinkResult( + data: $persistenceData->data, + treeData: $persistenceData->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..934054c6 --- /dev/null +++ b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkPayload.php @@ -0,0 +1,57 @@ +request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + + $scheduler = null; + if ($request->request->has('scheduler')) { + $decodedScheduler = json_decode($request->request->getString('scheduler'), true); + // a present-but-non-array value (e.g. JSON null) must still clear existing + // scheduled tasks, matching ApplySchedulerDataTrait's original behavior + $scheduler = is_array($decodedScheduler) ? $decodedScheduler : []; + } + + return new static( + id: (int) $request->request->getString('id'), + task: strtolower($request->query->getString('task')), + data: $request->request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? null) + : null, + properties: is_array($properties) ? $properties : null, + scheduler: $scheduler, + ); + } +} diff --git a/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php new file mode 100644 index 00000000..52de8423 --- /dev/null +++ b/src/Handler/Document/Hardlink/SaveHardlink/SaveHardlinkResult.php @@ -0,0 +1,31 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Link not found'); + } + + if (!$link->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + 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->phpMetaEnricher->enrich($cloned, $data); + $this->adminStyleEnricher->forEditor($cloned, $data); + $this->userNamesEnricher->enrich($cloned, $data); + $this->propertiesEnricher->enrich($cloned, $data); + $this->translationEnricher->enrich($cloned, $data); + + $this->preSendDataEventEnricher->enrich($cloned, $data); + + return new GetLinkDataResult(data: $data); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php b/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php new file mode 100644 index 00000000..272e1cef --- /dev/null +++ b/src/Handler/Document/Link/GetLinkData/GetLinkDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$link) { + throw new NotFoundHttpException('Link not found'); + } + + $this->mapper->applyLinkPayload($payload, $link); + $persistenceData = $this->coordinator->save($link, $payload->task); + + return new SaveLinkResult( + data: $persistenceData->data, + treeData: $persistenceData->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..98c5dce1 --- /dev/null +++ b/src/Handler/Document/Link/SaveLink/SaveLinkPayload.php @@ -0,0 +1,57 @@ +request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + + $scheduler = null; + if ($request->request->has('scheduler')) { + $decodedScheduler = json_decode($request->request->getString('scheduler'), true); + // a present-but-non-array value (e.g. JSON null) must still clear existing + // scheduled tasks, matching ApplySchedulerDataTrait's original behavior + $scheduler = is_array($decodedScheduler) ? $decodedScheduler : []; + } + + return new static( + id: (int) $request->request->getString('id'), + task: strtolower($request->query->getString('task')), + data: $request->request->has('data') + ? (json_decode($request->request->getString('data'), true) ?? null) + : null, + properties: is_array($properties) ? $properties : null, + scheduler: $scheduler, + ); + } +} diff --git a/src/Handler/Document/Link/SaveLink/SaveLinkResult.php b/src/Handler/Document/Link/SaveLink/SaveLinkResult.php new file mode 100644 index 00000000..7d52d3fb --- /dev/null +++ b/src/Handler/Document/Link/SaveLink/SaveLinkResult.php @@ -0,0 +1,31 @@ +path, '/'); + + if ($path === '') { + return; + } + + $messages = []; + + // must start with / + if (!str_starts_with($path, '/')) { + $messages[] = 'URL must start with /.'; + } + + if (strlen($path) < 2) { + $messages[] = 'URL must be at least 2 characters long.'; + } + + if (!Element\Service::isValidPath($path, 'document')) { + $messages[] = 'URL is invalid.'; + } + + if (empty($messages)) { + $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) { + $messages[] = 'URL path already exists.'; + + break; + } + } + } + } + + if (!empty($messages)) { + throw new AdminOperationFailedException(implode('
', $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/GeneratePagePreviews/GeneratePagePreviewsHandler.php b/src/Handler/Document/Page/GeneratePagePreviews/GeneratePagePreviewsHandler.php new file mode 100644 index 00000000..e63d8db4 --- /dev/null +++ b/src/Handler/Document/Page/GeneratePagePreviews/GeneratePagePreviewsHandler.php @@ -0,0 +1,44 @@ +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..db4999bc --- /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 new GenerateQrCodeResult($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/GenerateQrCode/GenerateQrCodeResult.php b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodeResult.php new file mode 100644 index 00000000..28168688 --- /dev/null +++ b/src/Handler/Document/Page/GenerateQrCode/GenerateQrCodeResult.php @@ -0,0 +1,27 @@ +id); + if (!$page) { + throw new NotFoundHttpException('Page not found'); + } + + if (!$page->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + 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->phpMetaEnricher->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); + + $this->preSendDataEventEnricher->enrich($page, $data); + + return new GetPageDataResult(data: $data); + } +} 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..cc686ee3 --- /dev/null +++ b/src/Handler/Document/Page/GetPageData/GetPageDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$document instanceof Document\Page) { + throw new NotFoundHttpException('Page not found'); + } + + return new GetPagePreviewImagePathResult($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/GetPagePreviewImagePath/GetPagePreviewImagePathResult.php b/src/Handler/Document/Page/GetPagePreviewImagePath/GetPagePreviewImagePathResult.php new file mode 100644 index 00000000..2fa0f8ba --- /dev/null +++ b/src/Handler/Document/Page/GetPagePreviewImagePath/GetPagePreviewImagePathResult.php @@ -0,0 +1,27 @@ +request->has('data') + ? json_decode($request->request->getString('data'), true) + : null; + + $properties = $request->request->has('properties') + ? json_decode($request->request->getString('properties'), true) + : null; + + $scheduler = null; + if ($request->request->has('scheduler')) { + $decodedScheduler = json_decode($request->request->getString('scheduler'), true); + // a present-but-non-array value (e.g. JSON null) must still clear existing + // scheduled tasks, matching ApplySchedulerDataTrait's original behavior + $scheduler = is_array($decodedScheduler) ? $decodedScheduler : []; + } + + return new static( + id: $request->request->getInt('id'), + task: strtolower($request->query->getString('task')), + settings: $request->request->has('settings') + ? (json_decode($request->request->getString('settings'), true) ?? null) + : null, + editables: is_array($editables) ? $editables : null, + appendEditables: (bool) $request->request->get('appendEditables'), + properties: is_array($properties) ? $properties : null, + scheduler: $scheduler, + 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..078a862b --- /dev/null +++ b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeHandler.php @@ -0,0 +1,77 @@ +requestStack->getCurrentRequest(); + + $document = Document\PageSnippet::getById($payload->documentId); + if (!$document) { + throw new NotFoundHttpException(); + } + + $document = clone $document; + $document->setEditables([]); + + $this->documentResolver->setDocument($request, $document); + + $this->twig->addGlobal('document', $document); + $this->twig->addGlobal('editmode', true); + + $request->attributes->set(EditmodeResolver::ATTRIBUTE_EDITMODE, true); + + $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( + 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..62e8b0dd --- /dev/null +++ b/src/Handler/Document/Page/RenderAreabrickIndexEditmode/RenderAreabrickIndexEditmodeResult.php @@ -0,0 +1,28 @@ +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->elementDraftService->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/SavePageDraftResult.php b/src/Handler/Document/Page/SavePage/SavePageDraftResult.php new file mode 100644 index 00000000..f8ec362c --- /dev/null +++ b/src/Handler/Document/Page/SavePage/SavePageDraftResult.php @@ -0,0 +1,28 @@ +id); + if (!$loadedPage) { + throw new NotFoundHttpException('Page not found'); + } + + $page = $this->elementDraftService->resolveDraft($loadedPage); + + $this->mapper->applyPagePayload($payload, $page, $payload->task); + + $persistenceData = $this->coordinator->save($page, $payload->task); + + $this->dispatchEvent( + new DocumentEvent($page, ['oldPage' => $loadedPage, 'task' => $payload->task]), + DocumentEvents::PAGE_POST_SAVE_ACTION, + ); + + if (!in_array($payload->task, ['publish', 'unpublish'], true)) { + $this->elementDraftService->saveDocument($page); + } + + if ($payload->task === 'publish' || $payload->task === 'unpublish') { + $data = $persistenceData->data; + if ($staticGeneratorEnabled = $page->getStaticGeneratorEnabled()) { + $data['staticGeneratorEnabled'] = $staticGeneratorEnabled; + $data['staticLastGenerated'] = $this->staticPageGenerator->getLastModified($page); + } + + return new SavePagePublishedResult( + data: $data, + treeData: $persistenceData->treeData + ); + } + + return new SavePageDraftResult( + treeData: $persistenceData->treeData, + draft: $persistenceData->draft ?? [] + ); + } +} 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 @@ +elementDraftService->removeDocument($payload->id); + } +} diff --git a/src/Handler/Document/RemoveFromSession/RemoveFromSessionPayload.php b/src/Handler/Document/RemoveFromSession/RemoveFromSessionPayload.php new file mode 100644 index 00000000..f4df752b --- /dev/null +++ b/src/Handler/Document/RemoveFromSession/RemoveFromSessionPayload.php @@ -0,0 +1,30 @@ +request->get('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..841b9339 --- /dev/null +++ b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletPayload.php @@ -0,0 +1,45 @@ +query->get('type'), + id: ((int) $request->query->get('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..7d641c14 --- /dev/null +++ b/src/Handler/Document/Renderlet/RenderRenderlet/RenderRenderletResult.php @@ -0,0 +1,27 @@ +id) { + return; + } + + $document = $this->elementDraftService->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->elementDraftService->saveDocument($document); + } +} diff --git a/src/Handler/Document/SaveToSession/SaveToSessionPayload.php b/src/Handler/Document/SaveToSession/SaveToSessionPayload.php new file mode 100644 index 00000000..1169d947 --- /dev/null +++ b/src/Handler/Document/SaveToSession/SaveToSessionPayload.php @@ -0,0 +1,55 @@ +request->get('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..7f00a289 --- /dev/null +++ b/src/Handler/Document/Site/GetSiteCustomSettings/GetSiteCustomSettingsResult.php @@ -0,0 +1,27 @@ +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..cca32e4c --- /dev/null +++ b/src/Handler/Document/Site/UpdateSite/UpdateSiteResult.php @@ -0,0 +1,27 @@ +id); + if (!$snippet) { + throw new NotFoundHttpException('Snippet not found'); + } + + if (!$snippet->isAllowed('view')) { + throw new AccessDeniedHttpException(); + } + + 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->phpMetaEnricher->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); + + $this->preSendDataEventEnricher->enrich($snippet, $data); + + return new GetSnippetDataResult(data: $data); + } +} \ No newline at end of file diff --git a/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php b/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php new file mode 100644 index 00000000..10125ec0 --- /dev/null +++ b/src/Handler/Document/Snippet/GetSnippetData/GetSnippetDataResult.php @@ -0,0 +1,27 @@ +id); + if (!$loadedSnippet) { + throw new NotFoundHttpException('Snippet not found'); + } + + $snippet = $this->elementDraftService->resolveDraft($loadedSnippet); + + $this->mapper->applyPagePayload($payload, $snippet, $payload->task); + + $persistenceData = $this->coordinator->save($snippet, $payload->task); + + $this->elementDraftService->saveDocument($snippet); + + if ($payload->task === 'publish' || $payload->task === 'unpublish') { + return new SaveSnippetPublishedResult( + data: $persistenceData->data, + treeData: $persistenceData->treeData, + ); + } + + return new SaveSnippetDraftResult(draft: $persistenceData->draft ?? []); + } +} 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..80544807 --- /dev/null +++ b/src/Handler/Document/Translation/CheckTranslationLanguage/CheckTranslationLanguageResult.php @@ -0,0 +1,33 @@ +found; + } +} diff --git a/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentHandler.php b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentHandler.php new file mode 100644 index 00000000..1c9bb4e2 --- /dev/null +++ b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentHandler.php @@ -0,0 +1,51 @@ +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..40fb82c4 --- /dev/null +++ b/src/Handler/Document/Translation/DetermineTranslationParent/DetermineTranslationParentResult.php @@ -0,0 +1,33 @@ +found; + } +} diff --git a/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeHandler.php b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeHandler.php new file mode 100644 index 00000000..5d8d3dad --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeHandler.php @@ -0,0 +1,80 @@ +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..4d70d13f --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTree/GetLanguageTreeResult.php @@ -0,0 +1,26 @@ +id); + + if (!$document) { + throw new AdminOperationFailedException(); + } + + $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..5d1b1418 --- /dev/null +++ b/src/Handler/Document/Translation/GetLanguageTreeRoot/GetLanguageTreeRootResult.php @@ -0,0 +1,28 @@ +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..7106e98c --- /dev/null +++ b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenHandler.php @@ -0,0 +1,139 @@ +allParams['limit'] ?? 100000000); + $offset = (int)($payload->allParams['start'] ?? 0); + + $filter = $payload->allParams['filter'] ?? null; + $sqlFilter = $filter; + if (!is_null($sqlFilter)) { + if (!str_ends_with($sqlFilter, '*')) { + $sqlFilter .= '*'; + } + $sqlFilter = str_replace('*', '%', $sqlFilter); + $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 ($sqlFilter) { + $condition = '(' . $condition . ')' . ' AND CAST(documents.key AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci LIKE ' . $db->quote($sqlFilter); + } + + $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'); + + $documents = $this->loadChildren($list); + } + + $event = new GenericEvent($this, ['documents' => $documents]); + $this->eventDispatcher->dispatch($event, AdminEvents::DOCUMENT_TREE_GET_CHILDREN_BY_ID_PRE_SEND_DATA); + $documents = $event->getArgument('documents'); + + if (!$payload->hasPagination()) { + return new TreeGetDocumentChildrenListResult($documents); + } + + return new TreeGetDocumentChildrenPaginatedResult( + nodes: $documents, + offset: $offset, + limit: $limit, + total: $document->getChildAmount($adminUser), + filter: $filter ?? '', + inSearch: $payload->inSearch, + ); + } + + private function loadChildren(Document\Listing $list): array + { + $documents = []; + foreach ($list->getDocuments() 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 $documents; + } +} diff --git a/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenListResult.php b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenListResult.php new file mode 100644 index 00000000..b799227e --- /dev/null +++ b/src/Handler/Document/TreeGetDocumentChildren/TreeGetDocumentChildrenListResult.php @@ -0,0 +1,27 @@ +allParams['limit']); + } + + public static function fromRequest(Request $request): static + { + return new static( + node: (int) $request->query->getString('node'), + inSearch: (int) $request->query->getString('inSearch'), + allParams: $request->query->all(), + ); + } +} diff --git a/src/Handler/Document/UpdateDocument/UpdateDocumentHandler.php b/src/Handler/Document/UpdateDocument/UpdateDocumentHandler.php new file mode 100644 index 00000000..cf13bcfe --- /dev/null +++ b/src/Handler/Document/UpdateDocument/UpdateDocumentHandler.php @@ -0,0 +1,182 @@ +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 AdminOperationFailedException("You can't rename or relocate if there's a newer not published version"); + } + } + + if ($document->isAllowed('settings')) { + if (!empty($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 AdminOperationFailedException($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 AdminOperationFailedException('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..7b1e0025 --- /dev/null +++ b/src/Handler/Document/UpdateDocument/UpdateDocumentResult.php @@ -0,0 +1,26 @@ +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..2128093d --- /dev/null +++ b/src/Handler/Document/Version/DiffVersions/DiffVersionsResult.php @@ -0,0 +1,30 @@ +userContext->getAdminUser()?->getId() ?? 0; + $version = Version::getById($payload->id); + $document = $version?->loadData(); + + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + $this->elementDraftService->saveDocument($document); + + $currentDocument = Document::getById($document->getId()); + if (!$currentDocument?->isAllowed('publish')) { + throw new AccessDeniedHttpException('Missing permission to publish document version'); + } + + $document->setPublished(true); + + try { + $document->setKey($currentDocument->getKey()); + $document->setPath($currentDocument->getRealPath()); + $document->setUserModification($userId); + $document->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + $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..8ac65be7 --- /dev/null +++ b/src/Handler/Document/Version/PublishVersion/PublishVersionResult.php @@ -0,0 +1,27 @@ +id); + $document = $version?->loadData(); + + if (!$document instanceof Document) { + throw new DocumentNotFoundException($payload->id); + } + + $this->elementDraftService->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..e6403d5e --- /dev/null +++ b/src/Handler/Element/AnalyzePermissions/AnalyzePermissionsResult.php @@ -0,0 +1,14 @@ +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..5032b1cd --- /dev/null +++ b/src/Handler/Element/DeleteAllVersions/DeleteAllVersionsPayload.php @@ -0,0 +1,29 @@ +request->get('date'); + $type = $request->request->get('type'); + + return new static( + id: $request->request->getInt('id'), + date: $date !== null ? (string) $date : null, + type: $type !== null ? (string) $type : null, + ); + } +} diff --git a/src/Handler/Element/DeleteDraft/DeleteDraftHandler.php b/src/Handler/Element/DeleteDraft/DeleteDraftHandler.php new file mode 100644 index 00000000..47a41b9d --- /dev/null +++ b/src/Handler/Element/DeleteDraft/DeleteDraftHandler.php @@ -0,0 +1,20 @@ +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..cd859277 --- /dev/null +++ b/src/Handler/Element/DeleteNote/DeleteNoteHandler.php @@ -0,0 +1,27 @@ +id); + + if (!$note) { + throw new AdminOperationFailedException('note_not_found'); + } + + if ($note->getLocked()) { + throw new AdminOperationFailedException('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..07184644 --- /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 AdminOperationFailedException('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..340d81e4 --- /dev/null +++ b/src/Handler/Element/FindUsages/FindUsagesResult.php @@ -0,0 +1,16 @@ +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 new GetDeleteInfoResult( + 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/GetDeleteInfo/GetDeleteInfoResult.php b/src/Handler/Element/GetDeleteInfo/GetDeleteInfoResult.php new file mode 100644 index 00000000..a43be04d --- /dev/null +++ b/src/Handler/Element/GetDeleteInfo/GetDeleteInfoResult.php @@ -0,0 +1,32 @@ +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..5777d318 --- /dev/null +++ b/src/Handler/Element/GetNicePath/GetNicePathPayload.php @@ -0,0 +1,61 @@ + */ + public readonly array $source; + + /** @var array */ + public readonly array $context; + + /** @var array */ + 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') : null; + $targets = $request->request->get('targets'); + + return new static( + source: self::decodeToArray($source), + context: self::decodeToArray($context), + targets: self::decodeToArray($targets), + loadEditModeData: $request->request->getBoolean('loadEditModeData'), + idProperty: (string) $request->request->get('idProperty', 'id'), + ); + } + + /** + * @return array + */ + private static function decodeToArray(mixed $value): array + { + if (!is_string($value)) { + return []; + } + + $decoded = json_decode($value, true); + + return is_array($decoded) ? $decoded : []; + } +} diff --git a/src/Handler/Element/GetNicePath/GetNicePathResult.php b/src/Handler/Element/GetNicePath/GetNicePathResult.php new file mode 100644 index 00000000..f7e11314 --- /dev/null +++ b/src/Handler/Element/GetNicePath/GetNicePathResult.php @@ -0,0 +1,14 @@ +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..b50d80a9 --- /dev/null +++ b/src/Handler/Element/GetNoteList/GetNoteListResult.php @@ -0,0 +1,15 @@ +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..3a422f6a --- /dev/null +++ b/src/Handler/Element/GetNoteTypes/GetNoteTypesResult.php @@ -0,0 +1,14 @@ +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..c94f7a44 --- /dev/null +++ b/src/Handler/Element/GetPredefinedProperties/GetPredefinedPropertiesResult.php @@ -0,0 +1,14 @@ +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..056214f3 --- /dev/null +++ b/src/Handler/Element/GetReplaceAssignmentsBatchJobs/GetReplaceAssignmentsBatchJobsResult.php @@ -0,0 +1,14 @@ +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/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/GetSubtype/GetSubtypeHandler.php b/src/Handler/Element/GetSubtype/GetSubtypeHandler.php new file mode 100644 index 00000000..e7282304 --- /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 AdminOperationFailedException('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: $resolvedType); + } +} 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..8be1846c --- /dev/null +++ b/src/Handler/Element/GetSubtype/GetSubtypeResult.php @@ -0,0 +1,16 @@ +elementType, self::ALLOWED_TYPES)) { + throw new AdminOperationFailedException('missing_permission'); + } + + $root = Service::getElementById($payload->elementType, $payload->id); + + if (!$root?->isAllowed('list')) { + throw new AdminOperationFailedException('', ['id' => $payload->id]); + } + + return new GetTreeRootResult($this->elementService->getElementTreeNodeConfig($root)); + } +} diff --git a/src/Handler/Element/GetTreeRoot/GetTreeRootPayload.php b/src/Handler/Element/GetTreeRoot/GetTreeRootPayload.php new file mode 100644 index 00000000..ecd7968b --- /dev/null +++ b/src/Handler/Element/GetTreeRoot/GetTreeRootPayload.php @@ -0,0 +1,36 @@ +query->get('elementType'), + id: filter_var($request->query->get('id'), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ?? 1, + ); + } +} diff --git a/src/Handler/Element/GetTreeRoot/GetTreeRootResult.php b/src/Handler/Element/GetTreeRoot/GetTreeRootResult.php new file mode 100644 index 00000000..da453cc6 --- /dev/null +++ b/src/Handler/Element/GetTreeRoot/GetTreeRootResult.php @@ -0,0 +1,26 @@ +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..030b48ca --- /dev/null +++ b/src/Handler/Element/GetVersions/GetVersionsResult.php @@ -0,0 +1,14 @@ +id, $payload->type, $this->sessionIdentity->getId()); + } +} diff --git a/src/Handler/Element/LockElement/LockElementPayload.php b/src/Handler/Element/LockElement/LockElementPayload.php new file mode 100644 index 00000000..928f70bf --- /dev/null +++ b/src/Handler/Element/LockElement/LockElementPayload.php @@ -0,0 +1,24 @@ +request->getInt('id'), + type: $request->request->getString('type'), + ); + } +} diff --git a/src/Handler/Element/NoteListPayload.php b/src/Handler/Element/NoteListPayload.php new file mode 100644 index 00000000..a293a4ea --- /dev/null +++ b/src/Handler/Element/NoteListPayload.php @@ -0,0 +1,50 @@ +request->has('data')) { + $data = json_decode($request->request->getString('data'), true) ?? []; + + return new static( + hasData: true, + id: (int) ($data['id'] ?? 0), + ); + } + + $filterText = $request->request->get('filterText'); + $filterJson = $request->request->get('filter'); + + return new static( + hasData: false, + offset: $request->request->getInt('start', 0), + limit: $request->request->getInt('limit') ?: null, + sortingSettings: QueryParams::extractSortingSettings($request->request->all()), + filterText: $filterText !== null ? (string) $filterText : null, + filterJson: $filterJson !== null ? (string) $filterJson : null, + 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..1e2b3e1d --- /dev/null +++ b/src/Handler/Element/ReplaceAssignments/ReplaceAssignmentsHandler.php @@ -0,0 +1,57 @@ +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 AdminOperationFailedException('One or more elements not found'); + } + + if ($payload->sourceType !== $payload->targetType || $sourceEl->getType() !== $targetEl->getType()) { + throw new AdminOperationFailedException('source-type and target-type do not match'); + } + + if (!$element->isAllowed('save')) { + throw new AdminOperationFailedException(''); + } + + $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/TypePath/TypePathHandler.php b/src/Handler/Element/TypePath/TypePathHandler.php new file mode 100644 index 00000000..2839e906 --- /dev/null +++ b/src/Handler/Element/TypePath/TypePathHandler.php @@ -0,0 +1,37 @@ +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 AdminOperationFailedException('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..fe34d3fd --- /dev/null +++ b/src/Handler/Element/TypePath/TypePathResult.php @@ -0,0 +1,18 @@ +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..b4e50f89 --- /dev/null +++ b/src/Handler/Element/UnlockElement/UnlockElementPayload.php @@ -0,0 +1,24 @@ +request->getString('id'), + type: $request->request->getString('type'), + ); + } +} diff --git a/src/Handler/Element/UnlockElements/UnlockElementsHandler.php b/src/Handler/Element/UnlockElements/UnlockElementsHandler.php new file mode 100644 index 00000000..1ddd2d0c --- /dev/null +++ b/src/Handler/Element/UnlockElements/UnlockElementsHandler.php @@ -0,0 +1,17 @@ +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/UnlockPropagate/UnlockPropagateHandler.php b/src/Handler/Element/UnlockPropagate/UnlockPropagateHandler.php new file mode 100644 index 00000000..6fcae532 --- /dev/null +++ b/src/Handler/Element/UnlockPropagate/UnlockPropagateHandler.php @@ -0,0 +1,21 @@ +type, $payload->id); + if (!$element) { + throw new AdminOperationFailedException(); + } + + $element->unlockPropagate(); + } +} 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/VersionUpdate/VersionUpdateHandler.php b/src/Handler/Element/VersionUpdate/VersionUpdateHandler.php new file mode 100644 index 00000000..c16f91bb --- /dev/null +++ b/src/Handler/Element/VersionUpdate/VersionUpdateHandler.php @@ -0,0 +1,22 @@ +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..6db6b2a8 --- /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: is_string($data) + ? (json_decode($data, true) ?? null) + : null, + ); + } +} diff --git a/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryHandler.php b/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryHandler.php new file mode 100644 index 00000000..81f2dedb --- /dev/null +++ b/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryHandler.php @@ -0,0 +1,23 @@ +data; + unset($data['id']); + + $address = new Tool\Email\Blocklist(); + $address->setValues($data); + $address->save(); + + return new CreateBlocklistEntryResult($address->getObjectVars()); + } +} diff --git a/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryResult.php b/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryResult.php new file mode 100644 index 00000000..50b4afd1 --- /dev/null +++ b/src/Handler/Email/Blocklist/CreateBlocklistEntry/CreateBlocklistEntryResult.php @@ -0,0 +1,14 @@ +data['address']); + $entry->delete(); + + return new DeleteBlocklistEntryResult(); + } +} diff --git a/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryResult.php b/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryResult.php new file mode 100644 index 00000000..1764eba2 --- /dev/null +++ b/src/Handler/Email/Blocklist/DeleteBlocklistEntry/DeleteBlocklistEntryResult.php @@ -0,0 +1,14 @@ +data['address']); + $address->setValues($payload->data); + $address->save(); + + return new UpdateBlocklistEntryResult($address->getObjectVars()); + } +} diff --git a/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryResult.php b/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryResult.php new file mode 100644 index 00000000..f84e8e52 --- /dev/null +++ b/src/Handler/Email/Blocklist/UpdateBlocklistEntry/UpdateBlocklistEntryResult.php @@ -0,0 +1,14 @@ +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: is_array($data) ? $data : []); + } + + return new static( + hasData: false, + limit: (int) $request->request->getString('limit', '50'), + offset: (int) $request->request->getString('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..928008db --- /dev/null +++ b/src/Handler/Email/DeleteEmailLog/DeleteEmailLogHandler.php @@ -0,0 +1,22 @@ +id); + if (!$emailLog instanceof Tool\Email\Log) { + throw new AdminOperationFailedException('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..ffb14d28 --- /dev/null +++ b/src/Handler/Email/GetBlocklist/GetBlocklistResult.php @@ -0,0 +1,15 @@ +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..4e5e2d6d --- /dev/null +++ b/src/Handler/Email/GetEmailLogs/GetEmailLogsResult.php @@ -0,0 +1,15 @@ +id); + if (!$emailLog instanceof Tool\Email\Log) { + throw new AdminOperationFailedException('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..ca69b566 --- /dev/null +++ b/src/Handler/Email/SendTestEmail/SendTestEmailPayload.php @@ -0,0 +1,45 @@ +|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')) { + $decodedMailParameters = json_decode($request->request->get('mailParamaters'), true) ?: null; + $mailParameters = is_array($decodedMailParameters) ? $decodedMailParameters : 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/GetEmailLogDetails/GetEmailLogDetailsHandler.php b/src/Handler/Email/ShowEmailLog/GetEmailLogDetails/GetEmailLogDetailsHandler.php new file mode 100644 index 00000000..c0bd7e0d --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogDetails/GetEmailLogDetailsHandler.php @@ -0,0 +1,23 @@ +id); + + if (!$log) { + throw new NotFoundHttpException(sprintf('Email log with id %d not found', $payload->id)); + } + + return new GetEmailLogDetailsResult(objectVars: $log->getObjectVars()); + } +} \ No newline at end of file diff --git a/src/Handler/Email/ShowEmailLog/GetEmailLogDetails/GetEmailLogDetailsResult.php b/src/Handler/Email/ShowEmailLog/GetEmailLogDetails/GetEmailLogDetailsResult.php new file mode 100644 index 00000000..34ccfdc2 --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogDetails/GetEmailLogDetailsResult.php @@ -0,0 +1,14 @@ +id); + + if (!$log) { + throw new NotFoundHttpException(sprintf('Email log with id %d not found', $payload->id)); + } + + return new GetEmailLogHtmlResult(htmlLog: $log->getHtmlLog() ?: null); + } +} \ No newline at end of file diff --git a/src/Handler/Email/ShowEmailLog/GetEmailLogHtml/GetEmailLogHtmlResult.php b/src/Handler/Email/ShowEmailLog/GetEmailLogHtml/GetEmailLogHtmlResult.php new file mode 100644 index 00000000..a695a86c --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogHtml/GetEmailLogHtmlResult.php @@ -0,0 +1,14 @@ +id); + if (!$emailLog) { + throw new NotFoundHttpException(); + } + + try { + $params = $emailLog->getParams(); + } catch (\Exception) { + Logger::warning('Could not decode JSON param string'); + $params = []; + } + + foreach ($params as &$entry) { + $this->enhanceLoggingData($entry); + } + + return new GetEmailLogParamsResult(params: $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/GetEmailLogParams/GetEmailLogParamsResult.php b/src/Handler/Email/ShowEmailLog/GetEmailLogParams/GetEmailLogParamsResult.php new file mode 100644 index 00000000..dd31c770 --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogParams/GetEmailLogParamsResult.php @@ -0,0 +1,14 @@ +id); + + if (!$log) { + throw new NotFoundHttpException(sprintf('Email log with id %d not found', $payload->id)); + } + + return new GetEmailLogTextResult(textLog: $log->getTextLog() ?: null); + } +} \ No newline at end of file diff --git a/src/Handler/Email/ShowEmailLog/GetEmailLogText/GetEmailLogTextResult.php b/src/Handler/Email/ShowEmailLog/GetEmailLogText/GetEmailLogTextResult.php new file mode 100644 index 00000000..355d518e --- /dev/null +++ b/src/Handler/Email/ShowEmailLog/GetEmailLogText/GetEmailLogTextResult.php @@ -0,0 +1,14 @@ +id); + if (!$asset) { + throw new NotFoundHttpException('Asset not found'); + } + if (!$asset->isAllowed('view')) { + throw new AccessDeniedHttpException('Export denied'); + } + + $zipContent = $this->assets->doExportData($asset); + + return new ExportAssetResult( + zipContent: $zipContent, + suggestedFilename: $asset->getFilename(), + ); + } +} diff --git a/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php new file mode 100644 index 00000000..6584b647 --- /dev/null +++ b/src/Handler/GDPR/Asset/ExportAsset/ExportAssetResult.php @@ -0,0 +1,28 @@ +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..ecb4c294 --- /dev/null +++ b/src/Handler/GDPR/Asset/SearchAssets/SearchAssetsResult.php @@ -0,0 +1,27 @@ +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() ?? 0); + } +} diff --git a/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php b/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php new file mode 100644 index 00000000..cdbafcdc --- /dev/null +++ b/src/Handler/GDPR/DataObject/ExportDataObject/ExportDataObjectResult.php @@ -0,0 +1,28 @@ +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..9402f2ab --- /dev/null +++ b/src/Handler/GDPR/DataObject/SearchDataObjects/SearchDataObjectsResult.php @@ -0,0 +1,27 @@ +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..ea669426 --- /dev/null +++ b/src/Handler/GDPR/GetDataProviders/GetDataProvidersResult.php @@ -0,0 +1,27 @@ +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..8ad586fc --- /dev/null +++ b/src/Handler/GDPR/OpenDxpUsers/ExportUserData/ExportUserDataResult.php @@ -0,0 +1,27 @@ +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..2a94f826 --- /dev/null +++ b/src/Handler/GDPR/OpenDxpUsers/SearchUsers/SearchUsersResult.php @@ -0,0 +1,27 @@ +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..343c62a4 --- /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() ?? 0); + } +} diff --git a/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php new file mode 100644 index 00000000..1dcd6c5d --- /dev/null +++ b/src/Handler/GDPR/SentMail/ExportSentMail/ExportSentMailResult.php @@ -0,0 +1,28 @@ +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..38185fbc --- /dev/null +++ b/src/Handler/Install/CheckSystem/CheckSystemResult.php @@ -0,0 +1,14 @@ +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..5d2cdfeb --- /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..7c8eb578 --- /dev/null +++ b/src/Handler/Login/Deeplink/DeeplinkResult.php @@ -0,0 +1,16 @@ +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(); + + $this->twoFactorSetupSession->storeSecret($secret); + + return new GenerateTwoFactorSetupResult( + 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..f2e0f7df --- /dev/null +++ b/src/Handler/Login/GenerateTwoFactorSetup/GenerateTwoFactorSetupResult.php @@ -0,0 +1,14 @@ +query->has('perspective') ? strip_tags($request->query->getString('perspective')) : null, + ); + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordHandler.php b/src/Handler/Login/LostPassword/LostPasswordHandler.php new file mode 100644 index 00000000..17e6ba76 --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordHandler.php @@ -0,0 +1,107 @@ +isPost) { + return new LostPasswordResult(error: null); + } + + $error = null; + + $user = User::getByName($payload->username); + if (!$user instanceof User) { + $error = 'user_unknown'; + } + + // consumed unconditionally, even for an unknown username, so enumeration attempts are rate-limited too + $limiter = $this->resetPasswordLimiter->create($payload->clientIp); + 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'; + } + } + + $eventResponse = null; + + if (!$error) { + $token = Authentication::generateTokenByUser($user); + + try { + $domain = $this->hostResolver->resolve(['source' => $this->requestStack->getCurrentRequest()]) ?? ''; + 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(); + } + + if ($event->hasResponse()) { + $eventResponse = $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)); + } + + return new LostPasswordResult(error: $error, eventResponse: $eventResponse); + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordPayload.php b/src/Handler/Login/LostPassword/LostPasswordPayload.php new file mode 100644 index 00000000..eb6ef753 --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordPayload.php @@ -0,0 +1,26 @@ +request->get('username'), + clientIp: (string) $request->getClientIp(), + isPost: $request->isMethod('POST') && $request->request->has('username'), + ); + } +} diff --git a/src/Handler/Login/LostPassword/LostPasswordResult.php b/src/Handler/Login/LostPassword/LostPasswordResult.php new file mode 100644 index 00000000..a47417ab --- /dev/null +++ b/src/Handler/Login/LostPassword/LostPasswordResult.php @@ -0,0 +1,16 @@ +twoFactorSetupSession->getSecret(); + + if (!$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', $secret); + + if (!$this->twoFactor->checkCode($proxyUser, $payload->authCode)) { + throw new TwoFactorCodeInvalidException('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..7c32c6fc --- /dev/null +++ b/src/Handler/Login/SaveTwoFactorSetup/SaveTwoFactorSetupPayload.php @@ -0,0 +1,22 @@ +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..e28660c5 --- /dev/null +++ b/src/Handler/Misc/AdminCss/AdminCssHandler.php @@ -0,0 +1,41 @@ +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..49757674 --- /dev/null +++ b/src/Handler/Misc/GetAvailableControllerReferences/GetAvailableControllerReferencesResult.php @@ -0,0 +1,28 @@ +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..76a92d2f --- /dev/null +++ b/src/Handler/Misc/GetAvailableTemplates/GetAvailableTemplatesResult.php @@ -0,0 +1,27 @@ +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..8951733d --- /dev/null +++ b/src/Handler/Misc/GetCountryList/GetCountryListResult.php @@ -0,0 +1,27 @@ +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..a421c037 --- /dev/null +++ b/src/Handler/Misc/GetIconList/GetIconListResult.php @@ -0,0 +1,31 @@ +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..dbde10fc --- /dev/null +++ b/src/Handler/Misc/GetJsonTranslations/GetJsonTranslationsResult.php @@ -0,0 +1,27 @@ +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..47935a53 --- /dev/null +++ b/src/Handler/Misc/GetLanguageFlag/GetLanguageFlagResult.php @@ -0,0 +1,27 @@ + $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..55723a69 --- /dev/null +++ b/src/Handler/Misc/GetLanguageList/GetLanguageListResult.php @@ -0,0 +1,27 @@ +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..7b72503e --- /dev/null +++ b/src/Handler/Misc/GetValidFilename/GetValidFilenameResult.php @@ -0,0 +1,27 @@ +activate) { + $this->maintenanceModeHelper->activate($this->sessionIdentity->getId()); + } + + 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..67c48f41 --- /dev/null +++ b/src/Handler/Misc/Maintenance/MaintenancePayload.php @@ -0,0 +1,37 @@ +query->has('activate') ? $request->query->getString('activate') : null, + deactivate: $request->query->has('deactivate') ? $request->query->getString('deactivate') : null, + ); + } +} 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..13988583 --- /dev/null +++ b/src/Handler/Misc/ScriptProxy/ScriptProxyResult.php @@ -0,0 +1,28 @@ +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..6b65eb37 --- /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..59d5957b --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsHandler.php @@ -0,0 +1,52 @@ + (int) $this->userContext->getAdminUser()?->getId()]; + + foreach ($this->filterParser->parse($payload->filter) 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..8ae0a68c --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsPayload.php @@ -0,0 +1,40 @@ +request->getString('filter', '[]'), true); + + return new static( + offset: $request->request->getInt('start'), + limit: $request->request->getInt('limit', 40), + filter: is_array($decodedFilter) ? $decodedFilter : [], + ); + } +} diff --git a/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php new file mode 100644 index 00000000..b7c817f0 --- /dev/null +++ b/src/Handler/Notification/FindAllNotifications/FindAllNotificationsResult.php @@ -0,0 +1,28 @@ +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..a57a60d4 --- /dev/null +++ b/src/Handler/Notification/FindLastUnreadNotifications/FindLastUnreadNotificationsResult.php @@ -0,0 +1,29 @@ +userContext->getAdminUser()?->getId() ?? 0; + try { + $notification = $this->notificationService->findAndMarkAsRead($payload->id, $userId); + } catch (UnexpectedValueException $e) { + throw new AdminOperationFailedException(sprintf('Notification with id %d not found', $payload->id)); + } + + $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..725237c0 --- /dev/null +++ b/src/Handler/Notification/FindNotification/FindNotificationResult.php @@ -0,0 +1,27 @@ +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..de1ea94a --- /dev/null +++ b/src/Handler/Notification/GetRecipients/GetRecipientsResult.php @@ -0,0 +1,27 @@ +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..dd8adfff --- /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..bb5c48de --- /dev/null +++ b/src/Handler/Portal/AddWidget/AddWidgetHandler.php @@ -0,0 +1,53 @@ +userContext->getAdminUser(); + $config = $this->dashboardService->getDashboard($user, $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, + ]; + + $this->dashboardService->saveDashboard($user, $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..2d1cb4ae --- /dev/null +++ b/src/Handler/Portal/AddWidget/AddWidgetResult.php @@ -0,0 +1,26 @@ +key)) { + throw new AdminOperationFailedException('empty'); + } + + $user = $this->userContext->getAdminUser(); + + $dashboards = $this->dashboardService->getAllDashboards($user); + if (isset($dashboards[$payload->key])) { + throw new AdminOperationFailedException('name_already_in_use'); + } + + $this->dashboardService->saveDashboard($user, $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..bf2dd652 --- /dev/null +++ b/src/Handler/Portal/DeleteDashboard/DeleteDashboardHandler.php @@ -0,0 +1,34 @@ +dashboardService->deleteDashboard($this->userContext->getAdminUser(), $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..4e1731c9 --- /dev/null +++ b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationHandler.php @@ -0,0 +1,36 @@ +dashboardService->getDashboard($this->userContext->getAdminUser(), $payload->key ?? 'welcome'); + + return new GetDashboardConfigurationResult(config: $config); + } +} 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..91c8b702 --- /dev/null +++ b/src/Handler/Portal/GetDashboardConfiguration/GetDashboardConfigurationResult.php @@ -0,0 +1,26 @@ +dashboardService->getAllDashboards($this->userContext->getAdminUser()); + + $dashboards = []; + foreach (array_keys($allDashboards) 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..83b5ec67 --- /dev/null +++ b/src/Handler/Portal/GetDashboardList/GetDashboardListResult.php @@ -0,0 +1,26 @@ +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..ed280fad --- /dev/null +++ b/src/Handler/Portal/GetModificationStatistics/GetModificationStatisticsResult.php @@ -0,0 +1,28 @@ +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..af36b4b8 --- /dev/null +++ b/src/Handler/Portal/GetModifiedAssets/GetModifiedAssetsResult.php @@ -0,0 +1,28 @@ +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..8343322a --- /dev/null +++ b/src/Handler/Portal/GetModifiedDocuments/GetModifiedDocumentsResult.php @@ -0,0 +1,28 @@ +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..5ecc835a --- /dev/null +++ b/src/Handler/Portal/GetModifiedObjects/GetModifiedObjectsResult.php @@ -0,0 +1,28 @@ +userContext->getAdminUser(); + $config = $this->dashboardService->getDashboard($user, $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; + $this->dashboardService->saveDashboard($user, $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..cef1e8ac --- /dev/null +++ b/src/Handler/Portal/ReorderWidget/ReorderWidgetHandler.php @@ -0,0 +1,54 @@ +userContext->getAdminUser(); + $config = $this->dashboardService->getDashboard($user, $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; + $this->dashboardService->saveDashboard($user, $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..f1b55efc --- /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: (int) $request->request->getString('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..f2f184de --- /dev/null +++ b/src/Handler/Portal/UpdatePortletConfig/UpdatePortletConfigHandler.php @@ -0,0 +1,45 @@ +userContext->getAdminUser(); + $config = $this->dashboardService->getDashboard($user, $payload->dashboardKey); + foreach ($config['positions'] as &$col) { + foreach ($col as &$portlet) { + if ($portlet['id'] === $payload->portletId) { + $portlet['config'] = $payload->configuration; + break; + } + } + } + + $this->dashboardService->saveDashboard($user, $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..0143fd45 --- /dev/null +++ b/src/Handler/Recyclebin/AddToRecyclebin/AddToRecyclebinHandler.php @@ -0,0 +1,57 @@ +addToRecyclebin($payload); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + } + + private function addToRecyclebin(AddToRecyclebinPayload $payload): void + { + $adminUser = $this->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..8aa652ca --- /dev/null +++ b/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemHandler.php @@ -0,0 +1,34 @@ +id); + if ($item) { + $item->delete(); + } + + return new DeleteRecyclebinItemResult(); + } +} \ No newline at end of file diff --git a/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemResult.php b/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemResult.php new file mode 100644 index 00000000..0d7e1c27 --- /dev/null +++ b/src/Handler/Recyclebin/DeleteRecyclebinItem/DeleteRecyclebinItemResult.php @@ -0,0 +1,27 @@ +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..eca8430f --- /dev/null +++ b/src/Handler/Recyclebin/ListRecyclebin/ListRecyclebinResult.php @@ -0,0 +1,28 @@ +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'; + + $filters = json_decode($request->request->getString('filter', '[]'), true); + + 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: is_array($filters) ? $filters : [], + ); + } +} 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/ResultInterface.php b/src/Handler/ResultInterface.php new file mode 100644 index 00000000..9ecb933d --- /dev/null +++ b/src/Handler/ResultInterface.php @@ -0,0 +1,21 @@ +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..de4818a4 --- /dev/null +++ b/src/Handler/Settings/AddThumbnail/AddThumbnailResult.php @@ -0,0 +1,33 @@ +created; + } +} diff --git a/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailHandler.php b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailHandler.php new file mode 100644 index 00000000..73f0ad50 --- /dev/null +++ b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailHandler.php @@ -0,0 +1,47 @@ +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..7b58959d --- /dev/null +++ b/src/Handler/Settings/AddVideoThumbnail/AddVideoThumbnailResult.php @@ -0,0 +1,33 @@ +created; + } +} diff --git a/src/Handler/Settings/ClearCache/ClearCacheHandler.php b/src/Handler/Settings/ClearCache/ClearCacheHandler.php new file mode 100644 index 00000000..75d37952 --- /dev/null +++ b/src/Handler/Settings/ClearCache/ClearCacheHandler.php @@ -0,0 +1,43 @@ +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..92961d84 --- /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 AdminOperationFailedException('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..600bcdb4 --- /dev/null +++ b/src/Handler/Settings/CreatePredefinedMetadata/CreatePredefinedMetadataResult.php @@ -0,0 +1,27 @@ +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..6d226e42 --- /dev/null +++ b/src/Handler/Settings/CreatePredefinedProperty/CreatePredefinedPropertyResult.php @@ -0,0 +1,27 @@ +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..801d2d6f --- /dev/null +++ b/src/Handler/Settings/CreateWebsiteSetting/CreateWebsiteSettingResult.php @@ -0,0 +1,27 @@ +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..3b276226 --- /dev/null +++ b/src/Handler/Settings/DeleteWebsiteSetting/DeleteWebsiteSettingHandler.php @@ -0,0 +1,36 @@ +data['id']); + + if (!$setting instanceof WebsiteSetting) { + throw new AdminOperationFailedException('Website setting not found'); + } + + $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..0b4c9578 --- /dev/null +++ b/src/Handler/Settings/DisplayCustomLogo/DisplayCustomLogoResult.php @@ -0,0 +1,31 @@ +config->getAdminSystemSettingsConfig()); + } +} diff --git a/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php b/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php new file mode 100644 index 00000000..8ccdc3dc --- /dev/null +++ b/src/Handler/Settings/GetAppearanceSettings/GetAppearanceSettingsResult.php @@ -0,0 +1,25 @@ + $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..8e925498 --- /dev/null +++ b/src/Handler/Settings/GetAvailableAdminLanguages/GetAvailableAdminLanguagesResult.php @@ -0,0 +1,25 @@ + 'password_hash', 'value' => 'password_hash']]; + + foreach (hash_algos() as $algorithm) { + $options[] = [ + 'key' => $algorithm . ' (' . $this->translator->trans('deprecated', [], 'admin') . ')', + 'value' => $algorithm, + ]; + } + + return new GetAvailableAlgorithmsResult(data: $options, total: count($options)); + } +} diff --git a/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php b/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php new file mode 100644 index 00000000..cc259035 --- /dev/null +++ b/src/Handler/Settings/GetAvailableAlgorithms/GetAvailableAlgorithmsResult.php @@ -0,0 +1,28 @@ +localeService->getDisplayRegions(); + asort($countries); + + $options = []; + foreach ($countries as $short => $translation) { + if (strlen((string) $short) === 2) { + $options[] = ['key' => $translation . ' (' . $short . ')', 'value' => $short]; + } + } + + return new GetAvailableCountriesResult(data: $options, total: count($options)); + } +} diff --git a/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php b/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php new file mode 100644 index 00000000..fb265456 --- /dev/null +++ b/src/Handler/Settings/GetAvailableCountries/GetAvailableCountriesResult.php @@ -0,0 +1,28 @@ +userContext->getAdminUser(); + + if ($adminUser === null || !$adminUser->isAllowed('documents')) { + Logger::log('[Startup] Sites are not loaded as "documents" permission is missing'); + + return new GetAvailableSitesResult(sites: []); + } + + $sitesList = new Model\Site\Listing(); + $sitesObjects = $sitesList->load(); + $sites = []; + + if (!$payload->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/GetAvailableSitesPayload.php b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesPayload.php new file mode 100644 index 00000000..e00052e7 --- /dev/null +++ b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesPayload.php @@ -0,0 +1,34 @@ +query->get('excludeMainSite'), + ); + } +} diff --git a/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php new file mode 100644 index 00000000..bbb9db4f --- /dev/null +++ b/src/Handler/Settings/GetAvailableSites/GetAvailableSitesResult.php @@ -0,0 +1,25 @@ +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..ff80894e --- /dev/null +++ b/src/Handler/Settings/GetDownloadableThumbnails/GetDownloadableThumbnailsResult.php @@ -0,0 +1,27 @@ +type, [$payload->subType]); + $result = []; + + foreach ($list as $item) { + $itemGroup = $item->getGroup() ?? ''; + if ($payload->group === 'default' || $payload->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/GetFilteredPredefinedMetadataPayload.php b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataPayload.php new file mode 100644 index 00000000..a820a300 --- /dev/null +++ b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataPayload.php @@ -0,0 +1,38 @@ +query->has('type') ? $request->query->getString('type') : null, + subType: $request->query->has('subType') ? $request->query->getString('subType') : null, + group: $request->query->has('group') ? $request->query->getString('group') : null, + ); + } +} diff --git a/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php new file mode 100644 index 00000000..3d81a3fa --- /dev/null +++ b/src/Handler/Settings/GetFilteredPredefinedMetadata/GetFilteredPredefinedMetadataResult.php @@ -0,0 +1,25 @@ +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..699c9354 --- /dev/null +++ b/src/Handler/Settings/GetPredefinedMetadataList/GetPredefinedMetadataListResult.php @@ -0,0 +1,28 @@ +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..81ac1332 --- /dev/null +++ b/src/Handler/Settings/GetPredefinedPropertiesList/GetPredefinedPropertiesListResult.php @@ -0,0 +1,28 @@ +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, config: ['languages' => $languageOptions]); + } +} diff --git a/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php b/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php new file mode 100644 index 00000000..c1a06258 --- /dev/null +++ b/src/Handler/Settings/GetSystemSettings/GetSystemSettingsResult.php @@ -0,0 +1,31 @@ +name); + $data = $pipe->getObjectVars(); + $data['writeable'] = $pipe->isWriteable(); + + return new GetThumbnailResult(data: $data); + } +} diff --git a/src/Handler/Settings/GetThumbnail/GetThumbnailPayload.php b/src/Handler/Settings/GetThumbnail/GetThumbnailPayload.php new file mode 100644 index 00000000..e381ad93 --- /dev/null +++ b/src/Handler/Settings/GetThumbnail/GetThumbnailPayload.php @@ -0,0 +1,34 @@ +query->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php b/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php new file mode 100644 index 00000000..0f91135d --- /dev/null +++ b/src/Handler/Settings/GetThumbnail/GetThumbnailResult.php @@ -0,0 +1,27 @@ +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..555821f7 --- /dev/null +++ b/src/Handler/Settings/GetThumbnailTree/GetThumbnailTreeResult.php @@ -0,0 +1,27 @@ +name); + $data = $pipe->getObjectVars(); + $data['writeable'] = $pipe->isWriteable(); + + return new GetVideoThumbnailResult(data: $data); + } +} diff --git a/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailPayload.php b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailPayload.php new file mode 100644 index 00000000..f92989a3 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailPayload.php @@ -0,0 +1,34 @@ +query->getString('name'), + ); + } +} diff --git a/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php new file mode 100644 index 00000000..28305f68 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnail/GetVideoThumbnailResult.php @@ -0,0 +1,27 @@ + '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..7d9b27a3 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnailList/GetVideoThumbnailListResult.php @@ -0,0 +1,27 @@ +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..e2d8e0f8 --- /dev/null +++ b/src/Handler/Settings/GetVideoThumbnailTree/GetVideoThumbnailTreeResult.php @@ -0,0 +1,27 @@ +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..f3a1b4f5 --- /dev/null +++ b/src/Handler/Settings/GetWebsiteSettingsList/GetWebsiteSettingsListResult.php @@ -0,0 +1,28 @@ +request->has('data')) { + return new static( + hasData: true, + data: json_decode($request->request->getString('data'), true) ?? [], + ); + } + + return new static( + hasData: false, + filter: $request->request->has('filter') ? $request->request->getString('filter') : null, + ); + } +} diff --git a/src/Handler/Settings/PredefinedPropertyPayload.php b/src/Handler/Settings/PredefinedPropertyPayload.php new file mode 100644 index 00000000..f5b66dff --- /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->has('filter') ? $request->request->getString('filter') : null, + ); + } +} 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..d3fb81ea --- /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..d282425f --- /dev/null +++ b/src/Handler/Settings/ThumbnailAdapterCheck/ThumbnailAdapterCheckResult.php @@ -0,0 +1,25 @@ +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 AdminOperationFailedException('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..9d6c64f4 --- /dev/null +++ b/src/Handler/Settings/UpdatePredefinedMetadata/UpdatePredefinedMetadataResult.php @@ -0,0 +1,27 @@ +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..9ae3ba66 --- /dev/null +++ b/src/Handler/Settings/UpdatePredefinedProperty/UpdatePredefinedPropertyResult.php @@ -0,0 +1,27 @@ +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..75715b8f --- /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..30f9b791 --- /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..2ee2424a --- /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 AdminOperationFailedException(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..36a3b90a --- /dev/null +++ b/src/Handler/Settings/UpdateWebsiteSetting/UpdateWebsiteSettingResult.php @@ -0,0 +1,27 @@ +logoFile instanceof UploadedFile) { + throw new BadRequestHttpException('No file uploaded.'); + } + + $extension = $payload->logoFile->guessExtension() ?? ''; + if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) { + throw new Exception('Unsupported file format.'); + } + + Tool\Storage::get('admin')->writeStream(self::LOGO_PATH, fopen($payload->logoFile->getPathname(), 'rb')); + } +} diff --git a/src/Handler/Settings/UploadCustomLogo/UploadCustomLogoPayload.php b/src/Handler/Settings/UploadCustomLogo/UploadCustomLogoPayload.php new file mode 100644 index 00000000..a321a587 --- /dev/null +++ b/src/Handler/Settings/UploadCustomLogo/UploadCustomLogoPayload.php @@ -0,0 +1,37 @@ +files->get('Filedata'); + + return new static( + logoFile: $logoFile instanceof UploadedFile ? $logoFile : null, + ); + } +} diff --git a/src/Handler/Settings/WebsiteSettingPayload.php b/src/Handler/Settings/WebsiteSettingPayload.php new file mode 100644 index 00000000..11f9dd95 --- /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: (int) $request->request->getString('limit', '50'), + offset: (int) $request->request->getString('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..1a48e32a --- /dev/null +++ b/src/Handler/Tags/AddTag/AddTagHandler.php @@ -0,0 +1,39 @@ +setName($payload->text); + $tag->setParentId($payload->parentId); + + try { + $tag->save(); + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + return new AddTagResult(id: $tag->getId() ?? 0); + } +} 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..085330f4 --- /dev/null +++ b/src/Handler/Tags/AddTag/AddTagResult.php @@ -0,0 +1,27 @@ +tagId); + if (!$tag) { + throw new AdminOperationFailedException('Tag with ID ' . $payload->tagId . ' not found.'); + } + + Tag::addTagToElement($payload->elementType, $payload->elementId, $tag); + + return new AddTagToElementResult(id: $tag->getId() ?? 0); + } +} 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..e3f02c33 --- /dev/null +++ b/src/Handler/Tags/AddTagToElement/AddTagToElementResult.php @@ -0,0 +1,27 @@ +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..b6ca019e --- /dev/null +++ b/src/Handler/Tags/DoBatchAssignment/DoBatchAssignmentPayload.php @@ -0,0 +1,44 @@ +request->get('childrenIds'), true); + $assignedTags = json_decode($request->request->get('assignedTags'), true); + + return new static( + elementType: strip_tags($request->request->get('elementType', '')), + childrenIds: is_array($childrenIds) ? $childrenIds : [], + assignedTags: is_array($assignedTags) ? $assignedTags : [], + 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..3bcf074b --- /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(idLists: $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..8e63c1bd --- /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..4198c80d --- /dev/null +++ b/src/Handler/Tags/GetBatchAssignmentJobs/GetBatchAssignmentJobsResult.php @@ -0,0 +1,31 @@ +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 = ?', (int) $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..466565c0 --- /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: strip_tags($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..88598af3 --- /dev/null +++ b/src/Handler/Tags/GetTagTreeChildren/GetTagTreeChildrenResult.php @@ -0,0 +1,30 @@ +> $tags + */ + public function __construct( + public array $tags, + ) {} +} diff --git a/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementHandler.php b/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementHandler.php new file mode 100644 index 00000000..720a95ff --- /dev/null +++ b/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementHandler.php @@ -0,0 +1,57 @@ +assignmentCId || !$payload->assignmentCType) { + return new GetTagsForElementResult(tags: []); + } + + $assignedTagArray = []; + $assignedTags = Tag::getTagsForElement($payload->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..537813f4 --- /dev/null +++ b/src/Handler/Tags/LoadTagsForElement/GetTagsForElement/GetTagsForElementResult.php @@ -0,0 +1,30 @@ +> $tags + */ + public function __construct( + public array $tags, + ) {} +} diff --git a/src/Handler/Tags/LoadTagsForElement/LoadTagsForElementPayload.php b/src/Handler/Tags/LoadTagsForElement/LoadTagsForElementPayload.php new file mode 100644 index 00000000..7987a2a6 --- /dev/null +++ b/src/Handler/Tags/LoadTagsForElement/LoadTagsForElementPayload.php @@ -0,0 +1,37 @@ +query->has('assignmentCId') ? (int) $request->query->get('assignmentCId') : null, + assignmentCType: strip_tags($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..27615c8b --- /dev/null +++ b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementHandler.php @@ -0,0 +1,36 @@ +tagId); + if (!$tag) { + throw new AdminOperationFailedException('Tag with ID ' . $payload->tagId . ' not found.'); + } + + Tag::removeTagFromElement($payload->elementType, $payload->elementId, $tag); + + return new RemoveTagFromElementResult(id: $tag->getId() ?? 0); + } +} 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..0dfd043a --- /dev/null +++ b/src/Handler/Tags/RemoveTagFromElement/RemoveTagFromElementResult.php @@ -0,0 +1,27 @@ +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..b44ddaa3 --- /dev/null +++ b/src/Handler/Translation/AddAdminTranslationKeys/AddAdminTranslationKeysPayload.php @@ -0,0 +1,37 @@ +request->get('keys'); + $decoded = $keys ? json_decode((string) $keys, true) : []; + + return new static( + keys: is_array($decoded) ? $decoded : [], + ); + } +} diff --git a/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsHandler.php b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsHandler.php new file mode 100644 index 00000000..915cbe68 --- /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, id: $exportId); + } +} diff --git a/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsPayload.php b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsPayload.php new file mode 100644 index 00000000..e054a6c8 --- /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((string) $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: (string) $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..b3fba375 --- /dev/null +++ b/src/Handler/Translation/BuildContentExportJobs/BuildContentExportJobsResult.php @@ -0,0 +1,31 @@ +>> $jobs + */ + public function __construct( + public array $jobs, + public string $id, + ) {} +} diff --git a/src/Handler/Translation/CleanupTranslations/CleanupTranslationsHandler.php b/src/Handler/Translation/CleanupTranslations/CleanupTranslationsHandler.php new file mode 100644 index 00000000..5a711b03 --- /dev/null +++ b/src/Handler/Translation/CleanupTranslations/CleanupTranslationsHandler.php @@ -0,0 +1,33 @@ +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..9b370cdd --- /dev/null +++ b/src/Handler/Translation/CreateTranslation/CreateTranslationHandler.php @@ -0,0 +1,67 @@ +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 AdminOperationFailedException('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(); + + $prefixedTranslations = []; + foreach ($t->getTranslations() as $lang => $translation) { + $prefixedTranslations['_' . $lang] = $translation; + } + + return new CreateTranslationResult(data: [ + 'key' => $t->getKey(), + 'creationDate' => $t->getCreationDate(), + 'modificationDate' => $t->getModificationDate(), + 'type' => $t->getType(), + ...$prefixedTranslations, + ]); + } +} diff --git a/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php b/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php new file mode 100644 index 00000000..61a4c59b --- /dev/null +++ b/src/Handler/Translation/CreateTranslation/CreateTranslationResult.php @@ -0,0 +1,30 @@ + $data + */ + public function __construct( + public array $data, + ) {} +} diff --git a/src/Handler/Translation/DeleteTranslation/DeleteTranslationHandler.php b/src/Handler/Translation/DeleteTranslation/DeleteTranslationHandler.php new file mode 100644 index 00000000..9317c06e --- /dev/null +++ b/src/Handler/Translation/DeleteTranslation/DeleteTranslationHandler.php @@ -0,0 +1,32 @@ +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..b127dc15 --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsHandler.php @@ -0,0 +1,154 @@ +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); + $list->setOrder('asc'); + $list->setOrderKey($tableName . '.key', false); + + $joins = []; + + $filterParameters = [ + 'filter' => $payload->filter, + 'searchString' => $payload->searchString, + ]; + + $conditions = $this->translationQueryService->getGridFilterCondition($filterParameters, $tableName, false, $allowedLanguages); + $filters = $this->translationQueryService->getGridFilterCondition($filterParameters, $tableName, true, $allowedLanguages); + + if ($filters) { + $joins = [...$joins, ...$filters['joins']]; + } + + if ($conditions !== []) { + $list->setCondition($conditions['condition'], $conditions['params']); + } + + $this->translationQueryService->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..b17d5cb4 --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsPayload.php @@ -0,0 +1,39 @@ +query->get('domain', Translation::DOMAIN_DEFAULT), + 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..fb7175b0 --- /dev/null +++ b/src/Handler/Translation/ExportTranslations/ExportTranslationsResult.php @@ -0,0 +1,28 @@ + ['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..175f19af --- /dev/null +++ b/src/Handler/Translation/GetTranslationDomains/GetTranslationDomainsResult.php @@ -0,0 +1,30 @@ + $domains + */ + public function __construct( + public array $domains, + ) {} +} diff --git a/src/Handler/Translation/GetTranslations/GetTranslationsHandler.php b/src/Handler/Translation/GetTranslations/GetTranslationsHandler.php new file mode 100644 index 00000000..dfd4940a --- /dev/null +++ b/src/Handler/Translation/GetTranslations/GetTranslationsHandler.php @@ -0,0 +1,122 @@ +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); + + $list->setLimit($payload->limit); + $list->setOffset($payload->offset ?? 0); + + $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']); + } + + $filterParameters = [ + 'filter' => $payload->filter, + 'searchString' => $payload->searchString, + ]; + + $conditions = $this->translationQueryService->getGridFilterCondition($filterParameters, $tableName, false, $validLanguages); + $filters = $this->translationQueryService->getGridFilterCondition($filterParameters, $tableName, true, $validLanguages); + + if ($filters) { + $joins = [...$joins, ...$filters['joins']]; + } + + if ($conditions !== []) { + $list->setCondition($conditions['condition'], $conditions['params']); + } + + $this->translationQueryService->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( + data: $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..21f86ffa --- /dev/null +++ b/src/Handler/Translation/GetTranslations/GetTranslationsResult.php @@ -0,0 +1,31 @@ +> $data + */ + public function __construct( + public array $data, + public int $total, + ) {} +} diff --git a/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesHandler.php b/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesHandler.php new file mode 100644 index 00000000..b65fe061 --- /dev/null +++ b/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesHandler.php @@ -0,0 +1,34 @@ +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..e21c993d --- /dev/null +++ b/src/Handler/Translation/GetWebsiteTranslationLanguages/GetWebsiteTranslationLanguagesResult.php @@ -0,0 +1,31 @@ +translationImportSession->getImportFile(); + $admin = $payload->domain === Translation::DOMAIN_ADMIN; + $allowedLanguages = $admin + ? Tool\Admin::getLanguages() + : $this->userContext->getAdminUser()->getAllowedLanguagesForEditingWebsiteTranslations(); + $delta = Translation::importTranslationsFromFile( + $tmpFile, + $payload->domain, + $payload->overwrite, + $allowedLanguages, + $payload->dialect + ); + + if (is_file($tmpFile)) { + @unlink($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: base64_encode(json_encode($enrichedDelta) ?: '')); + } + + return new ImportTranslationsResult(); + } +} diff --git a/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php b/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php new file mode 100644 index 00000000..2d4969a4 --- /dev/null +++ b/src/Handler/Translation/ImportTranslations/ImportTranslationsPayload.php @@ -0,0 +1,48 @@ +request->get('domain', Translation::DOMAIN_DEFAULT); + $merge = $request->query->get('merge'); + $dialect = $request->request->get('csvSettings'); + if ($dialect) { + $dialect = json_decode($dialect); + } + + return new static( + domain: $domain, + 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..6b2c2ee3 --- /dev/null +++ b/src/Handler/Translation/ImportTranslations/ImportTranslationsResult.php @@ -0,0 +1,27 @@ +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..324be3ee --- /dev/null +++ b/src/Handler/Translation/MergeTranslationItems/MergeTranslationItemsPayload.php @@ -0,0 +1,39 @@ +request->get('data'), true); + + return new static( + dataList: is_array($dataList) ? $dataList : [], + 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..439032e7 --- /dev/null +++ b/src/Handler/Translation/TranslationPayload.php @@ -0,0 +1,61 @@ +request->getString('domain', Translation::DOMAIN_DEFAULT); + $hasData = $request->request->has('data'); + + if ($hasData) { + $data = json_decode($request->request->getString('data'), true); + + return new static( + domain: $domain, + hasData: true, + data: is_array($data) ? $data : [], + ); + } + + return new static( + domain: $domain, + hasData: false, + requestParams: [...$request->request->all(), ...$request->query->all()], + limit: (int) $request->request->getString('limit', '50'), + offset: (int) $request->request->getString('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/UpdateTranslation/UpdateTranslationHandler.php b/src/Handler/Translation/UpdateTranslation/UpdateTranslationHandler.php new file mode 100644 index 00000000..81db7c5f --- /dev/null +++ b/src/Handler/Translation/UpdateTranslation/UpdateTranslationHandler.php @@ -0,0 +1,66 @@ +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(); + + $prefixedTranslations = []; + foreach ($t->getTranslations() as $lang => $translation) { + $prefixedTranslations['_' . $lang] = $translation; + } + + return new UpdateTranslationResult(data: [ + 'key' => $t->getKey(), + 'creationDate' => $t->getCreationDate(), + 'modificationDate' => $t->getModificationDate(), + 'type' => $t->getType(), + ...$prefixedTranslations, + ]); + } +} diff --git a/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php b/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php new file mode 100644 index 00000000..10e48504 --- /dev/null +++ b/src/Handler/Translation/UpdateTranslation/UpdateTranslationResult.php @@ -0,0 +1,30 @@ + $data + */ + public function __construct( + public array $data, + ) {} +} diff --git a/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileHandler.php b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileHandler.php new file mode 100644 index 00000000..fed428fb --- /dev/null +++ b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileHandler.php @@ -0,0 +1,56 @@ +file === null) { + throw new BadRequestHttpException('No file uploaded.'); + } + + $tmpData = file_get_contents($payload->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); + } + + $this->translationImportSession->storeImportFile($importFile); + + return new UploadTranslationImportFileResult( + config: ['csvSettings' => $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..a7f0529b --- /dev/null +++ b/src/Handler/Translation/UploadTranslationImportFile/UploadTranslationImportFileResult.php @@ -0,0 +1,30 @@ +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(); + } + } + } catch (\Exception $e) { + throw new AdminOperationFailedException($e->getMessage()); + } + + 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..5fb77769 --- /dev/null +++ b/src/Handler/User/AddUser/AddUserPayload.php @@ -0,0 +1,47 @@ +request->get('name', ''); + $referenceId = $request->request->has('rid') ? (int) $request->request->get('rid') : null; + $type = $request->request->get('type'); + + return new static( + type: $type !== null ? (string) $type : null, + 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..db982979 --- /dev/null +++ b/src/Handler/User/AddUser/AddUserResult.php @@ -0,0 +1,27 @@ +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..3d4a0113 --- /dev/null +++ b/src/Handler/User/DeleteUserImage/DeleteUserImageHandler.php @@ -0,0 +1,57 @@ +userContext->getAdminUser(); + $currentUserId = $adminUser?->getId(); + + // matches the old getUserId() helper: a differing target id requires the 'users' permission + if ($payload->targetUserId !== null && $payload->targetUserId !== $currentUserId && !$adminUser?->isAllowed('users')) { + throw new AccessDeniedHttpException(); + } + + $targetUserId = $payload->targetUserId ?? (int) $currentUserId; + + $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..ee71b037 --- /dev/null +++ b/src/Handler/User/Disable2Fa/Disable2FaHandler.php @@ -0,0 +1,43 @@ +userContext->getAdminUser(); + if (!$user instanceof User) { + throw new BadRequestHttpException('No user found'); + } + + if ($user->getTwoFactorAuthentication('required')) { + throw new AdminOperationFailedException('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'] = $this->passwordResetSession->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..98c3629d --- /dev/null +++ b/src/Handler/User/GetCurrentUser/GetCurrentUserPayload.php @@ -0,0 +1,28 @@ + $userData + */ + public function __construct( + public array $userData, + ) {} +} diff --git a/src/Handler/User/GetDefaultKeyBindings/GetDefaultKeyBindingsHandler.php b/src/Handler/User/GetDefaultKeyBindings/GetDefaultKeyBindingsHandler.php new file mode 100644 index 00000000..f6c4d169 --- /dev/null +++ b/src/Handler/User/GetDefaultKeyBindings/GetDefaultKeyBindingsHandler.php @@ -0,0 +1,28 @@ +id); + if (!$user) { + throw new NotFoundHttpException('User not found'); + } + + return new GetMinimalUserResult( + id: (int) $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..523c438c --- /dev/null +++ b/src/Handler/User/GetMinimalUser/GetMinimalUserResult.php @@ -0,0 +1,33 @@ +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..d016f37e --- /dev/null +++ b/src/Handler/User/GetRole/GetRoleResult.php @@ -0,0 +1,33 @@ +setCondition('parentId = ?', $payload->node); + $list->load(); + + $roles = []; + foreach ($list->getItems() as $role) { + $roles[] = $this->buildRoleTreeNodeConfig($role); + } + + return new GetRoleTreeChildrenResult(roles: $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/GetRoleTreeChildren/GetRoleTreeChildrenResult.php b/src/Handler/User/GetRoleTreeChildren/GetRoleTreeChildrenResult.php new file mode 100644 index 00000000..b97bc6fe --- /dev/null +++ b/src/Handler/User/GetRoleTreeChildren/GetRoleTreeChildrenResult.php @@ -0,0 +1,30 @@ +> $roles + */ + public function __construct( + public array $roles, + ) {} +} diff --git a/src/Handler/User/GetRoles/GetRolesHandler.php b/src/Handler/User/GetRoles/GetRolesHandler.php new file mode 100644 index 00000000..da52ac86 --- /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 new GetRolesResult(data: $roles, total: count($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/GetRoles/GetRolesResult.php b/src/Handler/User/GetRoles/GetRolesResult.php new file mode 100644 index 00000000..2dad320e --- /dev/null +++ b/src/Handler/User/GetRoles/GetRolesResult.php @@ -0,0 +1,31 @@ + $data + */ + public function __construct( + public array $data, + public int $total, + ) {} +} diff --git a/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkHandler.php b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkHandler.php new file mode 100644 index 00000000..b548e4d2 --- /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..ef651d16 --- /dev/null +++ b/src/Handler/User/GetTokenLoginLink/GetTokenLoginLinkResult.php @@ -0,0 +1,27 @@ +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((int) $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( + user: $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..427e3181 --- /dev/null +++ b/src/Handler/User/GetUser/GetUserResult.php @@ -0,0 +1,39 @@ + $user + * @param array $roles + * @param array $objectDependencies + */ + public function __construct( + public array $user, + public array $roles, + public array $permissions, + public array $availablePermissions, + public array $availablePerspectives, + public array $validLanguages, + public array $validLocales, + public array $objectDependencies, + ) {} +} diff --git a/src/Handler/User/GetUserImage/GetUserImageHandler.php b/src/Handler/User/GetUserImage/GetUserImageHandler.php new file mode 100644 index 00000000..4a51d58f --- /dev/null +++ b/src/Handler/User/GetUserImage/GetUserImageHandler.php @@ -0,0 +1,49 @@ +userContext->getAdminUser(); + $currentUserId = $adminUser?->getId(); + + // matches the old getUserId() helper: a differing target id requires the 'users' permission + if ($payload->targetUserId !== null && $payload->targetUserId !== $currentUserId && !$adminUser?->isAllowed('users')) { + throw new AccessDeniedHttpException(); + } + + $targetUserId = $payload->targetUserId ?? (int) $currentUserId; + + $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..bb2163b8 --- /dev/null +++ b/src/Handler/User/GetUserImage/GetUserImageResult.php @@ -0,0 +1,30 @@ +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 new GetUserTreeChildrenResult(users: $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/GetUserTreeChildren/GetUserTreeChildrenResult.php b/src/Handler/User/GetUserTreeChildren/GetUserTreeChildrenResult.php new file mode 100644 index 00000000..88ba402d --- /dev/null +++ b/src/Handler/User/GetUserTreeChildren/GetUserTreeChildrenResult.php @@ -0,0 +1,30 @@ +> $users + */ + public function __construct( + public array $users, + ) {} +} diff --git a/src/Handler/User/GetUsers/GetUsersHandler.php b/src/Handler/User/GetUsers/GetUsersHandler.php new file mode 100644 index 00000000..5f18ac12 --- /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 new GetUsersResult(data: $users, total: count($users)); + } +} diff --git a/src/Handler/User/GetUsers/GetUsersPayload.php b/src/Handler/User/GetUsers/GetUsersPayload.php new file mode 100644 index 00000000..435c7867 --- /dev/null +++ b/src/Handler/User/GetUsers/GetUsersPayload.php @@ -0,0 +1,37 @@ +query->get('include_current_user'), + permission: $request->query->get('permission'), + ); + } +} diff --git a/src/Handler/User/GetUsers/GetUsersResult.php b/src/Handler/User/GetUsers/GetUsersResult.php new file mode 100644 index 00000000..096ae138 --- /dev/null +++ b/src/Handler/User/GetUsers/GetUsersResult.php @@ -0,0 +1,31 @@ + $data + */ + public function __construct( + public array $data, + public int $total, + ) {} +} 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..cfc0f10e --- /dev/null +++ b/src/Handler/User/ResetMy2FaSecret/ResetMy2FaSecretHandler.php @@ -0,0 +1,33 @@ +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..230c4ffa --- /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 new SearchUsersResult(users: $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/SearchUsers/SearchUsersResult.php b/src/Handler/User/SearchUsers/SearchUsersResult.php new file mode 100644 index 00000000..c9ae44fa --- /dev/null +++ b/src/Handler/User/SearchUsers/SearchUsersResult.php @@ -0,0 +1,30 @@ + $users + */ + public function __construct( + public array $users, + ) {} +} diff --git a/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php b/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php new file mode 100644 index 00000000..2d22d05e --- /dev/null +++ b/src/Handler/User/SendInvitationLink/SendInvitationLinkHandler.php @@ -0,0 +1,98 @@ +username) { + throw new AdminOperationFailedException(); + } + + $user = User::getByName($payload->username); + if (!$user instanceof User) { + throw new AdminOperationFailedException('User unknown
'); + } + + $message = ''; + + if (!$user->getActive()) { + $message .= 'User is not active
'; + } + + if (!$user->getEmail()) { + $message .= 'User has no email address
'; + } + + if (!empty($message)) { + throw new AdminOperationFailedException($message); + } + + $domain = $this->generalHostResolver->resolve(['source' => $this->requestStack->getCurrentRequest()]) ?? ''; + + if (!$domain) { + throw new AdminOperationFailedException('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(); + } catch (\Symfony\Component\HttpKernel\Exception\BadRequestHttpException $e) { + throw new AdminOperationFailedException($e->getMessage() . '
'); + } catch (Exception) { + throw new AdminOperationFailedException('could not send email'); + } + + return new SendInvitationLinkResult( + message: sprintf($this->translator->trans('invitation_link_sent', [], 'admin_ext'), $user->getEmail()), + ); + } +} 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..4373096b --- /dev/null +++ b/src/Handler/User/SendInvitationLink/SendInvitationLinkResult.php @@ -0,0 +1,27 @@ +userContext->getAdminUser(); + $values = $payload->values; + unset($values['name'], $values['id'], $values['admin'], $values['permissions'], $values['roles'], $values['active']); + + if (!empty($values['new_password'])) { + $oldPasswordCheck = false; + + if ($this->passwordResetSession->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 AdminOperationFailedException('incorrect_password'); + } + + throw new AdminOperationFailedException('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..df72fc1e --- /dev/null +++ b/src/Handler/User/UpdateCurrentUser/UpdateCurrentUserPayload.php @@ -0,0 +1,48 @@ +request->get('data'); + $decodedValues = json_decode($data !== null ? (string) $data : '', true); + $keyBindingsJson = $request->request->has('keyBindings') ? $request->request->get('keyBindings') : null; + + return new static( + requestedUserId: (int) $request->request->get('id'), + values: is_array($decodedValues) ? $decodedValues : [], + keyBindingsJson: $keyBindingsJson !== null ? (string) $keyBindingsJson : null, + ); + } + + public function getOwnUserId(): int + { + return $this->requestedUserId; + } +} diff --git a/src/Handler/User/UpdateUser/UpdateUserHandler.php b/src/Handler/User/UpdateUser/UpdateUserHandler.php new file mode 100644 index 00000000..18dfa64c --- /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..eb37e167 --- /dev/null +++ b/src/Handler/User/UpdateUser/UpdateUserPayload.php @@ -0,0 +1,51 @@ +request->has('data') + ? json_decode((string) $request->request->get('data'), true) + : null; + $decodedWorkspaces = $request->request->has('workspaces') + ? json_decode((string) $request->request->get('workspaces'), true) + : null; + $keyBindingsJson = $request->request->has('keyBindings') + ? $request->request->get('keyBindings') + : null; + + return new static( + id: $request->request->getInt('id'), + values: is_array($decodedValues) ? $decodedValues : null, + workspaces: is_array($decodedWorkspaces) ? $decodedWorkspaces : null, + keyBindingsJson: $keyBindingsJson !== null ? (string) $keyBindingsJson : null, + ); + } +} diff --git a/src/Handler/User/UploadUserImage/UploadUserImageHandler.php b/src/Handler/User/UploadUserImage/UploadUserImageHandler.php new file mode 100644 index 00000000..a0835071 --- /dev/null +++ b/src/Handler/User/UploadUserImage/UploadUserImageHandler.php @@ -0,0 +1,62 @@ +userContext->getAdminUser(); + $currentUserId = $adminUser?->getId(); + + // matches the old getUserId() helper: a differing target id requires the 'users' permission + if ($payload->targetUserId !== null && $payload->targetUserId !== $currentUserId && !$adminUser?->isAllowed('users')) { + throw new AccessDeniedHttpException(); + } + + $targetUserId = $payload->targetUserId ?? (int) $currentUserId; + + $userObj = User::getById($targetUserId); + if (!$userObj) { + throw new NotFoundHttpException('User not found'); + } + + if ($userObj->isAdmin() && !$adminUser?->isAdmin()) { + throw new AccessDeniedHttpException('Only admin users are allowed to modify admin users'); + } + + $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..a939eb4a --- /dev/null +++ b/src/Handler/User/UploadUserImage/UploadUserImagePayload.php @@ -0,0 +1,43 @@ +query->has('id') ? $request->query->getInt('id') : null, + avatarFile: $request->files->get('Filedata'), + ); + } + + public function getOwnUserId(): int + { + return $this->targetUserId ?? 0; + } +} diff --git a/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlHandler.php b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlHandler.php new file mode 100644 index 00000000..dbc580f2 --- /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 AdminOperationFailedException('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..f8ec0ccf --- /dev/null +++ b/src/Handler/Workflow/GetModalCustomHtml/GetModalCustomHtmlResult.php @@ -0,0 +1,27 @@ +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, total: count($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..fb16e5b9 --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowDetails/GetWorkflowDetailsResult.php @@ -0,0 +1,28 @@ +elementResolver->resolve($payload->ctype, $payload->cid); + + try { + $workflow = $this->workflowManager->getWorkflowIfExists($element, $payload->workflowName); + + if (empty($workflow)) { + return new GetWorkflowFormResult( + message: 'workflow not found', + notes_enabled: false, + notes_required: false, + additional_fields: [], + ); + } + + $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), + notes_enabled: false, + notes_required: false, + additional_fields: [], + ); + } + + return new GetWorkflowFormResult( + message: '', + notes_enabled: false, + notes_required: $transition->getNotesCommentRequired(), + additional_fields: [], + ); + } catch (\Exception $e) { + return new GetWorkflowFormResult( + message: $e->getMessage(), + notes_enabled: false, + notes_required: false, + additional_fields: [], + ); + } + } +} 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..1d4e770d --- /dev/null +++ b/src/Handler/Workflow/GetWorkflowForm/GetWorkflowFormResult.php @@ -0,0 +1,30 @@ +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 new GetWorkflowSvgResult($process->getOutput()); + } +} diff --git a/src/Handler/Workflow/ShowGraph/GetWorkflowSvg/GetWorkflowSvgResult.php b/src/Handler/Workflow/ShowGraph/GetWorkflowSvg/GetWorkflowSvgResult.php new file mode 100644 index 00000000..c4560b18 --- /dev/null +++ b/src/Handler/Workflow/ShowGraph/GetWorkflowSvg/GetWorkflowSvgResult.php @@ -0,0 +1,27 @@ +query->getString('ctype'), + cid: $request->query->getInt('cid'), + workflowName: $request->query->has('workflow') ? $request->query->getString('workflow') : null, + ); + } +} diff --git a/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php new file mode 100644 index 00000000..c0ee77ff --- /dev/null +++ b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionHandler.php @@ -0,0 +1,59 @@ +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(); + + try { + $this->workflowManager->applyGlobalAction($workflow, $element, $payload->transition, $payload->workflowOptions, $saveSubject); + } catch (ValidationException $e) { + $reason = ''; + if (count($e->getSubItems()) > 0) { + $reason = '
    ' . implode('', array_map(static fn ($item) => '
  • ' . $item . '
  • ', $e->getSubItems())) . '
'; + } + + throw new AdminOperationFailedException($e->getMessage(), ['reasons' => [$reason]]); + } catch (Exception $e) { + throw new AdminOperationFailedException('error performing action on this element', ['reasons' => [$e->getMessage()]]); + } + + return new SubmitGlobalActionResult(); + } +} 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/SubmitGlobalAction/SubmitGlobalActionResult.php b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionResult.php new file mode 100644 index 00000000..e464dc99 --- /dev/null +++ b/src/Handler/Workflow/SubmitGlobalAction/SubmitGlobalActionResult.php @@ -0,0 +1,26 @@ +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) + ); + + throw new AdminOperationFailedException('transition failed', ['reasons' => $reasons]); + } + + try { + $this->workflowManager->applyWithAdditionalData($workflow, $element, $payload->transition, $payload->workflowOptions, true); + } catch (ValidationException $e) { + $reason = ''; + if (count($e->getSubItems()) > 0) { + $reason = '
    ' . implode('', array_map(static fn ($item) => '
  • ' . $item . '
  • ', $e->getSubItems())) . '
'; + } + + throw new AdminOperationFailedException($e->getMessage(), ['reasons' => [$reason]]); + } catch (Exception $e) { + throw new AdminOperationFailedException('error performing action on this element', ['reasons' => [$e->getMessage()]]); + } + + return new SubmitWorkflowTransitionResult(); + } +} 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..73e38c21 --- /dev/null +++ b/src/Handler/Workflow/SubmitWorkflowTransition/SubmitWorkflowTransitionResult.php @@ -0,0 +1,27 @@ +filesystem = new Filesystem(); - } - - public function getUser(): User - { - return $this->user; - } - - protected function getConfigDir(): string - { - return OPENDXP_CONFIGURATION_DIRECTORY.'/portal'; - } - - protected function getConfigFile(): string - { - return $this->getConfigDir().'/dashboards_'.$this->getUser()->getId().'.psf'; - } - - protected function loadFile(): array - { - if (!is_dir($this->getConfigDir())) { - $this->filesystem->mkdir($this->getConfigDir(), 0775); - } - - if (empty($this->dashboards)) { - if (is_file($this->getConfigFile())) { - - $dashboards = Serialize::unserialize( - file_get_contents($this->getConfigFile()), - ['allowed_classes' => false] - ); - - if (!empty($dashboards)) { - $this->dashboards = $dashboards; - } - } - - $perspectiveCfg = Config::getRuntimePerspective(); - $dashboardCfg = $perspectiveCfg['dashboards'] ?? []; - $dashboardsPerspective = $dashboardCfg['predefined'] ?? []; - - if (empty($this->dashboards)) { - $this->dashboards = $dashboardsPerspective; - } else { - foreach ($dashboardsPerspective as $key => $dashboard) { - if (!isset($this->dashboards[$key])) { - $this->dashboards[$key] = $dashboard; - } - } - } - } - - return $this->dashboards; - } - - public function getAllDashboards(): array - { - return $this->loadFile(); - } - - public function getDashboard(string $key = 'welcome'): array - { - $dashboards = $this->loadFile(); - $dashboard = $dashboards[$key] ?? null; - - if ($dashboard) { - $disabledPortlets = array_keys($this->getDisabledPortlets()); - $positions = $dashboard['positions']; - if (is_array($positions)) { - foreach ($positions as $columnKey => $column) { - if ($column) { - foreach ($column as $portletKey => $portletCfg) { - $type = $portletCfg['type']; - if (in_array($type, $disabledPortlets)) { - unset($dashboard['positions'][$columnKey][$portletKey]); - } - } - } - } - } - } - - return $dashboard ?: ['positions' => [[], []]]; - } - - public function saveDashboard(string $key, ?array $configuration = null): void - { - $this->loadFile(); - - if (empty($configuration)) { - $configuration = ['positions' => [[], []]]; - } - - $this->dashboards[$key] = $configuration; - $this->filesystem->dumpFile($this->getConfigFile(), Serialize::serialize($this->dashboards)); - } - - public function deleteDashboard(string $key): void - { - $this->loadFile(); - unset($this->dashboards[$key]); - $this->filesystem->dumpFile($this->getConfigFile(), Serialize::serialize($this->dashboards)); - } - - public function getDisabledPortlets(): array - { - $perspectiveCfg = Config::getRuntimePerspective($this->user); - $dashboardCfg = $perspectiveCfg['dashboards'] ?? []; - - return $dashboardCfg['disabledPortlets'] ?? []; - } -} diff --git a/src/Helper/DataObjectVersionHelper.php b/src/Helper/DataObjectVersionHelper.php new file mode 100644 index 00000000..89c1daef --- /dev/null +++ b/src/Helper/DataObjectVersionHelper.php @@ -0,0 +1,39 @@ +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/Helper/GridHelperService.php b/src/Helper/GridHelperService.php index 430dc435..0e7f7fdb 100644 --- a/src/Helper/GridHelperService.php +++ b/src/Helper/GridHelperService.php @@ -322,9 +322,14 @@ public function getFilterCondition(string $filterJson, ClassDefinition $class, ? if (is_array($filter['value'])) { $fieldConditions = []; foreach ($filter['value'] as $filterValue) { - $brickCondition = '(' . $brickField->getFilterCondition($filterValue, $operator, + + $brickFilterCondition = $brickField->getFilterCondition( + $filterValue, + $operator, ['brickPrefix' => $brickPrefix] - ) . ' AND ' . $brickType . '.fieldname = ' . $db->quote($brickFilterField) . ')'; + ); + + $brickCondition = '(' . $brickFilterCondition . ' AND ' . $brickType . '.fieldname = ' . $db->quote($brickFilterField) . ')'; $fieldConditions[] = $brickCondition; } @@ -332,8 +337,14 @@ public function getFilterCondition(string $filterJson, ClassDefinition $class, ? $conditionPartsFilters[] = '(' . implode(' OR ', $fieldConditions) . ')'; } } else { - $brickCondition = '(' . $brickField->getFilterCondition($filter['value'], $operator, - ['brickPrefix' => $brickPrefix]) . ' AND ' . $brickType . '.fieldname = ' . $db->quote($brickFilterField) . ')'; + + $brickFilterCondition = $brickField->getFilterCondition( + $filter['value'], + $operator, + ['brickPrefix' => $brickPrefix] + ); + + $brickCondition = '(' . $brickFilterCondition . ' AND ' . $brickType . '.fieldname = ' . $db->quote($brickFilterField) . ')'; $conditionPartsFilters[] = $brickCondition; } } elseif ($field instanceof ClassDefinition\Data\UrlSlug) { @@ -343,14 +354,28 @@ public function getFilterCondition(string $filterJson, ClassDefinition $class, ? if (is_array($filter['value'] ?? false)) { $fieldConditions = []; foreach ($filter['value'] as $filterValue) { - $fieldConditions[] = $field->getFilterCondition($filterValue, $operator, ['brickPrefix' => ($tablePrefix ? $tablePrefix . '.' : null)]); + $fieldConditions[] = $field->getFilterCondition( + $filterValue, + $operator, + [ + 'brickPrefix' => ($tablePrefix ? $tablePrefix . '.' : null), + 'classId' => $class->getId() + ] + ); } if ($fieldConditions !== []) { $conditionPartsFilters[] = '(' . implode(' OR ', $fieldConditions) . ')'; } } else { - $conditionPartsFilters[] = $field->getFilterCondition($filter['value'] ?? null, $operator, ['brickPrefix' => ($tablePrefix ? $tablePrefix . '.' : null)]); + $conditionPartsFilters[] = $field->getFilterCondition( + $filter['value'] ?? null, + $operator, + [ + 'brickPrefix' => ($tablePrefix ? $tablePrefix . '.' : null), + 'classId' => $class->getId() + ] + ); } } elseif (in_array($filterField, $systemFields)) { // system field diff --git a/src/Helper/QueryParams.php b/src/Helper/QueryParams.php index e1117040..78790583 100644 --- a/src/Helper/QueryParams.php +++ b/src/Helper/QueryParams.php @@ -52,11 +52,21 @@ public static function extractSortingSettings(array $params): array $groupId = (int) $groupKeyId[0]; $keyid = (int) $groupKeyId[1]; - return ['orderKey' => $sortParam['property'], 'fieldname' => $fieldname, 'groupId' => $groupId, 'keyId' => $keyid, 'order' => $order, 'isFeature' => 1]; + return [ + 'orderKey' => $sortParam['property'], + 'fieldname' => $fieldname, + 'groupId' => $groupId, + 'keyId' => $keyid, + 'order' => $order, + 'isFeature' => 1 + ]; } } - return ['orderKey' => $orderKey, 'order' => $order]; + return [ + 'orderKey' => $orderKey, + 'order' => $order + ]; } public static function getRecordIdForGridRequest(string $param): int @@ -69,8 +79,6 @@ public static function getRecordIdForGridRequest(string $param): int /** * Creates a condition string from the passed ExtJs filter definitions * - * - * * @throws Exception */ public static function getFilterCondition(string $filterString, array $matchExact = ['id', 'id'], bool $returnString = true, array $callbacks = []): array|string 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/Mapper/Asset/AssetPayloadMapper.php b/src/Mapper/Asset/AssetPayloadMapper.php new file mode 100644 index 00000000..8fdc3dd4 --- /dev/null +++ b/src/Mapper/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) { + $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/Mapper/DataObject/ClassificationstoreKeyConfigMapper.php b/src/Mapper/DataObject/ClassificationstoreKeyConfigMapper.php new file mode 100644 index 00000000..e0829e20 --- /dev/null +++ b/src/Mapper/DataObject/ClassificationstoreKeyConfigMapper.php @@ -0,0 +1,56 @@ +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/Mapper/DataObject/DataObjectPayloadMapper.php b/src/Mapper/DataObject/DataObjectPayloadMapper.php new file mode 100644 index 00000000..1820eea8 --- /dev/null +++ b/src/Mapper/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/Mapper/Document/DocumentPayloadMapper.php b/src/Mapper/Document/DocumentPayloadMapper.php new file mode 100644 index 00000000..abe5f03a --- /dev/null +++ b/src/Mapper/Document/DocumentPayloadMapper.php @@ -0,0 +1,255 @@ +applyScheduler($payload->scheduler, $document); + + return; + } + + if ($payload->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 + { + $isTargetSpecific = interface_exists(TargetingDocumentInterface::class) + && $document instanceof TargetingDocumentInterface + && $document->hasTargetGroupSpecificEditables(); + + if ($appendEditables || $isTargetSpecific) { + $document->getEditables(); + } else { + // ensure no editables (e.g. from session, version, ...) are still referenced + $document->setEditables(null); + } + + if ($editables === null) { + return; + } + + 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/Mapper/GridData/Asset.php b/src/Mapper/GridData/Asset.php new file mode 100644 index 00000000..f48a2145 --- /dev/null +++ b/src/Mapper/GridData/Asset.php @@ -0,0 +1,118 @@ + $asset->getId(), + 'id~system' => $asset->getId(), + 'type~system' => $asset->getType(), + 'fullpath~system' => $asset->getRealFullPath(), + 'filename~system' => $asset->getKey(), + 'creationDate~system' => $asset->getCreationDate(), + 'modificationDate~system' => $asset->getModificationDate(), + 'idPath~system' => Service::getIdPath($asset), + ]; + + $requestedLanguage = str_replace('default', '', $requestedLanguage); + + foreach ($fields as $field) { + $fieldDef = explode('~', $field); + if (isset($fieldDef[1]) && $fieldDef[1] === 'system') { + if ($fieldDef[0] === 'preview') { + $data[$field] = self::getPreviewThumbnail($asset, ['treepreview' => true, 'width' => 108, 'height' => 70, 'frame' => true]); + } elseif ($fieldDef[0] === 'size') { + $size = $asset->getFileSize(); + $data[$field] = OpenDxp\Helper\FileSystemHelper::formatBytes($size); + } + } else { + if (isset($fieldDef[1])) { + $language = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); + $rawMetaData = $asset->getMetadata($fieldDef[0], $language, true, true); + } else { + $rawMetaData = $asset->getMetadata($field, $requestedLanguage, true, true); + } + + $metaData = $rawMetaData['data'] ?? null; + + if ($rawMetaData) { + $type = $rawMetaData['type']; + if (!$loader) { + $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); + } + + $metaData = $rawMetaData['data'] ?? null; + + try { + /** @var Data $instance */ + $instance = $loader->build($type); + $metaData = $instance->getDataForListfolderGrid($rawMetaData['data'] ?? null, $rawMetaData); + } catch (UnsupportedException) { + } + } + + $data[$field] = $metaData; + } + } + } + + return $data; + } + + public static function getPreviewThumbnail(Model\Asset $asset, array $params = [], bool $onlyMethod = false): ?string + { + $thumbnailMethod = ''; + $thumbnailUrl = null; + + if ($asset instanceof Model\Asset\Image) { + $thumbnailMethod = 'getThumbnail'; + } elseif ($asset instanceof Model\Asset\Video && \OpenDxp\Video::isAvailable()) { + $thumbnailMethod = 'getImageThumbnail'; + } elseif ($asset instanceof Model\Asset\Document && \OpenDxp\Document::isAvailable()) { + $thumbnailMethod = 'getImageThumbnail'; + } + + if ($onlyMethod) { + return $thumbnailMethod; + } + + if (!empty($thumbnailMethod)) { + $thumbnailUrl = '/admin/asset/get-' . $asset->getType() . '-thumbnail?id=' . $asset->getId(); + if (count($params) > 0) { + $thumbnailUrl .= '&' . http_build_query($params); + } + } + + return $thumbnailUrl; + } +} diff --git a/src/Mapper/GridData/DataObject.php b/src/Mapper/GridData/DataObject.php new file mode 100644 index 00000000..638690f3 --- /dev/null +++ b/src/Mapper/GridData/DataObject.php @@ -0,0 +1,390 @@ + $object, + 'purpose' => 'gridview', + 'language' => $requestedLanguage, ]; + $data['classname'] = $object->getClassName(); + $data['idPath'] = Service::getIdPath($object); + $data['inheritedFields'] = []; + $data['permissions'] = $object->getUserPermissions($user); + $data['locked'] = $object->isLocked(); + + if (is_null($fields)) { + $fields = array_keys($object->getclass()->getFieldDefinitions()); + } + + $helperDefinitions = $params['helperDefinitions'] ?? []; + + foreach ($fields as $key) { + $brickDescriptor = null; + $brickKey = null; + $brickType = null; + $brickGetter = null; + $dataKey = $key; + $keyParts = explode('~', $key); + + $def = $object->getClass()->getFieldDefinition($key, $context); + + if (str_starts_with($key, '#')) { + if (!empty($helperDefinitions[$key])) { + $context['fieldname'] = $key; + $data[$key] = \OpenDxp\Model\DataObject\Service::calculateCellValue($object, $helperDefinitions, $key, $context); + } + } elseif (str_starts_with($key, '~')) { + $type = $keyParts[1]; + if ($type === 'classificationstore') { + $data[$key] = self::getStoreValueForObject($object, $key, $requestedLanguage); + } + } elseif (count($keyParts) > 1) { + // brick + $brickType = $keyParts[0]; + if (str_contains($brickType, '?')) { + $brickDescriptor = substr($brickType, 1); + $brickDescriptor = json_decode($brickDescriptor, true); + $brickType = $brickDescriptor['containerKey']; + } + + $brickKey = $keyParts[1]; + + $key = Service::getFieldForBrickType($object->getclass(), $brickType); + + $brickClass = Objectbrick\Definition::getByKey($brickType); + $context['outerFieldname'] = $key; + + if ($brickDescriptor) { + $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; + /** @var Model\DataObject\ClassDefinition\Data\Localizedfields $localizedFields */ + $localizedFields = $brickClass->getFieldDefinition($innerContainer); + $def = $localizedFields->getFieldDefinition($brickDescriptor['brickfield']); + } elseif ($brickClass instanceof Objectbrick\Definition) { + $def = $brickClass->getFieldDefinition($brickKey, $context); + } + } + + if (!empty($key)) { + // some of the not editable field require a special response + $getter = 'get' . ucfirst($key); + $needLocalizedPermissions = false; + + // if the definition is not set try to get the definition from localized fields + if (!$def) { + /** @var Model\DataObject\ClassDefinition\Data\Localizedfields|null $locFields */ + $locFields = $object->getClass()->getFieldDefinition('localizedfields'); + if ($locFields) { + $def = $locFields->getFieldDefinition($key, $context); + if ($def) { + $needLocalizedPermissions = true; + } + } + } + + //relation type fields with remote owner do not have a getter + if (method_exists($object, $getter)) { + //system columns must not be inherited + if (in_array($key, Concrete::SYSTEM_COLUMN_NAMES)) { + $data[$dataKey] = $object->$getter(); + } else { + $valueObject = self::getValueForObject($object, $key, $brickType, $brickKey, $def, $context, $brickDescriptor, $requestedLanguage); + $data['inheritedFields'][$dataKey] = ['inherited' => $valueObject->objectid != $object->getId(), 'objectid' => $valueObject->objectid]; + + if ($csvMode || method_exists($def, 'getDataForGrid')) { + if ($brickKey) { + $context['containerType'] = 'objectbrick'; + $context['containerKey'] = $brickType; + $context['outerFieldname'] = $key; + } + + $params = [...$params, 'context' => $context]; + if (!isset($params['purpose'])) { + $params['purpose'] = 'gridview'; + } + + if ($csvMode) { + $getterParams = ['language' => $requestedLanguage]; + $tempData = $def->getForCsvExport($object, $getterParams); + } elseif (method_exists($def, 'getDataForGrid')) { + $tempData = $def->getDataForGrid($valueObject->value, $object, $params); + } else { + continue; + } + + if ($def instanceof ClassDefinition\Data\Localizedfields) { + $needLocalizedPermissions = true; + foreach ($tempData as $tempKey => $tempValue) { + $data[$tempKey] = $tempValue; + } + } else { + $data[$dataKey] = $tempData; + if ( + $def instanceof Model\DataObject\ClassDefinition\Data\Select + && !$def->useConfiguredOptions() + && $def->getOptionsProviderClass() + ) { + $data[$dataKey . '%options'] = $def->getOptions(); + } + + // to prevent malforded grids in case of empty fieldcollections + if ($def instanceof ClassDefinition\Data\Fieldcollections) { + $data[$dataKey] ??= ''; + } + } + } else { + $data[$dataKey] = $valueObject->value; + } + } + } + + // because the key for the classification store has not a direct getter, you have to check separately if the data is inheritable + if ( + str_starts_with($key, '~') && + ($keyParts[1] ?? null) === 'classificationstore' + ) { + $fieldDef = self::getClassificationStoreFieldDefinition($key); + $value = self::normalizeValue($data[$key]); + + if ($fieldDef->isEmpty($value)) { + $inheritedData = static::getInheritedData($object, $key, $requestedLanguage); + + if ($inheritedData !== []) { + $parent = $inheritedData['parent']; + $data[$dataKey] = $inheritedData['value']; + $data['inheritedFields'][$dataKey] = [ + 'inherited' => $parent->getId() !== $object->getId(), + 'objectid' => $parent->getId(), + ]; + } + } + } + + if ($needLocalizedPermissions && !$user->isAdmin()) { + $locale = OpenDxp::getContainer()->get(LocaleServiceInterface::class)->findLocale(); + $permissionTypes = ['View', 'Edit']; + foreach ($permissionTypes as $permissionType) { + //TODO, this needs refactoring! Ideally, call it only once! + $languagesAllowed = Service::getLanguagePermissions($object, $user, 'l' . $permissionType); + + if ($languagesAllowed) { + $languagesAllowed = array_keys($languagesAllowed); + + if (!in_array($locale, $languagesAllowed)) { + $data['metadata']['permission'][$key]['no' . $permissionType] = 1; + if ($permissionType === 'View') { + $data[$key] = null; + } + } + } + } + } + } + } + } + + return $data; + } + + /** + * gets value for given object and getter, including inherited values + * + * @return stdClass value and objectid where the value comes from + */ + private static function getValueForObject(Concrete $object, string $key, ?string $brickType = null, ?string $brickKey = null, ?ClassDefinition\Data $fieldDefinition = null, array $context = [], ?array $brickDescriptor = null, ?string $requestedLanguage = null): stdClass + { + $getter = 'get' . ucfirst($key); + $value = null; + + try { + $value = $object->$getter($requestedLanguage ?? AdminTool::getCurrentUser()?->getLanguage()); + } catch (Throwable) { + } + + if (empty($value)) { + $value = $object->$getter(); + } + if (!empty($value) && !empty($brickType)) { + $getBrickType = 'get' . ucfirst($brickType); + $value = $value->$getBrickType(); + if (!empty($value) && !empty($brickKey)) { + if ($brickDescriptor) { + $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; + $localizedFields = $value->{'get' . ucfirst($innerContainer)}(); + $brickDefinition = Model\DataObject\Objectbrick\Definition::getByKey($brickType); + /** @var Model\DataObject\ClassDefinition\Data\Localizedfields $fieldDefinitionLocalizedFields */ + $fieldDefinitionLocalizedFields = $brickDefinition->getFieldDefinition('localizedfields'); + $fieldDefinition = $fieldDefinitionLocalizedFields->getFieldDefinition($brickKey); + $value = $localizedFields->getLocalizedValue($brickDescriptor['brickfield']); + } else { + $brickFieldGetter = 'get' . ucfirst($brickKey); + $value = $value->$brickFieldGetter(); + } + } + } + + if (!$fieldDefinition) { + $fieldDefinition = $object->getClass()->getFieldDefinition($key, $context); + } + + if (!empty($brickType) && !empty($brickKey) && !$brickDescriptor) { + $brickClass = Objectbrick\Definition::getByKey($brickType); + $context = ['object' => $object, 'outerFieldname' => $key]; + $fieldDefinition = $brickClass->getFieldDefinition($brickKey, $context); + } + + if ($fieldDefinition->isEmpty($value) && $fieldDefinition->supportsInheritance()) { + $parent = Service::hasInheritableParentObject($object); + if (!empty($parent)) { + return self::getValueForObject($parent, $key, $brickType, $brickKey, $fieldDefinition, $context, $brickDescriptor); + } + } + + $result = new stdClass(); + $result->value = $value; + $result->objectid = $object->getId(); + + return $result; + } + + /** + * gets store value for given object and key + */ + private static function getStoreValueForObject(Concrete $object, string $key, ?string $requestedLanguage): mixed + { + $keyParts = explode('~', $key); + + if (str_starts_with($key, '~')) { + $type = $keyParts[1]; + if ($type === 'classificationstore') { + $field = $keyParts[2]; + $groupKeyId = explode('-', $keyParts[3]); + + $groupId = (int) $groupKeyId[0]; + $keyid = (int) $groupKeyId[1]; + $getter = 'get' . ucfirst($field); + + if (method_exists($object, $getter)) { + /** @var Classificationstore $classificationStoreData */ + $classificationStoreData = $object->$getter(); + + /** @var Model\DataObject\ClassDefinition\Data\Classificationstore $csFieldDefinition */ + $csFieldDefinition = $object->getClass()->getFieldDefinition($field); + $csLanguage = $requestedLanguage; + + if (!$csFieldDefinition->isLocalized()) { + $csLanguage = 'default'; + } + + $fielddata = $classificationStoreData->getLocalizedKeyValue($groupId, $keyid, $csLanguage, true, true); + + $keyConfig = Model\DataObject\Classificationstore\KeyConfig::getById($keyid); + $type = $keyConfig->getType(); + $definition = json_decode($keyConfig->getDefinition(), true); + $definition = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); + + if (method_exists($definition, 'getDataForGrid')) { + return $definition->getDataForGrid($fielddata, $object); + } + + return $fielddata; + } + } + } + + return null; + } + + protected static function getInheritedData(Concrete $object, string $key, string $requestedLanguage): array + { + if (!$parent = Service::hasInheritableParentObject($object)) { + return []; + } + + $inheritedValue = self::normalizeValue(self::getStoreValueForObject($parent, $key, $requestedLanguage)); + + if (!static::getClassificationStoreFieldDefinition($key)->isEmpty($inheritedValue)) { + return [ + 'parent' => $parent, + 'value' => $inheritedValue, + ]; + } + + return self::getInheritedData($parent, $key, $requestedLanguage); + } + + /** + * The actual data could be a plain array for tables, + * an associative array for RGB or Quantity Value where the value is a key. + */ + private static function normalizeValue(mixed $data): mixed + { + if (is_array($data)) { + if (array_is_list($data)) { + return $data; + } + if (array_key_exists('value', $data)) { + return $data['value']; + } + } + + return $data; + } + + protected static function getClassificationStoreFieldDefinition(string $key): Data + { + $keyParts = explode('~', $key); + $groupKeyId = explode('-', $keyParts[3]); + + $keyid = (int) $groupKeyId[1]; + + $keyConfig = Model\DataObject\Classificationstore\KeyConfig::getById($keyid); + $type = $keyConfig->getType(); + $definition = json_decode($keyConfig->getDefinition(), true); + + return \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); + } +} diff --git a/src/Mapper/GridData/Document.php b/src/Mapper/GridData/Document.php new file mode 100644 index 00000000..c0cdd809 --- /dev/null +++ b/src/Mapper/GridData/Document.php @@ -0,0 +1,41 @@ +getTitle(); + $data['description'] = $document->getDescription(); + } else { + $data['title'] = ''; + $data['description'] = ''; + $data['name'] = ''; + } + + return $data; + } +} diff --git a/src/Mapper/GridData/Element.php b/src/Mapper/GridData/Element.php new file mode 100644 index 00000000..f9cb4c33 --- /dev/null +++ b/src/Mapper/GridData/Element.php @@ -0,0 +1,43 @@ + $element->getId(), + 'fullpath' => $element->getRealFullPath(), + 'type' => Service::getElementType($element), + 'subtype' => $element->getType(), + 'filename' => $element->getKey(), + 'creationDate' => $element->getCreationDate(), + 'modificationDate' => $element->getModificationDate(), + ]; + + $data['published'] = method_exists($element, 'isPublished') ? $element->isPublished() : true; + + return $data; + } +} 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 @@ +getConfigFile($user))) { + return []; + } + + return Serialize::unserialize( + file_get_contents($this->getConfigFile($user)), + ['allowed_classes' => false] + ) ?: []; + } + + public function save(User $user, array $dashboards): void + { + $filesystem = new Filesystem(); + if (!is_dir($this->getConfigDir())) { + $filesystem->mkdir($this->getConfigDir(), 0775); + } + + $filesystem->dumpFile($this->getConfigFile($user), Serialize::serialize($dashboards)); + } + + private function getConfigDir(): string + { + return OPENDXP_CONFIGURATION_DIRECTORY . '/portal'; + } + + private function getConfigFile(User $user): string + { + return $this->getConfigDir() . '/dashboards_' . $user->getId() . '.psf'; + } +} diff --git a/src/Resolver/Asset/ImageThumbnailResolver.php b/src/Resolver/Asset/ImageThumbnailResolver.php new file mode 100644 index 00000000..5df77b23 --- /dev/null +++ b/src/Resolver/Asset/ImageThumbnailResolver.php @@ -0,0 +1,99 @@ +id; + $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())); + + if ($origin !== 'folderPreview') { + throw new NotFoundHttpException(sprintf('Tree preview thumbnail not available for asset %d', $id)); + } + + return new ImageThumbnailResolutionDto($image, null); + } + } + + if ($hasCropPercent) { + $thumbnailConfig->addItemAt(0, 'cropPercent', [ + 'width' => $cropWidth, + 'height' => $cropHeight, + 'y' => $cropTop, + 'x' => $cropLeft, + ]); + + $thumbnailConfig->generateAutoName(); + } + + return new ImageThumbnailResolutionDto($image, $image->getThumbnail($thumbnailConfig)); + } +} diff --git a/src/Resolver/Grid/AssetGridColumnConfigResolver.php b/src/Resolver/Grid/AssetGridColumnConfigResolver.php new file mode 100644 index 00000000..7d0fd237 --- /dev/null +++ b/src/Resolver/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 AssetGridColumnConfig( + 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/Resolver/Grid/DataObjectGridColumnConfigResolver.php b/src/Resolver/Grid/DataObjectGridColumnConfigResolver.php new file mode 100644 index 00000000..e5cb39e7 --- /dev/null +++ b/src/Resolver/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 = $this->getCalculatedColumnConfig($sc); + 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($user, $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 DataObjectGridColumnConfig( + 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(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 = $this->gridColumnConfigSession->getHelperColumns(); + + 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]; + $this->gridColumnConfigSession->setHelperColumns($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/Resolver/Translation/AdminSearchTermResolver.php b/src/Resolver/Translation/AdminSearchTermResolver.php new file mode 100644 index 00000000..cb83c02a --- /dev/null +++ b/src/Resolver/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/Resolver/Workflow/WorkflowElementResolver.php b/src/Resolver/Workflow/WorkflowElementResolver.php new file mode 100644 index 00000000..30eaba32 --- /dev/null +++ b/src/Resolver/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/src/Security/Permission/AdminPermission.php b/src/Security/Permission/AdminPermission.php new file mode 100644 index 00000000..1f6d6179 --- /dev/null +++ b/src/Security/Permission/AdminPermission.php @@ -0,0 +1,25 @@ +tokenResolver->getUser(); + + if ($user === null || $subject->getOwnUserId() !== $user->getId()) { + Logger::warn('prevented save current user, because ids do not match. '); + + return false; + } + + return true; + } +} diff --git a/src/Security/Voter/PermissionVoter.php b/src/Security/Voter/PermissionVoter.php new file mode 100644 index 00000000..7aed7f1b --- /dev/null +++ b/src/Security/Voter/PermissionVoter.php @@ -0,0 +1,46 @@ +tokenResolver->getUser(); + if (!$user) { + return false; + } + + return $user->isAllowed($attribute); + } +} diff --git a/src/Service/Admin/AdminSettingsService.php b/src/Service/Admin/AdminSettingsService.php new file mode 100644 index 00000000..d5ff7764 --- /dev/null +++ b/src/Service/Admin/AdminSettingsService.php @@ -0,0 +1,223 @@ +config; + $systemSettings = SystemSettingsConfig::get(); + $adminSettings = AdminConfig::get(); + + $runtimePerspective = PerspectiveConfig::getRuntimePerspective($user); + + 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($this->sessionIdentity->getId(), ENT_QUOTES, 'UTF-8'), + + language: $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: (bool) $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: $this->dashboardService->getDisabledPortlets($user), + + 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()) ?? '', + ); + } + + 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/Service/Admin/AdminStatisticsService.php b/src/Service/Admin/AdminStatisticsService.php new file mode 100644 index 00000000..fae10e37 --- /dev/null +++ b/src/Service/Admin/AdminStatisticsService.php @@ -0,0 +1,61 @@ +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'; + } + } +} diff --git a/src/Service/Admin/AdminUserContext.php b/src/Service/Admin/AdminUserContext.php new file mode 100644 index 00000000..98ed92c3 --- /dev/null +++ b/src/Service/Admin/AdminUserContext.php @@ -0,0 +1,38 @@ +tokenResolver->getUser(); + } + + public function getAdminUserProxy(): ?UserProxy + { + return $this->tokenResolver->getUserProxy(); + } +} diff --git a/src/Service/Admin/AdminUserContextInterface.php b/src/Service/Admin/AdminUserContextInterface.php new file mode 100644 index 00000000..cffab546 --- /dev/null +++ b/src/Service/Admin/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('~', (string) $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 (array) $afterListLoadEvent->getArgument('list'); + } + + return ['success' => false]; + } +} diff --git a/src/Service/Asset/AssetUploadService.php b/src/Service/Asset/AssetUploadService.php new file mode 100644 index 00000000..0e706051 --- /dev/null +++ b/src/Service/Asset/AssetUploadService.php @@ -0,0 +1,228 @@ +config['assets']['default_upload_path'] ?? '/'; + + if ($payload->filedataPath !== null) { + $filename = $payload->filedataOriginalName; + $sourcePath = $payload->filedataPath; + } elseif ($payload->type === 'base64') { + $filename = $payload->filename ?? ''; + $sourcePath = OPENDXP_SYSTEM_TEMP_DIRECTORY . '/upload-base64' . uniqid('', false) . '.tmp'; + $data = preg_replace('@^data:[^,]+;base64,@', '', $payload->data ?? ''); + $filesystem = new Filesystem(); + $filesystem->dumpFile($sourcePath, base64_decode($data)); + } else { + throw new Exception('The filename of the asset is empty'); + } + + $parentId = $payload->parentId; + $parentPath = $payload->parentPath; + + if ($payload->hasDir && $payload->hasParentId) { + $parent = Asset::getById($payload->parentId); + $dir = $payload->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 (!$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 = $payload->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 (!$payload->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 = $payload->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 ($payload->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 $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/Controller/Admin/DataObject/DataObjectActionsTrait.php b/src/Service/DataObject/DataObjectGridService.php similarity index 74% rename from src/Controller/Admin/DataObject/DataObjectActionsTrait.php rename to src/Service/DataObject/DataObjectGridService.php index 37fc5956..ab4999e5 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) { @@ -94,7 +89,14 @@ protected function gridProxy( return [ 'success' => true, - 'data' => GridData\DataObject::getData($object, $allParams['fields'], $requestedLanguage), + 'data' => GridData\DataObject::getData( + $object, + $allParams['fields'], + $requestedLanguage, + [ + 'helperDefinitions' => $this->gridColumnConfigSession->getHelperColumns(), + ] + ), ]; } catch (Exception $e) { return [ @@ -103,14 +105,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'); } @@ -123,15 +125,37 @@ 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'], + $this->gridColumnConfigSession->getHelperColumns(), + $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; } + } else { - $o = GridData\DataObject::getData($object, $allParams['fields'] ?? null, $requestedLanguage, - ['csvMode' => $csvMode]); + + $o = GridData\DataObject::getData( + $object, + $allParams['fields'] ?? null, + $requestedLanguage, + [ + 'csvMode' => $csvMode, + 'helperDefinitions' => $this->gridColumnConfigSession->getHelperColumns() + ] + ); + if ($o['permissions']['list']) { $objects[] = $o; } @@ -145,12 +169,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 +189,6 @@ private function prepareObjectData( array $data, DataObject\Concrete $object, string $requestedLanguage, - LocaleServiceInterface $localeService ): array { $user = Tool\Admin::getCurrentUser(); $languagePermissions = []; @@ -271,7 +294,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 +315,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/Element/EditLockService.php b/src/Service/Element/EditLockService.php new file mode 100644 index 00000000..f7e96ba7 --- /dev/null +++ b/src/Service/Element/EditLockService.php @@ -0,0 +1,71 @@ +sessionIdentity->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/ElementDraftService.php b/src/Service/Element/ElementDraftService.php new file mode 100644 index 00000000..08f9db02 --- /dev/null +++ b/src/Service/Element/ElementDraftService.php @@ -0,0 +1,133 @@ +getDocument($document); + if ($sessionDocument !== null && $sessionDocument::class === $document::class) { + /** @var T $sessionDocument */ + return $sessionDocument; + } + + return DocumentVersionHelper::resolveLatestDraft($document, userId: $this->userContext->getAdminUser()?->getId()); + } + + public function saveDocument(Document $doc, bool $useForSave = false): void + { + $sessionId = $this->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($this->userContext->getAdminUser()?->getId()); + 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->sessionIdentity->getId(); + } +} diff --git a/src/Service/Element/ElementService.php b/src/Service/Element/ElementService.php new file mode 100644 index 00000000..2176e699 --- /dev/null +++ b/src/Service/Element/ElementService.php @@ -0,0 +1,385 @@ + $element->getId(), + 'key' => $element->getKey(), + 'text' => htmlspecialchars($element->getKey() ?? ''), + 'type' => $element->getType(), + 'path' => $element->getRealFullPath(), + 'basePath' => $element->getRealPath(), + 'locked' => $element->isLocked(), + 'lockOwner' => (bool)$element->getLocked(), + 'elementType' => Service::getElementType($element), + ]; + + if ($element instanceof Asset) { + $tmpNode['cls'] = ''; + $this->assignAssetTreeConfig($element, $tmpNode); + } elseif ($element instanceof Document) { + $tmpNode['idx'] = $element->getIndex(); + $tmpNode['published'] = $element->isPublished(); + $tmpNode['leaf'] = true; + + $this->assignDocumentTreeConfig($element, $tmpNode); + } elseif ($element instanceof DataObject) { + $tmpNode['idx'] = $element->getIndex(); + $tmpNode['sortBy'] = $element->getChildrenSortBy(); + $tmpNode['sortOrder'] = $element->getChildrenSortOrder(); + + $this->assignDataObjectTreeConfig($element, $tmpNode); + } + + $tmpNode['permissions'] = $this->mergeWorkflowPermissions($element, $tmpNode['permissions']); + + $this->addAdminStyle($element, ElementAdminStyleEvent::CONTEXT_TREE, $tmpNode); + + if ($element->isLocked()) { + $tmpNode['cls'] .= 'opendxp_treenode_locked '; + } + if ($element->getLocked()) { + $tmpNode['cls'] .= 'opendxp_treenode_lockOwner '; + } + + return $tmpNode; + } + + public function getThumbnailUrl(Asset $asset, array $params = []): ?string + { + $defaults = [ + 'id' => $asset->getId(), + 'treepreview' => true, + '_dc' => $asset->getModificationDate(), + ]; + + if ($asset instanceof Asset\Image && ($params['origin'] ?? null) === 'folderPreview') { + if (!$asset->getThumbnail(Asset\Image\Thumbnail\Config::getPreviewConfig())->exists()) { + $defaults['_dc'] = time(); + } + } + + $params = [...$defaults, ...$params]; + + switch ($asset) { + case $asset instanceof Asset\Image: + $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getimagethumbnail', $params); + + break; + case $asset instanceof Asset\Folder: + $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getfolderthumbnail', $params); + + break; + case $asset instanceof Asset\Video && \OpenDxp\Video::isAvailable(): + $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getvideothumbnail', $params); + + break; + case $asset instanceof Asset\Document && \OpenDxp\Document::isAvailable() && $asset->getPageCount(): + $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getdocumentthumbnail', $params); + + break; + case $asset instanceof Asset\Audio: + $thumbnailUrl = '/bundles/opendxpadmin/img/flat-color-icons/speaker.svg'; + + break; + default: + $thumbnailUrl = '/bundles/opendxpadmin/img/filetype-not-supported.svg'; + } + + return $thumbnailUrl; + } + + /** + * @throws Exception + */ + private function assignAssetTreeConfig( + Asset $element, + array &$tmpNode + ): void { + $user = $this->userLoader->getUser(); + $hasChildren = $element->getDao()->hasChildren($user); + $permissions = $element->getUserPermissions($user); + + $tmpNode['permissions'] = [ + 'remove' => $permissions['delete'], + 'settings' => $permissions['settings'], + 'rename' => $permissions['rename'], + 'publish' => $permissions['publish'], + 'view' => $permissions['view'], + 'list' => $permissions['list'], + ]; + + if ($element instanceof Asset\Folder) { + $tmpNode['leaf'] = false; + $tmpNode['expanded'] = !$hasChildren; + $tmpNode['loaded'] = !$hasChildren; + $tmpNode['permissions']['create'] = $permissions['create']; + $tmpNode['thumbnail'] = $this->getThumbnailUrl($element, ['origin' => 'treeNode']); + } else { + $tmpNode['leaf'] = true; + $tmpNode['expandable'] = false; + $tmpNode['expanded'] = false; + } + + $this->assignAssetThumbnailConfig($element, $tmpNode); + } + + /** + * @throws Exception + */ + private function assignDataObjectTreeConfig(DataObject\AbstractObject $element, array &$tmpNode): void + { + $allowedTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER]; + if ($element instanceof DataObject\Concrete && $element->getClass()->getShowVariants()) { + $allowedTypes[] = DataObject::OBJECT_TYPE_VARIANT; + } + $user = $this->userLoader->getUser(); + $hasChildren = $element->getDao()->hasChildren($allowedTypes, null, $user); + + $tmpNode['allowDrop'] = ($tmpNode['type'] ?? false) != DataObject::OBJECT_TYPE_VARIANT; + $tmpNode['isTarget'] = true; + $tmpNode['allowChildren'] = true; + $tmpNode['leaf'] = !$hasChildren; + $tmpNode['cls'] = 'opendxp_class_icon '; + + if ($element instanceof DataObject\Concrete) { + $tmpNode['published'] = $element->isPublished(); + $tmpNode['className'] = $element->getClass()->getName(); + + if (!$element->isPublished()) { + $tmpNode['cls'] .= 'opendxp_unpublished '; + } + + $tmpNode['allowVariants'] = $element->getClass()->getAllowVariants(); + } + + $tmpNode['expanded'] = !$hasChildren; + $tmpNode['permissions'] = $element->getUserPermissions($user); + + if ($tmpNode['leaf']) { + $tmpNode['expandable'] = false; + $tmpNode['leaf'] = false; //this is required to allow drag&drop + $tmpNode['expanded'] = true; + $tmpNode['loaded'] = true; + } + } + + /** + * @throws Exception + */ + private function assignDocumentTreeConfig( + Document $element, + array &$tmpNode + ): void { + $user = $this->userLoader->getUser(); + $hasChildren = $element->getDao()->hasChildren(null, $user); + + $treeNodePermissionTypes = [ + 'view', + 'remove' => 'delete', + 'settings', + 'rename', + 'publish', + 'unpublish', + 'create', + 'list', + ]; + + $permissions = $element->getUserPermissions($user); + + foreach ($treeNodePermissionTypes as $key => $permissionType) { + $permissionKey = is_string($key) ? $key : $permissionType; + $tmpNode['permissions'][$permissionKey] = $permissions[$permissionType]; + } + + $tmpNode['expandable'] = $hasChildren; + $tmpNode['loaded'] = !$hasChildren; + + // set type specific settings + if ($element->getType() === 'page') { + $tmpNode['leaf'] = false; + $tmpNode['expanded'] = !$hasChildren; + + // test for a site + if ($site = Site::getByRootId($element->getId())) { + $site->setRootDocument(null); + $tmpNode['site'] = $site->getObjectVars(); + } + } elseif (in_array($element->getType(), ['folder', 'link', 'hardlink'])) { + $tmpNode['leaf'] = false; + $tmpNode['expanded'] = !$hasChildren; + } elseif (method_exists($element, 'getTreeNodeConfig')) { //for BC reasons + $tmp = $element->getTreeNodeConfig(); + $tmpNode = [...$tmpNode, ...$tmp]; + } + + $this->assignDocumentSpecificSettings($element, $tmpNode); + } + + private function assignAssetThumbnailConfig(Asset $asset, array &$tmpAsset): void + { + try { + switch ($asset) { + case $asset instanceof Asset\Image: + $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); + + // we need the dimensions for the wysiwyg editors, so that they can resize the image immediately + if ($asset->getCustomSetting('imageDimensionsCalculated')) { + $tmpAsset['imageWidth'] = $asset->getCustomSetting('imageWidth'); + $tmpAsset['imageHeight'] = $asset->getCustomSetting('imageHeight'); + } + + break; + + case $asset instanceof Asset\Video: + if (\OpenDxp\Video::isAvailable()) { + $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); + } + + break; + + case $asset instanceof Asset\Document: + // add the PDF check here, otherwise the preview layer in admin is shown without content + if ( + \OpenDxp\Document::isAvailable() && + \OpenDxp\Document::isFileTypeSupported($asset->getFilename()) + ) { + $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); + } + + break; + } + } catch (Exception $e) { + Logger::error('Cannot get dimensions of asset, seems to be broken. Reason: ' . $e->getMessage()); + } + } + + private function assignDocumentSpecificSettings(Document $document, array &$tmpDocument): void + { + // PREVIEWS temporary disabled, need's to be optimized some time + if ($document instanceof Document\Page && isset($this->config['documents']['generate_preview'])) { + $thumbnailFile = $document->getPreviewImageFilesystemPath(); + // only if the thumbnail exists and isn't out of time + if (file_exists($thumbnailFile) && filemtime($thumbnailFile) > ($document->getModificationDate() - 20)) { + $tmpDocument['thumbnail'] = + $this->urlGenerator->generate('opendxp_admin_document_page_display_preview_image', + ['id' => $document->getId()] + ); + } + } + + $tmpDocument['cls'] = ''; + + if ($document instanceof Document\Page) { + $tmpDocument['url'] = $document->getFullPath(); + $site = Frontend::getSiteForDocument($document); + if ($site instanceof Site) { + $tmpDocument['url'] = + 'http://' . + $site->getMainDomain() . + preg_replace( + '@^' . + $site->getRootPath() . + '/?@', + '/', + $document->getRealFullPath() + ); + } + } + + if ($document->getProperty('navigation_exclude')) { + $tmpDocument['cls'] .= 'opendxp_navigation_exclude '; + } + + if (!$document->isPublished()) { + $tmpDocument['cls'] .= 'opendxp_unpublished '; + } + } + + private function mergeWorkflowPermissions(ElementInterface $element, array $permissions): array + { + $workflowManager = OpenDxp::getContainer()->get(Manager::class); + + $workflowPermission = [ + 'settings' => !$workflowManager->isDeniedInWorkflow($element, 'settings'), + 'rename' => !$workflowManager->isDeniedInWorkflow($element, 'rename'), + 'publish' => !$workflowManager->isDeniedInWorkflow($element, 'publish'), + ]; + + if ($element instanceof Asset) { + $workflowPermission['remove'] = !$workflowManager->isDeniedInWorkflow($element, 'delete'); + } elseif ($element instanceof DataObject) { + $workflowPermission['delete'] = !$workflowManager->isDeniedInWorkflow($element, 'delete'); + } + + return [...$permissions, ...$workflowPermission]; + } +} diff --git a/src/Service/Element/ElementServiceInterface.php b/src/Service/Element/ElementServiceInterface.php new file mode 100644 index 00000000..c329378c --- /dev/null +++ b/src/Service/Element/ElementServiceInterface.php @@ -0,0 +1,34 @@ + $element->getId(), - 'key' => $element->getKey(), - 'text' => htmlspecialchars($element->getKey() ?? ''), - 'type' => $element->getType(), - 'path' => $element->getRealFullPath(), - 'basePath' => $element->getRealPath(), - 'locked' => $element->isLocked(), - 'lockOwner' => (bool)$element->getLocked(), - 'elementType' => Service::getElementType($element), - ]; - - if ($element instanceof Asset) { - $tmpNode['cls'] = ''; - $this->assignAssetTreeConfig($element, $tmpNode); - } elseif ($element instanceof Document) { - $tmpNode['idx'] = $element->getIndex(); - $tmpNode['published'] = $element->isPublished(); - $tmpNode['leaf'] = true; - - $this->assignDocumentTreeConfig($element, $tmpNode); - } elseif ($element instanceof DataObject) { - $tmpNode['idx'] = $element->getIndex(); - $tmpNode['sortBy'] = $element->getChildrenSortBy(); - $tmpNode['sortOrder'] = $element->getChildrenSortOrder(); - - $this->assignDataObjectTreeConfig($element, $tmpNode); - } - - $tmpNode['permissions'] = $this->mergeWorkflowPermissions($element, $tmpNode['permissions']); - - $this->addAdminStyle($element, ElementAdminStyleEvent::CONTEXT_TREE, $tmpNode); - - if ($element->isLocked()) { - $tmpNode['cls'] .= 'opendxp_treenode_locked '; - } - if ($element->getLocked()) { - $tmpNode['cls'] .= 'opendxp_treenode_lockOwner '; - } - - return $tmpNode; - } - - public function getThumbnailUrl(Asset $asset, array $params = []): ?string - { - $defaults = [ - 'id' => $asset->getId(), - 'treepreview' => true, - '_dc' => $asset->getModificationDate(), - ]; - - if ($asset instanceof Asset\Image && ($params['origin'] ?? null) === 'folderPreview') { - if (!$asset->getThumbnail(Asset\Image\Thumbnail\Config::getPreviewConfig())->exists()) { - $defaults['_dc'] = time(); - } - } - - $params = [...$defaults, ...$params]; - - switch ($asset) { - case $asset instanceof Asset\Image: - $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getimagethumbnail', $params); - - break; - case $asset instanceof Asset\Folder: - $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getfolderthumbnail', $params); - - break; - case $asset instanceof Asset\Video && \OpenDxp\Video::isAvailable(): - $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getvideothumbnail', $params); - - break; - case $asset instanceof Asset\Document && \OpenDxp\Document::isAvailable() && $asset->getPageCount(): - $thumbnailUrl = $this->urlGenerator->generate('opendxp_admin_asset_getdocumentthumbnail', $params); - - break; - case $asset instanceof Asset\Audio: - $thumbnailUrl = '/bundles/opendxpadmin/img/flat-color-icons/speaker.svg'; - - break; - default: - $thumbnailUrl = '/bundles/opendxpadmin/img/filetype-not-supported.svg'; - } - - return $thumbnailUrl; - } - - /** - * @throws Exception - */ - private function assignAssetTreeConfig( - Asset $element, - array &$tmpNode - ): void { - $user = $this->userLoader->getUser(); - $hasChildren = $element->getDao()->hasChildren($user); - $permissions = $element->getUserPermissions($user); - - $tmpNode['permissions'] = [ - 'remove' => $permissions['delete'], - 'settings' => $permissions['settings'], - 'rename' => $permissions['rename'], - 'publish' => $permissions['publish'], - 'view' => $permissions['view'], - 'list' => $permissions['list'], - ]; - - if ($element instanceof Asset\Folder) { - $tmpNode['leaf'] = false; - $tmpNode['expanded'] = !$hasChildren; - $tmpNode['loaded'] = !$hasChildren; - $tmpNode['permissions']['create'] = $permissions['create']; - $tmpNode['thumbnail'] = $this->getThumbnailUrl($element, ['origin' => 'treeNode']); - } else { - $tmpNode['leaf'] = true; - $tmpNode['expandable'] = false; - $tmpNode['expanded'] = false; - } - - $this->assignAssetThumbnailConfig($element, $tmpNode); - } - - /** - * @throws Exception - */ - private function assignDataObjectTreeConfig(DataObject\AbstractObject $element, array &$tmpNode): void - { - $allowedTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER]; - if ($element instanceof DataObject\Concrete && $element->getClass()->getShowVariants()) { - $allowedTypes[] = DataObject::OBJECT_TYPE_VARIANT; - } - $user = $this->userLoader->getUser(); - $hasChildren = $element->getDao()->hasChildren($allowedTypes, null, $user); - - $tmpNode['allowDrop'] = ($tmpNode['type'] ?? false) != DataObject::OBJECT_TYPE_VARIANT; - $tmpNode['isTarget'] = true; - $tmpNode['allowChildren'] = true; - $tmpNode['leaf'] = !$hasChildren; - $tmpNode['cls'] = 'opendxp_class_icon '; - - if ($element instanceof DataObject\Concrete) { - $tmpNode['published'] = $element->isPublished(); - $tmpNode['className'] = $element->getClass()->getName(); - - if (!$element->isPublished()) { - $tmpNode['cls'] .= 'opendxp_unpublished '; - } - - $tmpNode['allowVariants'] = $element->getClass()->getAllowVariants(); - } - - $tmpNode['expanded'] = !$hasChildren; - $tmpNode['permissions'] = $element->getUserPermissions($user); - - if ($tmpNode['leaf']) { - $tmpNode['expandable'] = false; - $tmpNode['leaf'] = false; //this is required to allow drag&drop - $tmpNode['expanded'] = true; - $tmpNode['loaded'] = true; - } - } - - /** - * @throws Exception - */ - private function assignDocumentTreeConfig( - Document $element, - array &$tmpNode - ): void { - $user = $this->userLoader->getUser(); - $hasChildren = $element->getDao()->hasChildren(null, $user); - - $treeNodePermissionTypes = [ - 'view', - 'remove' => 'delete', - 'settings', - 'rename', - 'publish', - 'unpublish', - 'create', - 'list', - ]; - - $permissions = $element->getUserPermissions($user); - - foreach ($treeNodePermissionTypes as $key => $permissionType) { - $permissionKey = is_string($key) ? $key : $permissionType; - $tmpNode['permissions'][$permissionKey] = $permissions[$permissionType]; - } - - $tmpNode['expandable'] = $hasChildren; - $tmpNode['loaded'] = !$hasChildren; - - // set type specific settings - if ($element->getType() === 'page') { - $tmpNode['leaf'] = false; - $tmpNode['expanded'] = !$hasChildren; - - // test for a site - if ($site = Site::getByRootId($element->getId())) { - $site->setRootDocument(null); - $tmpNode['site'] = $site->getObjectVars(); - } - } elseif (in_array($element->getType(), ['folder', 'link', 'hardlink'])) { - $tmpNode['leaf'] = false; - $tmpNode['expanded'] = !$hasChildren; - } elseif (method_exists($element, 'getTreeNodeConfig')) { //for BC reasons - $tmp = $element->getTreeNodeConfig(); - $tmpNode = [...$tmpNode, ...$tmp]; - } - - $this->assignDocumentSpecificSettings($element, $tmpNode); - } - - private function assignAssetThumbnailConfig(Asset $asset, array &$tmpAsset): void - { - try { - switch ($asset) { - case $asset instanceof Asset\Image: - $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); - - // we need the dimensions for the wysiwyg editors, so that they can resize the image immediately - if ($asset->getCustomSetting('imageDimensionsCalculated')) { - $tmpAsset['imageWidth'] = $asset->getCustomSetting('imageWidth'); - $tmpAsset['imageHeight'] = $asset->getCustomSetting('imageHeight'); - } - - break; - - case $asset instanceof Asset\Video: - if (\OpenDxp\Video::isAvailable()) { - $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); - } - - break; - - case $asset instanceof Asset\Document: - // add the PDF check here, otherwise the preview layer in admin is shown without content - if ( - \OpenDxp\Document::isAvailable() && - \OpenDxp\Document::isFileTypeSupported($asset->getFilename()) - ) { - $tmpAsset['thumbnail'] = $this->getThumbnailUrl($asset, ['origin' => 'treeNode']); - } - - break; - } - } catch (Exception $e) { - Logger::error('Cannot get dimensions of asset, seems to be broken. Reason: ' . $e->getMessage()); - } - } - - private function assignDocumentSpecificSettings(Document $document, array &$tmpDocument): void - { - // PREVIEWS temporary disabled, need's to be optimized some time - if ($document instanceof Document\Page && isset($this->config['documents']['generate_preview'])) { - $thumbnailFile = $document->getPreviewImageFilesystemPath(); - // only if the thumbnail exists and isn't out of time - if (file_exists($thumbnailFile) && filemtime($thumbnailFile) > ($document->getModificationDate() - 20)) { - $tmpDocument['thumbnail'] = - $this->urlGenerator->generate('opendxp_admin_document_page_display_preview_image', - ['id' => $document->getId()] - ); - } - } - - $tmpDocument['cls'] = ''; - - if ($document instanceof Document\Page) { - $tmpDocument['url'] = $document->getFullPath(); - $site = Frontend::getSiteForDocument($document); - if ($site instanceof Site) { - $tmpDocument['url'] = - 'http://' . - $site->getMainDomain() . - preg_replace( - '@^' . - $site->getRootPath() . - '/?@', - '/', - $document->getRealFullPath() - ); - } - } - - if ($document->getProperty('navigation_exclude')) { - $tmpDocument['cls'] .= 'opendxp_navigation_exclude '; - } - - if (!$document->isPublished()) { - $tmpDocument['cls'] .= 'opendxp_unpublished '; - } - } - - private function mergeWorkflowPermissions(ElementInterface $element, array $permissions): array - { - $workflowManager = OpenDxp::getContainer()->get(Manager::class); - - $workflowPermission = [ - 'settings' => !$workflowManager->isDeniedInWorkflow($element, 'settings'), - 'rename' => !$workflowManager->isDeniedInWorkflow($element, 'rename'), - 'publish' => !$workflowManager->isDeniedInWorkflow($element, 'publish'), - ]; +declare(strict_types=1); - if ($element instanceof Asset) { - $workflowPermission['remove'] = !$workflowManager->isDeniedInWorkflow($element, 'delete'); - } elseif ($element instanceof DataObject) { - $workflowPermission['delete'] = !$workflowManager->isDeniedInWorkflow($element, 'delete'); - } +namespace OpenDxp\Bundle\AdminBundle\Service; - return [...$permissions, ...$workflowPermission]; - } -} +// BC shim: class moved to Service\Element\ElementService in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Service\Element\ElementService::class, '\OpenDxp\Bundle\AdminBundle\Service\ElementService'); diff --git a/src/Service/ElementServiceInterface.php b/src/Service/ElementServiceInterface.php index 55e182bf..0866b0fd 100644 --- a/src/Service/ElementServiceInterface.php +++ b/src/Service/ElementServiceInterface.php @@ -1,7 +1,5 @@ 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. + * + * @throws Exception on permission denied, save failure, or when there is + * no asset to update (job already completed) or nothing changed + */ + public function executeAssetBatch(array $data, User $adminUser): void + { + $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; + } + + $language = null; + if (isset($data['language'])) { + $language = $data['language'] !== 'default' ? $data['language'] : null; + } + + $asset = Asset::getById((int) $data['job']); + + if (!$asset) { + throw new Exception('AssetHelperController::batchAction => There is no asset left to update.'); + } + + 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) { + throw new Exception('AssetHelperController::batchAction => There is no asset left to update.'); + } + + $metadataEvent = new GenericEvent(null, [ + 'id' => $asset->getId(), + 'metadata' => $metadata, + ]); + + $this->eventDispatcher->dispatch($metadataEvent, AdminEvents::ASSET_METADATA_PRE_SET); + + $asset->setMetadataRaw($metadata); + $asset->save(); + } + + /** + * Executes a batch field update on a single DataObject. + * + * @throws Exception on permission denied, save failure, or when there is + * no object to update (job already completed) + */ + public function executeObjectBatch(array $params, string $locale, User $adminUser): void + { + $object = DataObject\Concrete::getById($params['job']); + + if (!$object) { + throw new Exception('There is no object left to update.'); + } + + $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(); + } +} \ 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..5c1db82a --- /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() ?? 0, + config: $config ?? [], + name: SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getName()), + description: SecurityHelper::convertHtmlSpecialChars($savedGridConfig->getDescription()), + sharedGlobally: $savedGridConfig->isShareGlobally(), + setAsFavourite: $savedGridConfig->isSetAsFavourite(), + isShared: $isShared, + ownerId: $savedGridConfig->getOwnerId() ?? 0, + 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/GridData/Asset.php b/src/Service/GridData/Asset.php index cddd4b1b..4c2ab49c 100644 --- a/src/Service/GridData/Asset.php +++ b/src/Service/GridData/Asset.php @@ -1,5 +1,4 @@ $asset->getId(), - 'id~system' => $asset->getId(), - 'type~system' => $asset->getType(), - 'fullpath~system' => $asset->getRealFullPath(), - 'filename~system' => $asset->getKey(), - 'creationDate~system' => $asset->getCreationDate(), - 'modificationDate~system' => $asset->getModificationDate(), - 'idPath~system' => Service::getIdPath($asset), - ]; - - $requestedLanguage = str_replace('default', '', $requestedLanguage); - - foreach ($fields as $field) { - $fieldDef = explode('~', $field); - if (isset($fieldDef[1]) && $fieldDef[1] === 'system') { - if ($fieldDef[0] === 'preview') { - $data[$field] = self::getPreviewThumbnail($asset, ['treepreview' => true, 'width' => 108, 'height' => 70, 'frame' => true]); - } elseif ($fieldDef[0] === 'size') { - $size = $asset->getFileSize(); - $data[$field] = OpenDxp\Helper\FileSystemHelper::formatBytes($size); - } - } else { - if (isset($fieldDef[1])) { - $language = ($fieldDef[1] === 'none' ? '' : $fieldDef[1]); - $rawMetaData = $asset->getMetadata($fieldDef[0], $language, true, true); - } else { - $rawMetaData = $asset->getMetadata($field, $requestedLanguage, true, true); - } - - $metaData = $rawMetaData['data'] ?? null; - - if ($rawMetaData) { - $type = $rawMetaData['type']; - if (!$loader) { - $loader = OpenDxp::getContainer()->get('opendxp.implementation_loader.asset.metadata.data'); - } - - $metaData = $rawMetaData['data'] ?? null; - - try { - /** @var Data $instance */ - $instance = $loader->build($type); - $metaData = $instance->getDataForListfolderGrid($rawMetaData['data'] ?? null, $rawMetaData); - } catch (UnsupportedException) { - } - } - - $data[$field] = $metaData; - } - } - } - - return $data; - } - - public static function getPreviewThumbnail(Model\Asset $asset, array $params = [], bool $onlyMethod = false): ?string - { - $thumbnailMethod = ''; - $thumbnailUrl = null; - - if ($asset instanceof Model\Asset\Image) { - $thumbnailMethod = 'getThumbnail'; - } elseif ($asset instanceof Model\Asset\Video && \OpenDxp\Video::isAvailable()) { - $thumbnailMethod = 'getImageThumbnail'; - } elseif ($asset instanceof Model\Asset\Document && \OpenDxp\Document::isAvailable()) { - $thumbnailMethod = 'getImageThumbnail'; - } - - if ($onlyMethod) { - return $thumbnailMethod; - } +declare(strict_types=1); - if (!empty($thumbnailMethod)) { - $thumbnailUrl = '/admin/asset/get-' . $asset->getType() . '-thumbnail?id=' . $asset->getId(); - if (count($params) > 0) { - $thumbnailUrl .= '&' . http_build_query($params); - } - } +namespace OpenDxp\Bundle\AdminBundle\Service\GridData; - return $thumbnailUrl; - } -} +// BC shim: class moved to Mapper\GridData\Asset in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Mapper\GridData\Asset::class, '\OpenDxp\Bundle\AdminBundle\Service\GridData\Asset'); diff --git a/src/Service/GridData/DataObject.php b/src/Service/GridData/DataObject.php index be2b2509..e3ec6b55 100644 --- a/src/Service/GridData/DataObject.php +++ b/src/Service/GridData/DataObject.php @@ -1,5 +1,4 @@ $object, - 'purpose' => 'gridview', - 'language' => $requestedLanguage, ]; - $data['classname'] = $object->getClassName(); - $data['idPath'] = Service::getIdPath($object); - $data['inheritedFields'] = []; - $data['permissions'] = $object->getUserPermissions($user); - $data['locked'] = $object->isLocked(); - - if (is_null($fields)) { - $fields = array_keys($object->getclass()->getFieldDefinitions()); - } - - $haveHelperDefinition = false; - - foreach ($fields as $key) { - $brickDescriptor = null; - $brickKey = null; - $brickType = null; - $brickGetter = null; - $dataKey = $key; - $keyParts = explode('~', $key); - - $def = $object->getClass()->getFieldDefinition($key, $context); - - if (str_starts_with($key, '#')) { - if (!$haveHelperDefinition) { - $helperDefinitions = self::getHelperDefinitions(); - $haveHelperDefinition = true; - } - if (!empty($helperDefinitions[$key])) { - $context['fieldname'] = $key; - $data[$key] = \OpenDxp\Model\DataObject\Service::calculateCellValue($object, $helperDefinitions, $key, $context); - } - } elseif (str_starts_with($key, '~')) { - $type = $keyParts[1]; - if ($type === 'classificationstore') { - $data[$key] = self::getStoreValueForObject($object, $key, $requestedLanguage); - } - } elseif (count($keyParts) > 1) { - // brick - $brickType = $keyParts[0]; - if (str_contains($brickType, '?')) { - $brickDescriptor = substr($brickType, 1); - $brickDescriptor = json_decode($brickDescriptor, true); - $brickType = $brickDescriptor['containerKey']; - } - - $brickKey = $keyParts[1]; - - $key = Service::getFieldForBrickType($object->getclass(), $brickType); - - $brickClass = Objectbrick\Definition::getByKey($brickType); - $context['outerFieldname'] = $key; - - if ($brickDescriptor) { - $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; - /** @var Model\DataObject\ClassDefinition\Data\Localizedfields $localizedFields */ - $localizedFields = $brickClass->getFieldDefinition($innerContainer); - $def = $localizedFields->getFieldDefinition($brickDescriptor['brickfield']); - } elseif ($brickClass instanceof Objectbrick\Definition) { - $def = $brickClass->getFieldDefinition($brickKey, $context); - } - } - - if (!empty($key)) { - // some of the not editable field require a special response - $getter = 'get' . ucfirst($key); - $needLocalizedPermissions = false; - - // if the definition is not set try to get the definition from localized fields - if (!$def) { - /** @var Model\DataObject\ClassDefinition\Data\Localizedfields|null $locFields */ - $locFields = $object->getClass()->getFieldDefinition('localizedfields'); - if ($locFields) { - $def = $locFields->getFieldDefinition($key, $context); - if ($def) { - $needLocalizedPermissions = true; - } - } - } - - //relation type fields with remote owner do not have a getter - if (method_exists($object, $getter)) { - //system columns must not be inherited - if (in_array($key, Concrete::SYSTEM_COLUMN_NAMES)) { - $data[$dataKey] = $object->$getter(); - } else { - $valueObject = self::getValueForObject($object, $key, $brickType, $brickKey, $def, $context, $brickDescriptor, $requestedLanguage); - $data['inheritedFields'][$dataKey] = ['inherited' => $valueObject->objectid != $object->getId(), 'objectid' => $valueObject->objectid]; - - if ($csvMode || method_exists($def, 'getDataForGrid')) { - if ($brickKey) { - $context['containerType'] = 'objectbrick'; - $context['containerKey'] = $brickType; - $context['outerFieldname'] = $key; - } - - $params = [...$params, 'context' => $context]; - if (!isset($params['purpose'])) { - $params['purpose'] = 'gridview'; - } - - if ($csvMode) { - $getterParams = ['language' => $requestedLanguage]; - $tempData = $def->getForCsvExport($object, $getterParams); - } elseif (method_exists($def, 'getDataForGrid')) { - $tempData = $def->getDataForGrid($valueObject->value, $object, $params); - } else { - continue; - } - - if ($def instanceof ClassDefinition\Data\Localizedfields) { - $needLocalizedPermissions = true; - foreach ($tempData as $tempKey => $tempValue) { - $data[$tempKey] = $tempValue; - } - } else { - $data[$dataKey] = $tempData; - if ( - $def instanceof Model\DataObject\ClassDefinition\Data\Select - && !$def->useConfiguredOptions() - && $def->getOptionsProviderClass() - ) { - $data[$dataKey . '%options'] = $def->getOptions(); - } - - // to prevent malforded grids in case of empty fieldcollections - if ($def instanceof ClassDefinition\Data\Fieldcollections) { - $data[$dataKey] ??= ''; - } - } - } else { - $data[$dataKey] = $valueObject->value; - } - } - } - - // because the key for the classification store has not a direct getter, you have to check separately if the data is inheritable - if ( - str_starts_with($key, '~') && - ($keyParts[1] ?? null) === 'classificationstore' - ) { - $fieldDef = self::getClassificationStoreFieldDefinition($key); - $value = self::normalizeValue($data[$key]); - - if ($fieldDef->isEmpty($value)) { - $inheritedData = static::getInheritedData($object, $key, $requestedLanguage); - - if ($inheritedData !== []) { - $parent = $inheritedData['parent']; - $data[$dataKey] = $inheritedData['value']; - $data['inheritedFields'][$dataKey] = [ - 'inherited' => $parent->getId() !== $object->getId(), - 'objectid' => $parent->getId(), - ]; - } - } - } - - if ($needLocalizedPermissions && !$user->isAdmin()) { - $locale = OpenDxp::getContainer()->get(LocaleServiceInterface::class)->findLocale(); - $permissionTypes = ['View', 'Edit']; - foreach ($permissionTypes as $permissionType) { - //TODO, this needs refactoring! Ideally, call it only once! - $languagesAllowed = Service::getLanguagePermissions($object, $user, 'l' . $permissionType); - - if ($languagesAllowed) { - $languagesAllowed = array_keys($languagesAllowed); - - if (!in_array($locale, $languagesAllowed)) { - $data['metadata']['permission'][$key]['no' . $permissionType] = 1; - if ($permissionType === 'View') { - $data[$key] = null; - } - } - } - } - } - } - } - } - - return $data; - } - - public static function getHelperDefinitions(): array - { - $stack = OpenDxp::getContainer()->get('request_stack'); - if ($stack->getMainRequest()?->hasSession()) { - $session = $stack->getSession(); - - return Session::useBag($session, fn (AttributeBagInterface $session) => $session->get('helpercolumns', []), 'opendxp_gridconfig'); - } - - return []; - } - - /** - * gets value for given object and getter, including inherited values - * - * @return stdClass value and objectid where the value comes from - */ - private static function getValueForObject(Concrete $object, string $key, ?string $brickType = null, ?string $brickKey = null, ?ClassDefinition\Data $fieldDefinition = null, array $context = [], ?array $brickDescriptor = null, ?string $requestedLanguage = null): stdClass - { - $getter = 'get' . ucfirst($key); - $value = null; - - try { - $value = $object->$getter($requestedLanguage ?? AdminTool::getCurrentUser()?->getLanguage()); - } catch (Throwable) { - } - - if (empty($value)) { - $value = $object->$getter(); - } - if (!empty($value) && !empty($brickType)) { - $getBrickType = 'get' . ucfirst($brickType); - $value = $value->$getBrickType(); - if (!empty($value) && !empty($brickKey)) { - if ($brickDescriptor) { - $innerContainer = $brickDescriptor['innerContainer'] ?? 'localizedfields'; - $localizedFields = $value->{'get' . ucfirst($innerContainer)}(); - $brickDefinition = Model\DataObject\Objectbrick\Definition::getByKey($brickType); - /** @var Model\DataObject\ClassDefinition\Data\Localizedfields $fieldDefinitionLocalizedFields */ - $fieldDefinitionLocalizedFields = $brickDefinition->getFieldDefinition('localizedfields'); - $fieldDefinition = $fieldDefinitionLocalizedFields->getFieldDefinition($brickKey); - $value = $localizedFields->getLocalizedValue($brickDescriptor['brickfield']); - } else { - $brickFieldGetter = 'get' . ucfirst($brickKey); - $value = $value->$brickFieldGetter(); - } - } - } - - if (!$fieldDefinition) { - $fieldDefinition = $object->getClass()->getFieldDefinition($key, $context); - } - - if (!empty($brickType) && !empty($brickKey) && !$brickDescriptor) { - $brickClass = Objectbrick\Definition::getByKey($brickType); - $context = ['object' => $object, 'outerFieldname' => $key]; - $fieldDefinition = $brickClass->getFieldDefinition($brickKey, $context); - } - - if ($fieldDefinition->isEmpty($value) && $fieldDefinition->supportsInheritance()) { - $parent = Service::hasInheritableParentObject($object); - if (!empty($parent)) { - return self::getValueForObject($parent, $key, $brickType, $brickKey, $fieldDefinition, $context, $brickDescriptor); - } - } - - $result = new stdClass(); - $result->value = $value; - $result->objectid = $object->getId(); - - return $result; - } - - /** - * gets store value for given object and key - */ - private static function getStoreValueForObject(Concrete $object, string $key, ?string $requestedLanguage): mixed - { - $keyParts = explode('~', $key); - - if (str_starts_with($key, '~')) { - $type = $keyParts[1]; - if ($type === 'classificationstore') { - $field = $keyParts[2]; - $groupKeyId = explode('-', $keyParts[3]); - - $groupId = (int) $groupKeyId[0]; - $keyid = (int) $groupKeyId[1]; - $getter = 'get' . ucfirst($field); - - if (method_exists($object, $getter)) { - /** @var Classificationstore $classificationStoreData */ - $classificationStoreData = $object->$getter(); - - /** @var Model\DataObject\ClassDefinition\Data\Classificationstore $csFieldDefinition */ - $csFieldDefinition = $object->getClass()->getFieldDefinition($field); - $csLanguage = $requestedLanguage; - - if (!$csFieldDefinition->isLocalized()) { - $csLanguage = 'default'; - } - - $fielddata = $classificationStoreData->getLocalizedKeyValue($groupId, $keyid, $csLanguage, true, true); - - $keyConfig = Model\DataObject\Classificationstore\KeyConfig::getById($keyid); - $type = $keyConfig->getType(); - $definition = json_decode($keyConfig->getDefinition(), true); - $definition = \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); - - if (method_exists($definition, 'getDataForGrid')) { - return $definition->getDataForGrid($fielddata, $object); - } - - return $fielddata; - } - } - } - - return null; - } - - protected static function getInheritedData(Concrete $object, string $key, string $requestedLanguage): array - { - if (!$parent = Service::hasInheritableParentObject($object)) { - return []; - } - - $inheritedValue = self::normalizeValue(self::getStoreValueForObject($parent, $key, $requestedLanguage)); - - if (!static::getClassificationStoreFieldDefinition($key)->isEmpty($inheritedValue)) { - return [ - 'parent' => $parent, - 'value' => $inheritedValue, - ]; - } - - return self::getInheritedData($parent, $key, $requestedLanguage); - } - - /** - * The actual data could be a plain array for tables, - * an associative array for RGB or Quantity Value where the value is a key. - */ - private static function normalizeValue(mixed $data): mixed - { - if (is_array($data)) { - if (array_is_list($data)) { - return $data; - } - if (array_key_exists('value', $data)) { - return $data['value']; - } - } - - return $data; - } - - protected static function getClassificationStoreFieldDefinition(string $key): Data - { - $keyParts = explode('~', $key); - $groupKeyId = explode('-', $keyParts[3]); - - $keyid = (int) $groupKeyId[1]; +declare(strict_types=1); - $keyConfig = Model\DataObject\Classificationstore\KeyConfig::getById($keyid); - $type = $keyConfig->getType(); - $definition = json_decode($keyConfig->getDefinition(), true); +namespace OpenDxp\Bundle\AdminBundle\Service\GridData; - return \OpenDxp\Model\DataObject\Classificationstore\Service::getFieldDefinitionFromJson($definition, $type); - } -} +// BC shim: class moved to Mapper\GridData\DataObject in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Mapper\GridData\DataObject::class, '\OpenDxp\Bundle\AdminBundle\Service\GridData\DataObject'); diff --git a/src/Service/GridData/Document.php b/src/Service/GridData/Document.php index 7ee47849..4842ae13 100644 --- a/src/Service/GridData/Document.php +++ b/src/Service/GridData/Document.php @@ -1,5 +1,4 @@ getTitle(); - $data['description'] = $document->getDescription(); - } else { - $data['title'] = ''; - $data['description'] = ''; - $data['name'] = ''; - } +namespace OpenDxp\Bundle\AdminBundle\Service\GridData; - return $data; - } -} +// BC shim: class moved to Mapper\GridData\Document in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Mapper\GridData\Document::class, '\OpenDxp\Bundle\AdminBundle\Service\GridData\Document'); diff --git a/src/Service/GridData/Element.php b/src/Service/GridData/Element.php index 4ceb8782..b61dd41f 100644 --- a/src/Service/GridData/Element.php +++ b/src/Service/GridData/Element.php @@ -1,5 +1,4 @@ $element->getId(), - 'fullpath' => $element->getRealFullPath(), - 'type' => Service::getElementType($element), - 'subtype' => $element->getType(), - 'filename' => $element->getKey(), - 'creationDate' => $element->getCreationDate(), - 'modificationDate' => $element->getModificationDate(), - ]; +declare(strict_types=1); - $data['published'] = method_exists($element, 'isPublished') ? $element->isPublished() : true; +namespace OpenDxp\Bundle\AdminBundle\Service\GridData; - return $data; - } -} +// BC shim: class moved to Mapper\GridData\Element in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Mapper\GridData\Element::class, '\OpenDxp\Bundle\AdminBundle\Service\GridData\Element'); diff --git a/src/Service/Login/LoginPageService.php b/src/Service/Login/LoginPageService.php new file mode 100644 index 00000000..e1e9753b --- /dev/null +++ b/src/Service/Login/LoginPageService.php @@ -0,0 +1,115 @@ +requestStack->getCurrentRequest(); + $gcMaxlifetime = (int) ini_get('session.gc_maxlifetime') ?: 120; + + $params = $this->base() + [ + 'csrfTokenRefreshInterval' => ($gcMaxlifetime - 60) * 1000, + 'browserSupported' => $this->detectBrowser(), + 'debug' => OpenDxp::inDebugMode(), + 'includeTemplates' => [], + 'deeplink' => $request?->query->has('deeplink') ?? false, + 'error' => $this->resolveError($tooManyAttempts), + 'login_error' => $this->authenticationUtils->getLastAuthenticationError(), + ]; + + $event = new GenericEvent(null, [ + 'parameters' => $params, + 'config' => $this->config, + 'request' => $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); + } + + $request = $this->requestStack->getCurrentRequest(); + + if ($request?->query->has('auth_failed')) { + return 'error_auth_failed'; + } + + if ($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/Notification/NotificationFilterParser.php b/src/Service/Notification/NotificationFilterParser.php new file mode 100644 index 00000000..c9a53d2f --- /dev/null +++ b/src/Service/Notification/NotificationFilterParser.php @@ -0,0 +1,168 @@ + 'title', + 'timestamp' => 'creationDate', + ]; + + /** + * @param array> $filter + * + * @return array}> + */ + public function parse(array $filter = []): array + { + $result = []; + + foreach ($filter as $item) { + $type = $item[self::KEY_TYPE]; + + switch ($type) { + case self::TYPE_STRING: + [$key, $condition, $conditionVariables] = $this->parseString($item); + $result[$key] = [ + 'condition' => $condition, + 'conditionVariables' => $conditionVariables, + ]; + + break; + case self::TYPE_DATE: + [$key, $condition, $conditionVariables] = $this->parseDate($item); + $result[$key] = [ + 'condition' => $condition, + 'conditionVariables' => $conditionVariables, + ]; + + break; + } + } + + return $result; + } + + /** + * @return array{0: string, 1: string, 2: array} + * + * @throws Exception + */ + private function parseString(array $item): array + { + $result = null; + $property = $this->getDbProperty($item); + $value = $item[self::KEY_VALUE] ?? ''; + + if ($item[self::KEY_OPERATOR] === self::OPERATOR_LIKE) { + $key = $property . '_like'; + $result = [ + $key, + "{$property} LIKE :{$key}", + [$key => "%{$value}%"], + ]; + } + + if (is_null($result)) { + throw new Exception(); + } + + return $result; + } + + /** + * @return array{0: string, 1: string, 2: array} + * + * @throws Exception + */ + private function parseDate(array $item): array + { + $result = null; + $property = $this->getDbProperty($item); + $value = new Carbon($item[self::KEY_VALUE]); + + switch ($item[self::KEY_OPERATOR]) { + case self::OPERATOR_EQ: + $key = $property . '_eq'; + $result = [ + $key, + "{$property} BETWEEN :{$key}_start AND :{$key}_end", + [ + $key . '_start' => $value->toDateTimeString(), + $key . '_end' => $value->addDay()->subSecond()->toDateTimeString(), + ], + ]; + + break; + case self::OPERATOR_GT: + $key = $property . '_gt'; + $result = [ + $key, + "{$property} > :{$key}", + [$key => $value->toDateTimeString()], + ]; + + break; + case self::OPERATOR_LT: + $key = $property . '_lt'; + $result = [ + $key, + "{$property} < :{$key}", + [$key => $value->addDay()->subSecond()->toDateTimeString()], + ]; + + break; + } + + if (is_null($result)) { + throw new Exception(); + } + + return $result; + } + + private function getDbProperty(array $item): string + { + $property = $item[self::KEY_PROPERTY]; + + return $this->properties[$property] ?? $property; + } +} diff --git a/src/Service/Portal/DashboardService.php b/src/Service/Portal/DashboardService.php new file mode 100644 index 00000000..a366730f --- /dev/null +++ b/src/Service/Portal/DashboardService.php @@ -0,0 +1,99 @@ +mergeWithPredefined($this->repository->load($user)); + } + + public function getDashboard(User $user, string $key = 'welcome'): array + { + $dashboards = $this->getAllDashboards($user); + $dashboard = $dashboards[$key] ?? null; + + if ($dashboard) { + $disabledPortlets = array_keys($this->getDisabledPortlets($user)); + $positions = $dashboard['positions']; + if (is_array($positions)) { + foreach ($positions as $columnKey => $column) { + if ($column) { + foreach ($column as $portletKey => $portletCfg) { + $type = $portletCfg['type']; + if (in_array($type, $disabledPortlets)) { + unset($dashboard['positions'][$columnKey][$portletKey]); + } + } + } + } + } + } + + return $dashboard ?: ['positions' => [[], []]]; + } + + public function saveDashboard(User $user, string $key, ?array $configuration = null): void + { + $dashboards = $this->getAllDashboards($user); + $dashboards[$key] = $configuration ?: ['positions' => [[], []]]; + + $this->repository->save($user, $dashboards); + } + + public function deleteDashboard(User $user, string $key): void + { + $dashboards = $this->getAllDashboards($user); + unset($dashboards[$key]); + + $this->repository->save($user, $dashboards); + } + + public function getDisabledPortlets(User $user): array + { + $perspectiveCfg = Config::getRuntimePerspective($user); + $dashboardCfg = $perspectiveCfg['dashboards'] ?? []; + + return $dashboardCfg['disabledPortlets'] ?? []; + } + + private function mergeWithPredefined(array $dashboards): array + { + $perspectiveCfg = Config::getRuntimePerspective(); + $dashboardCfg = $perspectiveCfg['dashboards'] ?? []; + $predefined = $dashboardCfg['predefined'] ?? []; + + if (empty($dashboards)) { + return $predefined; + } + + foreach ($predefined as $key => $dashboard) { + if (!isset($dashboards[$key])) { + $dashboards[$key] = $dashboard; + } + } + + return $dashboards; + } +} diff --git a/src/Service/PreviewGenerator.php b/src/Service/PreviewGenerator.php index 546ff06b..4f671da0 100644 --- a/src/Service/PreviewGenerator.php +++ b/src/Service/PreviewGenerator.php @@ -8,134 +8,13 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://pimcore.com) - * @copyright Modification Copyright (c) OpenDXP (https://www.opendxp.io) + * @copyright Copyright (c) OpenDXP (https://www.opendxp.io) * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) */ -namespace OpenDxp\Bundle\AdminBundle\Service; - -use Locale; -use LogicException; -use OpenDxp\Model\DataObject\ClassDefinition\LinkGeneratorInterface; -use OpenDxp\Model\DataObject\ClassDefinition\PreviewGeneratorInterface; -use OpenDxp\Model\DataObject\Concrete; -use OpenDxp\Model\Site; -use OpenDxp\Model\Translation; -use OpenDxp\Tool; -use OpenDxp\Translation\Translator; -use Symfony\Contracts\Service\Attribute\Required; - -class PreviewGenerator implements PreviewGeneratorInterface -{ - protected Translator $translator; - - public function generatePreviewUrl(Concrete $object, array $params): string - { - $linkGenerator = $object->getClass()->getLinkGenerator(); - - if ($linkGenerator instanceof LinkGeneratorInterface) { - $filteredParameters = $this->filterParameters($object, $params); - - $locale = $filteredParameters[PreviewGeneratorInterface::PARAMETER_LOCALE] ?? Tool::getDefaultLanguage(); - $site = array_key_exists(PreviewGeneratorInterface::PARAMETER_SITE, $filteredParameters) ? Site::getById($filteredParameters[PreviewGeneratorInterface::PARAMETER_SITE]) : (new Site\Listing())->current(); - - return $linkGenerator->generate($object, [ - PreviewGeneratorInterface::PARAMETER_LOCALE => $locale, - PreviewGeneratorInterface::PARAMETER_SITE => $site, - ]); - } - - throw new LogicException("No link generator given for element of type {$object->getClassName()}"); - } - - /** - * @return array only parameters that are part of the preview generator config and are not empty - */ - protected function filterParameters(Concrete $object, array $parameters): array - { - $previewConfig = $this->getPreviewConfig($object); - - $filteredParameters = []; - foreach ($previewConfig as $config) { - $name = $config['name']; - $selectedValue = $parameters[$name] ?? $config['defaultValue']; - - if (!empty($selectedValue)) { - $filteredParameters[$name] = $selectedValue; - } - } - - return $filteredParameters; - } +declare(strict_types=1); - public function getPreviewConfig(Concrete $object): array - { - return array_filter([ - $this->getLocalePreviewConfig($object), - $this->getSitePreviewConfig($object), - ]); - } - - protected function getLocalePreviewConfig(Concrete $object): array - { - $user = Tool\Authentication::authenticateSession(); - $userLocale = $user->getLanguage(); - - $locales = []; - foreach (Tool::getValidLanguages() as $locale) { - $label = sprintf('%s (%s)', Locale::getDisplayLanguage($locale, $userLocale), $locale); - $locales[$label] = $locale; - } - - return [ - 'name' => PreviewGeneratorInterface::PARAMETER_LOCALE, - 'label' => $this->translator->trans('preview_generator_locale', [], Translation::DOMAIN_ADMIN), - 'values' => $locales, - 'defaultValue' => in_array($userLocale, Tool::getValidLanguages()) ? $userLocale : Tool::getDefaultLanguage(), - ]; - } - - protected function getSitePreviewConfig(Concrete $object): array - { - $sites = new Site\Listing(); - $sites->setOrderKey('mainDomain')->setOrder('ASC'); - - if ($sites->count() === 0) { - return []; - } - - $sitesOptions = [ - $this->translator->trans('main_site', [], Translation::DOMAIN_ADMIN) => '0', - ]; - - $preSelectedSite = null; - foreach ($sites as $site) { - $label = $site->getRootDocument()?->getKey(); - $sitesOptions[$label] = $site->getId(); - - $domains = $site->getDomains(); - array_unshift($domains, $site->getMainDomain()); - - if (is_null($preSelectedSite) && in_array(Tool::getHostname(), $domains)) { - $preSelectedSite = $sitesOptions[$label]; - } - } - - return [ - 'name' => PreviewGeneratorInterface::PARAMETER_SITE, - 'label' => $this->translator->trans('preview_generator_site', [], Translation::DOMAIN_ADMIN), - 'values' => $sitesOptions, - 'defaultValue' => $preSelectedSite ?? reset($sitesOptions), - ]; - } +namespace OpenDxp\Bundle\AdminBundle\Service; - /** - * @internal - */ - #[Required] - public function setTranslator(Translator $translator): void - { - $this->translator = $translator; - } -} +// BC shim: class moved to Generator\PreviewGenerator in 1.4, will be removed in 2.0. +class_alias(\OpenDxp\Bundle\AdminBundle\Generator\PreviewGenerator::class, '\OpenDxp\Bundle\AdminBundle\Service\PreviewGenerator'); diff --git a/src/Service/Translation/TranslationQueryService.php b/src/Service/Translation/TranslationQueryService.php new file mode 100644 index 00000000..28c9d445 --- /dev/null +++ b/src/Service/Translation/TranslationQueryService.php @@ -0,0 +1,187 @@ +onCreateQueryBuilder( + function (DoctrineQueryBuilder $select) use ($joins, $tableName, $filters): void { + + $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 = ' . $this->db->quote($fieldName) + . ')' + ); + } + + $havings = $filters['conditions']; + if ($havings) { + $havings = implode(' AND ', $havings); + $select->having($havings); + } + } + ); + } + + public function getGridFilterCondition(array $filterParameters, string $tableName, bool $languageMode, array $validLanguages): array + { + $placeHolderCount = 0; + $joins = []; + $conditions = []; + + $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; + } + + $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate']; + if (!$languageMode && !in_array($fieldName, $allowedNonLanguageFields)) { + 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 = $this->db->quoteIdentifier($field) . ' ' . $operator . ' ' . $this->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/Session/Gateway/BulkOperationSessionGateway.php b/src/Session/Gateway/BulkOperationSessionGateway.php new file mode 100644 index 00000000..12104125 --- /dev/null +++ b/src/Session/Gateway/BulkOperationSessionGateway.php @@ -0,0 +1,55 @@ +bag()->set('class_bulk_export_settings', $json); + } + + public function getExportSettings(): ?string + { + return $this->bag()->get('class_bulk_export_settings'); + } + + public function storeImportFile(string $tmpFile): void + { + $this->bag()->set('class_bulk_import_file', $tmpFile); + } + + public function getImportFile(): ?string + { + return $this->bag()->get('class_bulk_import_file'); + } + + private function bag(): AttributeBagInterface + { + return Tool\Session::getSessionBag($this->requestStack->getSession(), self::BAG_BULK_OPERATION); + } +} diff --git a/src/Session/Gateway/CopySessionGateway.php b/src/Session/Gateway/CopySessionGateway.php new file mode 100644 index 00000000..f603a7bd --- /dev/null +++ b/src/Session/Gateway/CopySessionGateway.php @@ -0,0 +1,94 @@ +bag()->set($transactionId, ['idMapping' => []]); + } + + public function getParentId(string $transactionId): ?int + { + $entry = $this->getEntry($transactionId); + + return isset($entry['parentId']) ? (int) $entry['parentId'] : null; + } + + public function rememberParentId(string $transactionId, int $parentId): void + { + $entry = $this->getEntry($transactionId); + $entry['parentId'] = $parentId; + $this->bag()->set($transactionId, $entry); + } + + public function getIdMapping(string $transactionId): array + { + return $this->getEntry($transactionId)['idMapping'] ?? []; + } + + public function rememberCopiedId(string $transactionId, int $sourceId, int $newId, bool $alsoAsParent): void + { + $entry = $this->getEntry($transactionId); + $entry['idMapping'][$sourceId] = $newId; + + if ($alsoAsParent) { + $entry['parentId'] = $newId; + } + + $this->bag()->set($transactionId, $entry); + } + + /** + * Pops and persists the next id off the transaction's rewrite stack, lazily seeding the + * stack from the id mapping on first call. + */ + public function popRewriteStackId(string $transactionId): int + { + $entry = $this->getEntry($transactionId); + + if (!array_key_exists('rewrite-stack', $entry)) { + $entry['rewrite-stack'] = array_values($entry['idMapping'] ?? []); + } + + $nextId = (int) array_shift($entry['rewrite-stack']); + $this->bag()->set($transactionId, $entry); + + return $nextId; + } + + private function getEntry(string $transactionId): array + { + return $this->bag()->get($transactionId, []); + } + + private function bag(): AttributeBagInterface + { + return Tool\Session::getSessionBag($this->requestStack->getSession(), self::BAG_COPY); + } +} diff --git a/src/Session/Gateway/GridColumnConfigSessionGateway.php b/src/Session/Gateway/GridColumnConfigSessionGateway.php new file mode 100644 index 00000000..ac210d86 --- /dev/null +++ b/src/Session/Gateway/GridColumnConfigSessionGateway.php @@ -0,0 +1,51 @@ +bag()->get('helpercolumns', []); + } + + public function setHelperColumns(array $helperColumns): void + { + $this->bag()->set('helpercolumns', $helperColumns); + } + + public function prependHelperColumns(array $helperColumns): void + { + $existing = $this->getHelperColumns(); + $this->setHelperColumns([...$helperColumns, ...$existing]); + } + + private function bag(): AttributeBagInterface + { + return Tool\Session::getSessionBag($this->requestStack->getSession(), self::BAG_GRID_COLUMN_CONFIG); + } +} diff --git a/src/Session/Gateway/PasswordResetSessionGateway.php b/src/Session/Gateway/PasswordResetSessionGateway.php new file mode 100644 index 00000000..c42c76f4 --- /dev/null +++ b/src/Session/Gateway/PasswordResetSessionGateway.php @@ -0,0 +1,39 @@ +requestStack->getSession(), + static fn (AttributeBagInterface $adminSession) => $adminSession->get('password_reset'), + self::BAG_ADMIN, + ); + } +} diff --git a/src/Session/Gateway/TranslationImportSessionGateway.php b/src/Session/Gateway/TranslationImportSessionGateway.php new file mode 100644 index 00000000..c9a30609 --- /dev/null +++ b/src/Session/Gateway/TranslationImportSessionGateway.php @@ -0,0 +1,45 @@ +bag()->set('translation_import_file', $tmpFile); + } + + public function getImportFile(): ?string + { + return $this->bag()->get('translation_import_file'); + } + + private function bag(): AttributeBagInterface + { + return Tool\Session::getSessionBag($this->requestStack->getSession(), self::BAG_TRANSLATION_IMPORT); + } +} diff --git a/src/Session/Gateway/TwoFactorSetupSessionGateway.php b/src/Session/Gateway/TwoFactorSetupSessionGateway.php new file mode 100644 index 00000000..21e76ec5 --- /dev/null +++ b/src/Session/Gateway/TwoFactorSetupSessionGateway.php @@ -0,0 +1,38 @@ +requestStack->getSession()->set('2fa_secret', $secret); + } + + public function getSecret(): string + { + return (string) $this->requestStack->getSession()->get('2fa_secret'); + } +} diff --git a/src/Session/SessionGatewayInterface.php b/src/Session/SessionGatewayInterface.php new file mode 100644 index 00000000..e5bc898a --- /dev/null +++ b/src/Session/SessionGatewayInterface.php @@ -0,0 +1,29 @@ +requestStack->getSession()->getId(); + } +} \ No newline at end of file diff --git a/src/Session/SessionIdentityInterface.php b/src/Session/SessionIdentityInterface.php new file mode 100644 index 00000000..fc712645 --- /dev/null +++ b/src/Session/SessionIdentityInterface.php @@ -0,0 +1,26 @@ +getModule('\\'.OpenDxp::class); $config = $openDxpModule->grabService(Config::class); - $elementService = Stub::construct( + + return Stub::construct( ElementService::class, [ Stub::makeEmpty(UrlGeneratorInterface::class), @@ -45,8 +47,20 @@ 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; + }, + ]); + } - return Stub::construct($classname, [$elementService], [ + protected function buildController(string $classname, User $user): mixed + { + return Stub::construct($classname, [], [ 'getAdminUser' => function () use ($user) { return $user; }, @@ -63,4 +77,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..af9fe33c 100644 --- a/tests/Model/Permissions/ModelAssetPermissionsTest.php +++ b/tests/Model/Permissions/ModelAssetPermissionsTest.php @@ -18,6 +18,8 @@ namespace OpenDxp\Bundle\AdminBundle\Tests\Model\Controller; use OpenDxp\Bundle\AdminBundle\Controller\Admin\Asset\AssetController; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\TreeGetAssetChildren\TreeGetAssetChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\Asset\TreeGetAssetChildren\TreeGetAssetChildrenPayload; use OpenDxp\Model\Asset; use OpenDxp\Model\Property; use OpenDxp\Model\User; @@ -290,6 +292,10 @@ public function testTreeGetChildrenById(): void protected function doTestTreeGetChildrenById(Asset $element, User $user, array $expectedChildren): void { + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $handler = new TreeGetAssetChildrenHandler($userContext, $elementService, new EventDispatcher()); + $controller = $this->buildController(AssetController::class, $user); $request = new Request([ @@ -297,12 +303,8 @@ protected function doTestTreeGetChildrenById(Asset $element, User $user, array $ 'limit' => 100, 'view' => 0, ]); - $eventDispatcher = new EventDispatcher(); - $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher - ); + $responseData = $controller->treeGetChildrenByIdPaginatedAction(TreeGetAssetChildrenPayload::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..06d0c870 100644 --- a/tests/Model/Permissions/ModelDataObjectPermissionsTest.php +++ b/tests/Model/Permissions/ModelDataObjectPermissionsTest.php @@ -19,6 +19,8 @@ use Exception; use OpenDxp\Bundle\AdminBundle\Controller\Admin\DataObject\DataObjectController; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetDataObjectChildren\TreeGetDataObjectChildrenHandler; +use OpenDxp\Bundle\AdminBundle\Handler\DataObject\TreeGetDataObjectChildren\TreeGetDataObjectChildrenPayload; use OpenDxp\Model\DataObject; use OpenDxp\Model\User; use OpenDxp\Tests\Support\Util\TestHelper; @@ -216,18 +218,19 @@ protected function doTestTreeGetChildrenById( User $user, ?array $expectedChildren ): void { + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $handler = new TreeGetDataObjectChildrenHandler($userContext, $elementService, new EventDispatcher()); + $controller = $this->buildController(DataObjectController::class, $user); - $request = new Request([ - 'node' => $element->getId(), - ]); - $eventDispatcher = new EventDispatcher(); + $request = new Request(['node' => $element->getId(), 'limit' => 100]); try { TestHelper::callMethod($controller, 'checkPermission', ['objects']); - $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher + $responseData = $controller->treeGetChildrenByIdPaginatedAction( + TreeGetDataObjectChildrenPayload::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..5fc32c59 100644 --- a/tests/Model/Permissions/ModelDocumentPermissionsTest.php +++ b/tests/Model/Permissions/ModelDocumentPermissionsTest.php @@ -18,6 +18,8 @@ namespace OpenDxp\Bundle\AdminBundle\Tests\Model\Controller; use OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentController; +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 +164,10 @@ protected function prepareUsers(): void protected function doTestTreeGetChildrenById(Document $element, User $user, array $expectedChildren): void { + $elementService = $this->buildElementService($user); + $userContext = $this->buildUserContext($user); + $handler = new TreeGetDocumentChildrenHandler($userContext, $elementService, new EventDispatcher()); + $controller = $this->buildController(DocumentController::class, $user); $request = new Request([ @@ -169,12 +175,8 @@ protected function doTestTreeGetChildrenById(Document $element, User $user, arra 'limit' => 100, 'view' => 0, ]); - $eventDispatcher = new EventDispatcher(); - $responseData = $controller->treeGetChildrenByIdAction( - $request, - $eventDispatcher - ); + $responseData = $controller->treeGetChildrenByIdPaginatedAction(TreeGetDocumentChildrenPayload::fromRequest($request), $handler); $responsePaths = []; $responseData = json_decode($responseData->getContent(), true);