Commit f16b4787 authored by Oliver Hader's avatar Oliver Hader Committed by Benni Mack
Browse files

[TASK] Avoid inline JavaScript generated by BackendUtility:viewOnClick

Inline JavaScript produced by BackendUtility:viewOnClick is substituted
with markup based instructions and static JavaScript event handlers.

// basically delivers window.open(generatedUri)
BackendUtility::viewOnClick($pageId, $backPath, $rootLine, $section,
    $viewUri, $getVars, $switchFocus);

can be substituted with e.g.

\TYPO3\CMS\Backend\Routing\PreviewUriBuilder::create($pageId, $viewUri)
    ->withRootLine($rootLine)
    ->withSection($section)
    ->withAdditionalQueryParameters($getVars)
    ->serializeDispatcherAttributes([
        PreviewUriBuilder::OPTION_SWITCH_FOCUS => $switchFocus,
    ]);

which results in the following HTML data attributes
(data can be retrieved as array of complete element as well)

data-dispatch-action="TYPO3.WindowManager.localOpen"
data-dispatch-args="["https://...",null,"previewWin"]"

Resolves: #91123
Releases: master
Change-Id: Iedd9bfe60827977677ee68e2c948c63e359abf84
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64243

Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent a7bade17
......@@ -14,7 +14,9 @@
import InfoWindow = require('TYPO3/CMS/Backend/InfoWindow');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
import shortcutMenu = require('TYPO3/CMS/Backend/Toolbar/ShortcutMenu');
import windowManager = require('TYPO3/CMS/Backend/WindowManager');
import documentService = require('TYPO3/CMS/Core/DocumentService');
import Utility = require('TYPO3/CMS/Backend/Utility');
/**
* Module: TYPO3/CMS/Backend/ActionDispatcher
......@@ -35,23 +37,14 @@ class ActionDispatcher {
// all other payload values are expected to be serialized to unicode literals
const json = element.dataset.dispatchArgs.replace(/&quot;/g, '"');
const args = JSON.parse(json);
return args instanceof Array ? ActionDispatcher.trimItems(args) : null;
return args instanceof Array ? Utility.trimItems(args) : null;
} else if (element.dataset.dispatchArgsList) {
const args = element.dataset.dispatchArgsList.split(',');
return ActionDispatcher.trimItems(args);
return Utility.trimItems(args);
}
return null;
}
private static trimItems(items: any[]): any[] {
return items.map((item: any) => {
if (item instanceof String) {
return item.trim();
}
return item;
});
}
private static enrichItems(items: any[], evt: Event, target: HTMLElement): any[] {
return items.map((item: any) => {
if (!(item instanceof Object) || !item.$event) {
......@@ -75,6 +68,7 @@ class ActionDispatcher {
this.delegates = {
'TYPO3.InfoWindow.showItem': InfoWindow.showItem.bind(null),
'TYPO3.ShortcutMenu.createShortcut': shortcutMenu.createShortcut.bind(shortcutMenu),
'TYPO3.WindowManager.localOpen': windowManager.localOpen.bind(windowManager),
};
}
......
......@@ -13,6 +13,8 @@
import moduleMenuApp = require('TYPO3/CMS/Backend/ModuleMenu');
import viewportObject = require('TYPO3/CMS/Backend/Viewport');
import windowManager = require('TYPO3/CMS/Backend/WindowManager');
import Utility = require('TYPO3/CMS/Backend/Utility');
/**
* Module: TYPO3/CMS/Backend/Element/ImmediateActionElement
......@@ -25,6 +27,7 @@ import viewportObject = require('TYPO3/CMS/Backend/Viewport');
*/
export class ImmediateActionElement extends HTMLElement {
private action: string;
private args: any[] = [];
private static getDelegate(action: string): Function {
switch (action) {
......@@ -32,6 +35,8 @@ export class ImmediateActionElement extends HTMLElement {
return moduleMenuApp.App.refreshMenu.bind(moduleMenuApp);
case 'TYPO3.Backend.Topbar.refresh':
return viewportObject.Topbar.refresh.bind(viewportObject.Topbar);
case 'TYPO3.WindowManager.localOpen':
return windowManager.localOpen.bind(windowManager);
default:
throw Error('Unknown action "' + action + '"');
}
......@@ -41,7 +46,7 @@ export class ImmediateActionElement extends HTMLElement {
* Observed attributes handled by `attributeChangedCallback`.
*/
public static get observedAttributes(): string[] {
return ['action'];
return ['action', 'args', 'args-list'];
}
/**
......@@ -50,6 +55,15 @@ export class ImmediateActionElement extends HTMLElement {
public attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === 'action') {
this.action = newValue;
} else if (name === 'args') {
// `&quot;` is the only literal of a PHP `json_encode` that needs to be substituted
// all other payload values are expected to be serialized to unicode literals
const json = newValue.replace(/&quot;/g, '"');
const args = JSON.parse(json);
this.args = args instanceof Array ? Utility.trimItems(args) : [];
} else if (name === 'args-list') {
const args = newValue.split(',');
this.args = Utility.trimItems(args);
}
}
......@@ -61,8 +75,7 @@ export class ImmediateActionElement extends HTMLElement {
if (!this.action) {
throw new Error('Missing mandatory action attribute');
}
// @todo similar to ActionDispatcher, it might be required to pass custom arguments
ImmediateActionElement.getDelegate(this.action).apply(null, []);
ImmediateActionElement.getDelegate(this.action).apply(null, this.args);
}
}
......
......@@ -26,6 +26,20 @@ class Utility {
return string.split(delimiter).map((item: string) => item.trim()).filter((item: string) => item !== '');
}
/**
* Trims string items.
*
* @param {string[]|any[]} items
*/
public static trimItems(items: any[]): any[] {
return items.map((item: any) => {
if (item instanceof String) {
return item.trim();
}
return item;
});
}
/**
* Splits a string by a given delimiter and converts the values to integer
*
......
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Module: TYPO3/CMS/Backend/WindowManager
*/
class WindowManager {
private windows: {[key: string]: Window} = {};
// alias for `localOpen`
public open = (...args: any[]): Window => this.localOpen.apply(this, args);
// @todo Not implemented, yet
public globalOpen = (...args: any[]): Window => this.localOpen.apply(this, args);
public localOpen(uri: string, switchFocus?: boolean, windowName: string = 'newTYPO3frontendWindow', windowFeatures: string = ''): Window | null {
if (!uri) {
return null;
}
if (switchFocus === null) {
// @todo Check how this would happen, taken from legacy code
switchFocus = !window.opener;
} else if (switchFocus === undefined) {
switchFocus = true;
}
const existingWindow = this.windows[windowName];
const existingUri = existingWindow instanceof Window && !existingWindow.closed ? existingWindow.location.href : null;
if (existingUri === uri) {
existingWindow.location.reload();
return existingWindow;
}
const newWindow = window.open(uri, windowName, windowFeatures);
this.windows[windowName] = newWindow;
if (switchFocus) {
newWindow.focus();
}
return newWindow;
}
}
const windowManager = new WindowManager();
if (!top.TYPO3.WindowManager) {
if (top.document === window.document) {
// our instance is available in top/global scope
top.TYPO3.WindowManager = windowManager;
} else {
// ensure there is an instance in top/global scope
top.TYPO3.WindowManager = new WindowManager();
}
}
export = windowManager;
......@@ -24,6 +24,7 @@ import Utility = require('TYPO3/CMS/Backend/Utility');
import Viewport = require('TYPO3/CMS/Backend/Viewport');
import Wizard = require('TYPO3/CMS/Backend/Wizard');
import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');
import windowManager = require('TYPO3/CMS/Backend/WindowManager');
enum Identifiers {
searchForm = '#workspace-settings-form',
......@@ -275,7 +276,7 @@ class Backend extends Workspaces {
}).on('click', '[data-action="nextstage"]', (e: JQueryEventObject): void => {
this.sendToStage($(e.currentTarget).closest('tr'), 'next');
}).on('click', '[data-action="changes"]', this.viewChanges)
.on('click', '[data-action="preview"]', this.openPreview)
.on('click', '[data-action="preview"]', this.openPreview.bind(this))
.on('click', '[data-action="open"]', (e: JQueryEventObject): void => {
const row = <HTMLTableRowElement>e.currentTarget.closest('tr');
let newUrl = TYPO3.settings.FormEngine.moduleUrl
......@@ -899,18 +900,18 @@ class Backend extends Workspaces {
/**
* Opens a record in a preview window
*
* @param {Event} e
* @param {JQueryEventObject} evt
*/
private openPreview = (e: JQueryEventObject): void => {
const $tr = $(e.currentTarget).closest('tr');
private openPreview(evt: JQueryEventObject): void {
const $tr = $(evt.currentTarget).closest('tr');
this.sendRemoteRequest(
this.generateRemoteActionsPayload('viewSingleRecord', [
$tr.data('table'), $tr.data('uid'),
]),
).then(async (response: AjaxResponse): Promise<void> => {
// eslint-disable-next-line no-eval
eval((await response.resolve())[0].result);
const previewUri: string = (await response.resolve())[0].result;
windowManager.localOpen(previewUri);
});
}
......
......@@ -22,6 +22,7 @@ declare namespace TYPO3 {
export let Storage: any;
export let Tooltip: any;
export let Utility: any;
export let WindowManager: any;
export let Wizard: any;
export let WorkspacesMenu: any;
export let settings: any;
......
......@@ -25,6 +25,7 @@ use TYPO3\CMS\Backend\Backend\Avatar\Avatar;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
......@@ -621,8 +622,10 @@ class ElementInformationController
// Recordlist button
$actions['webListUrl'] = (string)$uriBuilder->buildUriFromRoute('web_list', ['id' => $uid, 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()]);
// View page button
$actions['viewOnClick'] = BackendUtility::viewOnClick($uid, '', BackendUtility::BEgetRootLine($uid));
$previewUriBuilder = PreviewUriBuilder::create((int)$uid)
->withRootLine(BackendUtility::BEgetRootLine($uid));
// View page button (`previewUrlAttributes` is the substitute for previous `viewOnClick`)
$actions['previewUrlAttributes'] = $previewUriBuilder->serializeDispatcherAttributes();
}
return $actions;
......
......@@ -29,6 +29,7 @@ use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormResultCompiler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
......@@ -186,6 +187,11 @@ class EditDocumentController
*/
protected $viewUrl;
/**
* @var string|null
*/
protected $previewCode;
/**
* Alternative title for the document handler.
*
......@@ -773,10 +779,9 @@ class EditDocumentController
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
$this->moduleTemplate->addJavaScriptCode(
'previewCode',
(isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
);
if (isset($parsedBody['_savedokview']) && $this->popViewId) {
$this->previewCode = $this->generatePreviewCode();
}
// Set context sensitive menu
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
......@@ -785,46 +790,22 @@ class EditDocumentController
}
/**
* Generate the Javascript for opening the preview window
* Generates markup for immediate action dispatching.
*
* @return string
*/
protected function generatePreviewCode(): string
{
$previewPageId = $this->getPreviewPageId();
$previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
$anchorSection = $this->getPreviewUrlAnchorSection();
$previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
$previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
try {
$previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
return '
if (window.opener) {
'
. BackendUtility::viewOnClick(
$previewPageId,
'',
$previewPageRootLine,
$anchorSection,
$this->viewUrl,
$previewUrlParameters,
false
)
. '
} else {
'
. BackendUtility::viewOnClick(
$previewPageId,
'',
$previewPageRootLine,
$anchorSection,
$this->viewUrl,
$previewUrlParameters
)
. '
}';
} catch (UnableToLinkToPageException $e) {
return '';
}
return PreviewUriBuilder::create($previewPageId, $this->viewUrl)
->withRootLine($previewPageRootLine)
->withSection($anchorSection)
->withAdditionalQueryParameters($previewUrlParameters)
->buildImmediateActionElement([PreviewUriBuilder::OPTION_SWITCH_FOCUS => null]);
}
/**
......@@ -998,7 +979,7 @@ class EditDocumentController
*/
protected function main(ServerRequestInterface $request): void
{
$body = '';
$body = $this->previewCode ?? '';
// Begin edit
if (is_array($this->editconf)) {
$this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
......@@ -1020,7 +1001,7 @@ class EditDocumentController
$this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
}
$body = $this->formResultCompiler->addCssFiles();
$body .= $this->formResultCompiler->addCssFiles();
$body .= $this->compileForm($editForm);
$body .= $this->formResultCompiler->printNeededJSFunctions();
$body .= '</form>';
......
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Backend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
......@@ -372,13 +373,12 @@ class NewRecordController
];
}
if (!in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)) {
$previewDataAttributes = PreviewUriBuilder::create($this->pageinfo['uid'])
->withRootLine(BackendUtility::BEgetRootLine($this->pageinfo['uid']))
->buildDispatcherDataAttributes();
$viewButton = $buttonBar->makeLinkButton()
->setHref('#')
->setOnClick(BackendUtility::viewOnClick(
$this->pageinfo['uid'],
'',
BackendUtility::BEgetRootLine($this->pageinfo['uid'])
))
->setDataAttributes($previewDataAttributes ?? [])
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
'actions-view-page',
......
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Backend\Controller\Page;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
......@@ -84,8 +85,11 @@ class NewMultiplePagesController
$cshButton = $buttonBar->makeHelpButton()
->setModuleName('pages_new')
->setFieldName('pages_new');
$previewDataAttributes = PreviewUriBuilder::create($pageUid)
->withRootLine(BackendUtility::BEgetRootLine($pageUid))
->buildDispatcherDataAttributes();
$viewButton = $buttonBar->makeLinkButton()
->setOnClick(BackendUtility::viewOnClick($pageUid, '', BackendUtility::BEgetRootLine($pageUid)))
->setDataAttributes($previewDataAttributes ?? [])
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
->setHref('#');
......
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Backend\Controller\Page;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
......@@ -82,8 +83,11 @@ class SortSubPagesController
$cshButton = $buttonBar->makeHelpButton()
->setModuleName('pages_sort')
->setFieldName('pages_sort');
$previewDataAttributes = PreviewUriBuilder::create($parentPageUid)
->withRootLine(BackendUtility::BEgetRootLine($parentPageUid))
->buildDispatcherDataAttributes();
$viewButton = $buttonBar->makeLinkButton()
->setOnClick(BackendUtility::viewOnClick($parentPageUid, '', BackendUtility::BEgetRootLine($parentPageUid)))
->setDataAttributes($previewDataAttributes ?? [])
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
->setHref('#');
......
......@@ -20,6 +20,7 @@ namespace TYPO3\CMS\Backend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
......@@ -751,16 +752,12 @@ class PageLayoutController
&& !VersionState::cast($this->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
) {
$languageParameter = $this->current_sys_language ? ('&L=' . $this->current_sys_language) : '';
$onClick = BackendUtility::viewOnClick(
$this->pageinfo['uid'],
'',
BackendUtility::BEgetRootLine($this->pageinfo['uid']),
'',
'',
$languageParameter
);
$previewDataAttributes = PreviewUriBuilder::create($this->pageinfo['uid'])
->withRootLine(BackendUtility::BEgetRootLine($this->pageinfo['uid']))
->withAdditionalQueryParameters($languageParameter)
->buildDispatcherDataAttributes();
$viewButton = $this->buttonBar->makeLinkButton()
->setOnClick($onClick)
->setDataAttributes($previewDataAttributes ?? [])
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
->setHref('#');
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Routing;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Substitution for `BackendUtility::viewOnClick` and `BackendUtility::getPreviewUrl`.
* Internally `BackendUtility::getPreviewUrl` is still called due to hooks being invoked
* there - in the future it basically aims to be a replacement for mentioned function.
*/
class PreviewUriBuilder
{
public const OPTION_SWITCH_FOCUS = 'switchFocus';
public const OPTION_WINDOW_NAME = 'windowName';
public const OPTION_WINDOW_FEATURES = 'windowFeatures';
public const OPTION_WINDOW_SCOPE = 'windowScope';
public const OPTION_WINDOW_SCOPE_LOCAL = 'local';
public const OPTION_WINDOW_SCOPE_GLOBAL = 'global';
/**
* @var int
*/
protected $pageId;
/**
* @var string|null
*/
protected $alternativeUri;
/**
* @var array|null
*/
protected $rootLine;
/**
* @var string|null
*/
protected $section;
/**
* @var string|null
*/
protected $additionalQueryParameters;
/**
* @var string|null
* @internal Not used, kept for potential compatibility issues
*/
protected $backPath;
/**
* @var bool
*/
protected $moduleLoading = true;
/**
* @param int $pageId Page ID to be previewed
* @param string|null $alternativeUri Alternative URL to be used instead of `/index.php?id=`
* @return static
*/
public static function create(int $pageId, string $alternativeUri = null): self
{
return GeneralUtility::makeInstance(static::class, $pageId, $alternativeUri);
}
/**
* @param int $pageId Page ID to be previewed
* @param string|null $alternativeUri Alternative URL to be used instead of `/index.php?id=`
*/
public function __construct(int $pageId, string $alternativeUri = null)
{
$this->pageId = $pageId;
$this->alternativeUri = $alternativeUri;
}
/**
* @param bool $moduleLoading whether to enable JavaScript module loading
* @return static
*/
public function withModuleLoading(bool $moduleLoading): self
{
if ($this->moduleLoading === $moduleLoading) {
return $this;
}
$target = clone $this;
$target->moduleLoading = $moduleLoading;