Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions Classes/Backend/Controller/PersistenceController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace TYPO3\CMS\VisualEditor\Backend\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\VisualEditor\Service\DataHandlerService;

use function array_keys;
use function implode;
use function is_array;

#[AsController]
final readonly class PersistenceController
{
public function __construct(
private DataHandlerService $dataHandlerService,
) {
}

public function saveAction(ServerRequestInterface $request): ResponseInterface
{
$input = $this->getJsonPayload($request);

$data = $input['data'] ?? [];
unset($input['data']);
$cmdArray = $input['cmdArray'] ?? [];
unset($input['cmdArray']);
if (!is_array($data)) {
throw new RuntimeException('Data must be an array of table names to rows', 5781185589);
}

if (!is_array($cmdArray)) {
throw new RuntimeException('Command array must be a list of DataHandler commands', 4576273831);
}

if ($input !== []) {
throw new RuntimeException('Unknown data operations: ' . implode(', ', array_keys($input)) . ' only data and cmdArray are allowed', 8110225095);
}

$GLOBALS['TYPO3_REQUEST'] = $request;
$errorLog = $this->dataHandlerService->run($data, []);

foreach ($cmdArray as $cmd) {
$errorLog = [...$errorLog, ...$this->dataHandlerService->run([], $cmd)];
}

if ($errorLog) {
return new JsonResponse(['success' => false, 'errorLog' => $errorLog], 500);
}

return new JsonResponse(['success' => true]);
}

/**
* @return array<string, mixed>
*/
private function getJsonPayload(ServerRequestInterface $request): array
{
$payload = $request->getParsedBody();
if (!is_array($payload)) {
$payload = json_decode((string)$request->getBody(), true, 512, JSON_THROW_ON_ERROR);
}

if (!is_array($payload)) {
throw new RuntimeException('Save payload must be a JSON object', 2634277014);
}

return $payload;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use TYPO3\CMS\Backend\Middleware\JavaScriptLabelImportMapEntryResolver;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Routing\UriBuilder;
Expand All @@ -17,33 +16,23 @@
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Error\Http\UnauthorizedException;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Page\Event\ResolveVirtualJavaScriptImportEvent;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Frontend\Page\PageInformation;
use TYPO3\CMS\VisualEditor\Service\DataHandlerService;

use function array_keys;
use function implode;
use function json_decode;
use function substr;

readonly class PersistenceMiddleware implements MiddlewareInterface
readonly class EditModeMiddleware implements MiddlewareInterface
{
public function __construct(
private Context $context,
private DataHandlerService $dataHandlerService,
private UriBuilder $uriBuilder,
private ViewFactoryInterface $viewFactory,
private FormProtectionFactory $formProtectionFactory,
private Typo3Version $typo3Version,
private ListenerProvider $listenerProvider,
private PageRenderer $pageRenderer,
Expand All @@ -52,54 +41,19 @@ public function __construct(

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return match ($this->whatToDo($request)) {
MiddlewareAction::Edit => $this->handleEdit($request, $handler),
MiddlewareAction::Save => $this->saveStuff($request),
MiddlewareAction::None => $handler->handle($request),
};
}

private function saveStuff(ServerRequestInterface $request): ResponseInterface
{
$token = $request->getHeaderLine('X-Request-Token');
if (!$token || !$this->formProtectionFactory->createForType('backend')->validateToken($token, 'visual_editor', 'save')) {
throw new UnauthorizedException('Invalid or missing request token', 8148623595);
}

$input = $request->getParsedBody() ??
json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);

$data = $input['data'] ?? [];
unset($input['data']);
$cmdArray = $input['cmdArray'] ?? [];
unset($input['cmdArray']);

if (!empty($input)) {
throw new RuntimeException('Unknown data operations: ' . implode(', ', array_keys($input)) . ' only data and cmdArray are allowed', 8110225095);
if ($this->shouldInitEditMode($request)) {
return $this->handleEdit($request, $handler);
}

// Required by DefaultSanitizerBuilder when processing RTE fields via DataHandler;
// this middleware short-circuits before the FE RequestHandler which normally sets this global.
$GLOBALS['TYPO3_REQUEST'] = $request;
$errorLog = $this->dataHandlerService->run($data, []);

foreach ($cmdArray as $cmd) {
$errorLog = [...$errorLog, ...$this->dataHandlerService->run([], $cmd)];
}

if ($errorLog) {
return new JsonResponse(['success' => false, 'errorLog' => $errorLog], 500);
}

return new JsonResponse(['success' => true]);
return $handler->handle($request);
}

private function whatToDo(ServerRequestInterface $request): MiddlewareAction
private function shouldInitEditMode(ServerRequestInterface $request): bool
{
// parameter editMode must be set
$params = $request->getQueryParams();
if (!isset($params['editMode'])) {
return MiddlewareAction::None;
return false;
}

// backend user required
Expand All @@ -123,42 +77,7 @@ private function whatToDo(ServerRequestInterface $request): MiddlewareAction
throw new UnauthorizedException('No $GLOBALS[\'BE_USER\'] available', 8725323237);
}

// only do something on POST requests
if ($request->getMethod() !== 'POST') {
return MiddlewareAction::Edit;
}

// only allow application/json content type
if ($request->getHeaderLine('Content-Type') !== 'application/json') {
throw new UnauthorizedException('Content-Type must be application/json to save stuff with visual_editor', 5015404100);
}

if ($user->isAdmin()) {
return MiddlewareAction::Save;
}

// check permissions of user on page
$pageInformation = $this->getPageInformation($request);

if (!$beUser->isInWebMount($pageInformation->getId())) {
throw new UnauthorizedException('No permission to access this page', 1610177162);
}

if (!$beUser->doesUserHaveAccess($pageInformation->getPageRecord(), Permission::CONTENT_EDIT)) {
throw new UnauthorizedException('No permission to edit content on this page', 7668402611);
}

return MiddlewareAction::Save;
}

private function getPageInformation(ServerRequestInterface $request): PageInformation
{
$frontendPageInformation = $request->getAttribute('frontend.page.information');
if (!$frontendPageInformation instanceof PageInformation) {
throw new RuntimeException('No frontend page information available', 7005099635);
}

return $frontendPageInformation;
return true;
}

private function handleEdit(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
Expand Down
12 changes: 0 additions & 12 deletions Classes/Middleware/MiddlewareAction.php

This file was deleted.

7 changes: 7 additions & 0 deletions Configuration/Backend/AjaxRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use TYPO3\CMS\VisualEditor\Backend\Controller\CrossOriginNavigationController;
use TYPO3\CMS\VisualEditor\Backend\Controller\PersistenceController;

return [
'visual_editor_resolve_cross_origin_backend_url' => [
Expand All @@ -11,4 +12,10 @@
'methods' => ['POST'],
'inheritAccessFromModule' => 'web_edit',
],
'visual_editor_save' => [
'path' => '/visual-editor/save',
'target' => PersistenceController::class . '::saveAction',
'methods' => ['POST'],
'inheritAccessFromModule' => 'web_edit',
],
];
4 changes: 2 additions & 2 deletions Configuration/RequestMiddlewares.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
declare(strict_types=1);

use TYPO3\CMS\VisualEditor\Middleware\DisableCacheInEditModeMiddleware;
use TYPO3\CMS\VisualEditor\Middleware\PersistenceMiddleware;
use TYPO3\CMS\VisualEditor\Middleware\EditModeMiddleware;

return [
'frontend' => [
'typo3/cms-visual-editor/persistence-middleware' => [
'target' => PersistenceMiddleware::class,
'target' => EditModeMiddleware::class,
'after' => [
'typo3/cms-frontend/prepare-tsfe-rendering',
'typo3/cms-frontend/tsfe', // TODO typo3/cms-frontend/tsfe can be dropped if TYPO3 14 is lowest supported version
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {css, html, LitElement} from 'lit';
import {onMessageDebounced, sendMessage} from '@typo3/visual-editor/Shared/iframe-messaging';
import {autoSaveActive} from '@typo3/visual-editor/Shared/local-stores';
import {VeBackendSaveButton} from '@typo3/visual-editor/Backend/components/ve-backend-save-button';


/**
Expand Down Expand Up @@ -76,7 +77,7 @@ export class VeAutoSaveToggle extends LitElement {
this.count = count;
this.invalidCount = invalidCount;
if (this.active && this.count > 0 && this.invalidCount === 0) {
sendMessage('doSave');
this.triggerDoSave();
}
}

Expand All @@ -91,9 +92,18 @@ export class VeAutoSaveToggle extends LitElement {
autoSaveActive.set(this.active);

if (this.active && this.count > 0 && this.invalidCount === 0) {
sendMessage('doSave');
this.triggerDoSave();
}
}

triggerDoSave() {
const element = document.querySelector('ve-backend-save-button');
if (!(element instanceof VeBackendSaveButton)) {
throw new Error('ve-backend-save-button is missing, could not autosave');
}
element.doSave();
}

#onKeydown(e) {
if (this.hasAttribute('disabled') || (e.key !== 'Enter' && e.key !== ' ')) {
return;
Expand Down
Loading