Commit a669f117 authored by Benjamin Franzke's avatar Benjamin Franzke
Browse files

[TASK] Separate JavaScriptHandler concerns

This change is preparatory refactoring in order to support
native JavaScript modules (ESM) where a module can't be used both
as classic script-src and as ES6 module providing exports at the same
time (this was/is possible for AMD as implemented in #95953):
`export` is an invalid keyword in non-module scripts and cannot be
specified conditionally, that is very different to AMD modules where
`define()` can be called conditionally.

JavaScriptHandler currently has three execution modes:

 * If used as script-scr: Call processItems from text content for:
   * RequireJS configuration
   * Generic JavaScriptItems processing
 * If used as AMD module:
   * Generic JavaScriptItems processing

JavaScriptHandler is now separated into the following three concerns:

JavaScriptItemProcessor
-----------------------
The previous processor "mode" is now available as
streamlined JavaScriptItemProcessor module, in order
to drop type invariance (it requires parsed objects
instead of allowing raw json data or objects)

JavaScriptItemHandler
---------------------
The previous JavaScriptHandler module that processes
text content and calls arbitrary hooks has been
simplified to the one usecase that was actually being used:
Handling JavaScriptItems by parsing the text content.
The extracted JavaScriptItemProcessor is used as backend
for the actual work.

RequireJSConfigHandler
----------------------
The RequireJS configurator is now available as
separate module, as this logic is only needed once and not by
other modules. There is no need to keep that functionality in
JavaScriptItemProcessor, especially as it has been hidden from
AMD modules, which complicated the JavaScriptHandler.
This extraction also actually allows to make use of modules
in JavaScriptHandler and therefore enables the possibility to
share code using JavaScriptItemProcessor.

Also the code has been migrated to TypeScript.
JavaScriptItemHandler and RequireJSConfigHandler do not export
or import code, therefore they are usable as plain targets
for script-tags.

In addition the JavaScriptItemHandler is now executed
asynchronously, which allows the browser to parse DOM
in parallel. (synchronous mode is required for RequireJS
config, but not for loading of requirejs-modules/instructions,
they are asynchronous anyway).

FileClipboardCest is adapted to wait more than 1 second, since the
rerendering of the clipboard (a new iframe request is made) takes
longer than 1 second in some test runs (not locally, but on CI)
(note: the default 1 second wait timeout is configured in
typo3/sysext/core/Tests/Acceptance/Application.suite.yml, but that's
too less in this case).

Releases: main
Resolves: #96476
Related: #95953
Related: #96323
Change-Id: I48a5751cea03537344e925beba365841e0855dde
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72892

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent 4e275727
......@@ -20,7 +20,7 @@ import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import FlexFormContainerContainer from './FlexFormContainerContainer';
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import RegularEvent from 'TYPO3/CMS/Core/Event/RegularEvent';
import javaScriptHandler = require('TYPO3/CMS/Core/JavaScriptHandler');
import {JavaScriptItemProcessor} from 'TYPO3/CMS/Core/JavaScriptItemProcessor';
enum Selectors {
toggleAllSelector = '.t3-form-flexsection-toggle',
......@@ -154,7 +154,8 @@ class FlexFormSectionContainer {
sectionContainer.insertAdjacentElement('beforeend', createdContainer);
if (data.scriptItems instanceof Array && data.scriptItems.length > 0) {
javaScriptHandler.processItems(data.scriptItems, true);
const processor = new JavaScriptItemProcessor();
processor.processItems(data.scriptItems);
}
// @todo deprecate or remove with TYPO3 v12.0
......
......@@ -13,7 +13,7 @@
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import javaScriptHandler = require('TYPO3/CMS/Core/JavaScriptHandler');
import {JavaScriptItemProcessor} from 'TYPO3/CMS/Core/JavaScriptItemProcessor';
import Notification = require('../../Notification');
import Utility = require('../../Utility');
......@@ -120,7 +120,8 @@ export class AjaxDispatcher {
}
if (json.scriptItems instanceof Array && json.scriptItems.length > 0) {
javaScriptHandler.processItems(json.scriptItems, true);
const processor = new JavaScriptItemProcessor();
processor.processItems(json.scriptItems);
}
// @todo deprecate or remove with TYPO3 v12.0
......
/*
* 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!
*/
/**
* This handler is used as client-side counterpart of `\TYPO3\CMS\Core\Page\JavaScriptRenderer`.
*
* @module TYPO3/CMS/Core/JavaScriptItemHandler
* @internal Use in TYPO3 core only, API can change at any time!
*/
if (document.currentScript) {
const scriptElement = document.currentScript;
// extracts JSON payload from `/* [JSON] */` content
const textContent = scriptElement.textContent.replace(/^\s*\/\*\s*|\s*\*\/\s*/g, '')
const items = JSON.parse(textContent);
window.require(['TYPO3/CMS/Core/JavaScriptItemProcessor'], ({JavaScriptItemProcessor}: typeof import('TYPO3/CMS/Core/JavaScriptItemProcessor')) => {
const processor = new JavaScriptItemProcessor();
processor.processItems(items);
});
}
/*
* 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!
*/
/**
* This processor is used as client-side counterpart of `\TYPO3\CMS\Core\Page\JavaScriptItems
*
* @module TYPO3/CMS/Core/JavaScriptItemProcessor
* @internal Use in TYPO3 core only, API can change at any time!
*/
const FLAG_USE_REQUIRE_JS = 1;
const FLAG_USE_TOP_WINDOW = 16;
const deniedProperties = ['__proto__', 'prototype', 'constructor'];
const allowedJavaScriptItemTypes = ['assign', 'invoke', 'instance'];
interface JavaScriptInstruction {
type: string;
assignments?: object;
method?: string;
args: Array<any>;
}
interface JavaScriptItemPayload {
name: string;
flags: number;
exportName?: string;
items: JavaScriptInstruction[];
}
interface JavaScriptItem {
type: string;
payload: JavaScriptItemPayload;
}
function loadModule(payload: JavaScriptItemPayload): Promise<any> {
if (!payload.name) {
throw new Error('JavaScript module name is required');
}
if ((payload.flags & FLAG_USE_REQUIRE_JS) === FLAG_USE_REQUIRE_JS) {
return new Promise((resolve, reject) => {
const windowRef = (payload.flags & FLAG_USE_TOP_WINDOW) === FLAG_USE_TOP_WINDOW ? top.window : window;
windowRef.require(
[payload.name],
(module: any) => resolve(module),
(e: any) => reject(e)
);
});
}
throw new Error('Unknown JavaScript module type')
}
function executeJavaScriptModuleInstruction(json: JavaScriptItemPayload) {
// `name` is required
if (!json.name) {
throw new Error('JavaScript module name is required');
}
if (!json.items) {
loadModule(json);
return;
}
const exportName = json.exportName;
const resolveSubjectRef = (__esModule: any): any => {
return typeof exportName === 'string' ? __esModule[exportName] : __esModule;
}
const items = json.items
.filter((item) => allowedJavaScriptItemTypes.includes(item.type))
.map((item) => {
if (item.type === 'assign') {
return (__esModule: any) => {
const subjectRef = resolveSubjectRef(__esModule);
mergeRecursive(subjectRef, item.assignments);
};
} else if (item.type === 'invoke') {
return (__esModule: any) => {
const subjectRef = resolveSubjectRef(__esModule);
subjectRef[item.method].apply(subjectRef, item.args);
};
} else if (item.type === 'instance') {
return (__esModule: any) => {
// this `null` is `thisArg` scope of `Function.bind`,
// which will be reset when invoking `new`
const args = [null].concat(item.args);
const subjectRef = resolveSubjectRef(__esModule);
new (subjectRef.bind.apply(subjectRef, args));
}
} else {
return (__esModule: any) => {
return;
}
}
});
loadModule(json).then(
(subjectRef) => items.forEach((item) => item.call(null, subjectRef))
);
}
function isObjectInstance(item: any) {
return item instanceof Object && !(item instanceof Array);
}
function mergeRecursive(target: { [key: string]: any }, source: { [key: string]: any }) {
Object.keys(source).forEach((property) => {
if (deniedProperties.indexOf(property) !== -1) {
throw new Error('Property ' + property + ' is not allowed');
}
if (!isObjectInstance(source[property]) || typeof target[property] === 'undefined') {
Object.assign(target, {[property]:source[property]});
} else {
mergeRecursive(target[property], source[property]);
}
});
}
export class JavaScriptItemProcessor {
private invokableNames: string[] = ['globalAssignment', 'javaScriptModuleInstruction'];
/**
* Processes multiple items and delegates to handlers
* (globalAssignment, javaScriptModuleInstruction)
*/
public processItems(items: JavaScriptItem[]) {
items.forEach((item) => this.invoke(item.type, item.payload));
}
private invoke(name: string, data: any) {
if (!this.invokableNames.includes(name) || typeof (this as any)[name] !== 'function') {
throw new Error('Unknown handler name "' + name + '"');
}
(this as any)[name].call(this, data);
}
/**
* Assigns (filtered) variables to `window` object globally.
*/
private globalAssignment(payload: { [key: string]: any }) {
mergeRecursive(window, payload);
}
/**
* Loads and invokes a JavaScript (ESM) or requires.js (AMD) module.
*/
private javaScriptModuleInstruction(payload: JavaScriptItemPayload) {
executeJavaScriptModuleInstruction(payload);
}
}
/*
* 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!
*/
/**
* This handler is used as client-side counterpart of `\TYPO3\CMS\Core\Page\PageRenderer`.
*
* @module TYPO3/CMS/Core/RequireJSConfigHandler
* @internal Use in TYPO3 core only, API can change at any time!
*/
if (document.currentScript) {
// extracts JSON payload from `/* [JSON] */` content
window.require.config(
JSON.parse(document.currentScript.textContent.replace(/^\s*\/\*\s*|\s*\*\/\s*/g, ''))
);
}
......@@ -30,14 +30,6 @@ declare namespace TYPO3 {
export const lang: { [key: string]: string };
export const configuration: any;
export namespace CMS {
export namespace Core {
export class JavaScriptHandler {
public processItems(data: string|any[], isParsed?: boolean): void;
public globalAssignment(data: string|any, isParsed?: boolean): void;
public javaScriptModuleInstruction(data: string|any, isParsed?: boolean): void;
}
}
export namespace Backend {
export class FormEngineValidation {
public USmode: number;
......@@ -123,11 +115,6 @@ declare namespace TBE_EDITOR {
* Current AMD/RequireJS modules are returning *instances* of ad-hoc *classes*, make that known to TypeScript
*/
declare module 'TYPO3/CMS/Core/JavaScriptHandler' {
const _exported: TYPO3.CMS.Core.JavaScriptHandler;
export = _exported;
}
declare module 'TYPO3/CMS/Backend/FormEngineValidation' {
const _exported: TYPO3.CMS.Backend.FormEngineValidation;
export = _exported;
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","bootstrap","jquery","sortablejs","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Core/DocumentService","./FlexFormContainerContainer","TYPO3/CMS/Backend/FormEngine","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Core/JavaScriptHandler"],(function(require,exports,bootstrap_1,jquery_1,sortablejs_1,AjaxRequest_1,DocumentService,FlexFormContainerContainer_1,FormEngine,RegularEvent_1,javaScriptHandler){"use strict";var Selectors;jquery_1=__importDefault(jquery_1),sortablejs_1=__importDefault(sortablejs_1),AjaxRequest_1=__importDefault(AjaxRequest_1),FlexFormContainerContainer_1=__importDefault(FlexFormContainerContainer_1),RegularEvent_1=__importDefault(RegularEvent_1),function(e){e.toggleAllSelector=".t3-form-flexsection-toggle",e.addContainerSelector=".t3js-flex-container-add",e.actionFieldSelector=".t3js-flex-control-action",e.sectionContainerSelector=".t3js-flex-section",e.sectionContentContainerSelector=".t3js-flex-section-content",e.sortContainerButtonSelector=".t3js-sortable-handle"}(Selectors||(Selectors={}));class FlexFormSectionContainer{constructor(e){this.allowRestructure=!1,this.flexformContainerContainers=[],this.updateSorting=e=>{this.container.querySelectorAll(Selectors.actionFieldSelector).forEach((e,t)=>{e.value=t.toString()}),this.updateToggleAllState(),this.flexformContainerContainers.splice(e.newIndex,0,this.flexformContainerContainers.splice(e.oldIndex,1)[0]),document.dispatchEvent(new Event("formengine:flexform:sorting-changed"))},this.sectionContainerId=e,DocumentService.ready().then(t=>{this.container=t.getElementById(e),this.sectionContainer=this.container.querySelector(this.container.dataset.section),this.allowRestructure="1"===this.sectionContainer.dataset.t3FlexAllowRestructure,this.registerEvents(),this.registerContainers()})}static getCollapseInstance(e){var t;return null!==(t=bootstrap_1.Collapse.getInstance(e))&&void 0!==t?t:new bootstrap_1.Collapse(e,{toggle:!1})}getContainer(){return this.container}isRestructuringAllowed(){return this.allowRestructure}registerEvents(){this.allowRestructure&&(this.registerSortable(),this.registerContainerDeleted()),this.registerToggleAll(),this.registerCreateNewContainer(),this.registerPanelToggle()}registerContainers(){const e=this.container.querySelectorAll(Selectors.sectionContainerSelector);for(let t of e)this.flexformContainerContainers.push(new FlexFormContainerContainer_1.default(this,t));this.updateToggleAllState()}getToggleAllButton(){return this.container.querySelector(Selectors.toggleAllSelector)}registerSortable(){new sortablejs_1.default(this.sectionContainer,{group:this.sectionContainer.id,handle:Selectors.sortContainerButtonSelector,onSort:this.updateSorting})}registerToggleAll(){new RegularEvent_1.default("click",e=>{const t="true"===e.target.dataset.expandAll,n=this.container.querySelectorAll(Selectors.sectionContentContainerSelector);for(let e of n)t?FlexFormSectionContainer.getCollapseInstance(e).show():FlexFormSectionContainer.getCollapseInstance(e).hide()}).bindTo(this.getToggleAllButton())}registerCreateNewContainer(){new RegularEvent_1.default("click",(e,t)=>{e.preventDefault(),this.createNewContainer(t.dataset)}).delegateTo(this.container,Selectors.addContainerSelector)}createNewContainer(dataset){new AjaxRequest_1.default(TYPO3.settings.ajaxUrls.record_flex_container_add).post({vanillaUid:dataset.vanillauid,databaseRowUid:dataset.databaserowuid,command:dataset.command,tableName:dataset.tablename,fieldName:dataset.fieldname,recordTypeValue:dataset.recordtypevalue,dataStructureIdentifier:JSON.parse(dataset.datastructureidentifier),flexFormSheetName:dataset.flexformsheetname,flexFormFieldName:dataset.flexformfieldname,flexFormContainerName:dataset.flexformcontainername}).then(async response=>{const data=await response.resolve(),createdContainer=(new DOMParser).parseFromString(data.html,"text/html").body.firstElementChild;this.flexformContainerContainers.push(new FlexFormContainerContainer_1.default(this,createdContainer));const sectionContainer=document.querySelector(dataset.target);sectionContainer.insertAdjacentElement("beforeend",createdContainer),data.scriptItems instanceof Array&&data.scriptItems.length>0&&javaScriptHandler.processItems(data.scriptItems,!0),data.scriptCall&&data.scriptCall.length>0&&jquery_1.default.each(data.scriptCall,(function(index,value){eval(value)})),data.stylesheetFiles&&data.stylesheetFiles.length>0&&jquery_1.default.each(data.stylesheetFiles,(function(e,t){let n=document.createElement("link");n.rel="stylesheet",n.type="text/css",n.href=t,document.head.appendChild(n)})),this.updateToggleAllState(),FormEngine.reinitialize(),FormEngine.Validation.initializeInputFields(),FormEngine.Validation.validate(sectionContainer)})}registerContainerDeleted(){new RegularEvent_1.default("formengine:flexform:container-deleted",e=>{const t=e.detail.containerId;this.flexformContainerContainers=this.flexformContainerContainers.filter(e=>e.getStatus().id!==t),FormEngine.Validation.validate(this.container),this.updateToggleAllState()}).bindTo(this.container)}registerPanelToggle(){["hide.bs.collapse","show.bs.collapse"].forEach(e=>{new RegularEvent_1.default(e,()=>{this.updateToggleAllState()}).delegateTo(this.container,Selectors.sectionContentContainerSelector)})}updateToggleAllState(){if(this.flexformContainerContainers.length>0){const e=this.flexformContainerContainers.find(Boolean);this.getToggleAllButton().dataset.expandAll=!0===e.getStatus().collapsed?"true":"false"}}}return FlexFormSectionContainer}));
\ No newline at end of file
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","bootstrap","jquery","sortablejs","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Core/DocumentService","./FlexFormContainerContainer","TYPO3/CMS/Backend/FormEngine","TYPO3/CMS/Core/Event/RegularEvent","TYPO3/CMS/Core/JavaScriptItemProcessor"],(function(require,exports,bootstrap_1,jquery_1,sortablejs_1,AjaxRequest_1,DocumentService,FlexFormContainerContainer_1,FormEngine,RegularEvent_1,JavaScriptItemProcessor_1){"use strict";var Selectors;jquery_1=__importDefault(jquery_1),sortablejs_1=__importDefault(sortablejs_1),AjaxRequest_1=__importDefault(AjaxRequest_1),FlexFormContainerContainer_1=__importDefault(FlexFormContainerContainer_1),RegularEvent_1=__importDefault(RegularEvent_1),function(e){e.toggleAllSelector=".t3-form-flexsection-toggle",e.addContainerSelector=".t3js-flex-container-add",e.actionFieldSelector=".t3js-flex-control-action",e.sectionContainerSelector=".t3js-flex-section",e.sectionContentContainerSelector=".t3js-flex-section-content",e.sortContainerButtonSelector=".t3js-sortable-handle"}(Selectors||(Selectors={}));class FlexFormSectionContainer{constructor(e){this.allowRestructure=!1,this.flexformContainerContainers=[],this.updateSorting=e=>{this.container.querySelectorAll(Selectors.actionFieldSelector).forEach((e,t)=>{e.value=t.toString()}),this.updateToggleAllState(),this.flexformContainerContainers.splice(e.newIndex,0,this.flexformContainerContainers.splice(e.oldIndex,1)[0]),document.dispatchEvent(new Event("formengine:flexform:sorting-changed"))},this.sectionContainerId=e,DocumentService.ready().then(t=>{this.container=t.getElementById(e),this.sectionContainer=this.container.querySelector(this.container.dataset.section),this.allowRestructure="1"===this.sectionContainer.dataset.t3FlexAllowRestructure,this.registerEvents(),this.registerContainers()})}static getCollapseInstance(e){var t;return null!==(t=bootstrap_1.Collapse.getInstance(e))&&void 0!==t?t:new bootstrap_1.Collapse(e,{toggle:!1})}getContainer(){return this.container}isRestructuringAllowed(){return this.allowRestructure}registerEvents(){this.allowRestructure&&(this.registerSortable(),this.registerContainerDeleted()),this.registerToggleAll(),this.registerCreateNewContainer(),this.registerPanelToggle()}registerContainers(){const e=this.container.querySelectorAll(Selectors.sectionContainerSelector);for(let t of e)this.flexformContainerContainers.push(new FlexFormContainerContainer_1.default(this,t));this.updateToggleAllState()}getToggleAllButton(){return this.container.querySelector(Selectors.toggleAllSelector)}registerSortable(){new sortablejs_1.default(this.sectionContainer,{group:this.sectionContainer.id,handle:Selectors.sortContainerButtonSelector,onSort:this.updateSorting})}registerToggleAll(){new RegularEvent_1.default("click",e=>{const t="true"===e.target.dataset.expandAll,n=this.container.querySelectorAll(Selectors.sectionContentContainerSelector);for(let e of n)t?FlexFormSectionContainer.getCollapseInstance(e).show():FlexFormSectionContainer.getCollapseInstance(e).hide()}).bindTo(this.getToggleAllButton())}registerCreateNewContainer(){new RegularEvent_1.default("click",(e,t)=>{e.preventDefault(),this.createNewContainer(t.dataset)}).delegateTo(this.container,Selectors.addContainerSelector)}createNewContainer(dataset){new AjaxRequest_1.default(TYPO3.settings.ajaxUrls.record_flex_container_add).post({vanillaUid:dataset.vanillauid,databaseRowUid:dataset.databaserowuid,command:dataset.command,tableName:dataset.tablename,fieldName:dataset.fieldname,recordTypeValue:dataset.recordtypevalue,dataStructureIdentifier:JSON.parse(dataset.datastructureidentifier),flexFormSheetName:dataset.flexformsheetname,flexFormFieldName:dataset.flexformfieldname,flexFormContainerName:dataset.flexformcontainername}).then(async response=>{const data=await response.resolve(),createdContainer=(new DOMParser).parseFromString(data.html,"text/html").body.firstElementChild;this.flexformContainerContainers.push(new FlexFormContainerContainer_1.default(this,createdContainer));const sectionContainer=document.querySelector(dataset.target);if(sectionContainer.insertAdjacentElement("beforeend",createdContainer),data.scriptItems instanceof Array&&data.scriptItems.length>0){const e=new JavaScriptItemProcessor_1.JavaScriptItemProcessor;e.processItems(data.scriptItems)}data.scriptCall&&data.scriptCall.length>0&&jquery_1.default.each(data.scriptCall,(function(index,value){eval(value)})),data.stylesheetFiles&&data.stylesheetFiles.length>0&&jquery_1.default.each(data.stylesheetFiles,(function(e,t){let n=document.createElement("link");n.rel="stylesheet",n.type="text/css",n.href=t,document.head.appendChild(n)})),this.updateToggleAllState(),FormEngine.reinitialize(),FormEngine.Validation.initializeInputFields(),FormEngine.Validation.validate(sectionContainer)})}registerContainerDeleted(){new RegularEvent_1.default("formengine:flexform:container-deleted",e=>{const t=e.detail.containerId;this.flexformContainerContainers=this.flexformContainerContainers.filter(e=>e.getStatus().id!==t),FormEngine.Validation.validate(this.container),this.updateToggleAllState()}).bindTo(this.container)}registerPanelToggle(){["hide.bs.collapse","show.bs.collapse"].forEach(e=>{new RegularEvent_1.default(e,()=>{this.updateToggleAllState()}).delegateTo(this.container,Selectors.sectionContentContainerSelector)})}updateToggleAllState(){if(this.flexformContainerContainers.length>0){const e=this.flexformContainerContainers.find(Boolean);this.getToggleAllButton().dataset.expandAll=!0===e.getStatus().collapsed?"true":"false"}}}return FlexFormSectionContainer}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Core/JavaScriptHandler","../../Notification","../../Utility"],(function(require,exports,AjaxRequest,javaScriptHandler,Notification,Utility){"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AjaxDispatcher=void 0;class AjaxDispatcher{constructor(e){this.objectGroup=null,this.objectGroup=e}newRequest(e){return new AjaxRequest(e)}getEndpoint(e){if(void 0!==TYPO3.settings.ajaxUrls[e])return TYPO3.settings.ajaxUrls[e];throw'Undefined endpoint for route "'+e+'"'}send(e,t){const s=e.post(this.createRequestBody(t)).then(async e=>this.processResponse(await e.resolve()));return s.catch(e=>{Notification.error("Error "+e.message)}),s}createRequestBody(e){const t={};for(let s=0;s<e.length;s++)t["ajax["+s+"]"]=e[s];return t["ajax[context]"]=JSON.stringify(this.getContext()),t}getContext(){let e;return void 0!==TYPO3.settings.FormEngineInline.config[this.objectGroup]&&void 0!==TYPO3.settings.FormEngineInline.config[this.objectGroup].context&&(e=TYPO3.settings.FormEngineInline.config[this.objectGroup].context),e}processResponse(json){if(json.hasErrors)for(const e of json.messages)Notification.error(e.title,e.message);if(json.stylesheetFiles)for(const[e,t]of json.stylesheetFiles.entries()){if(!t)break;const s=document.createElement("link");s.rel="stylesheet",s.type="text/css",s.href=t,document.querySelector("head").appendChild(s),delete json.stylesheetFiles[e]}if("object"==typeof json.inlineData&&(TYPO3.settings.FormEngineInline=Utility.mergeDeep(TYPO3.settings.FormEngineInline,json.inlineData)),json.scriptItems instanceof Array&&json.scriptItems.length>0&&javaScriptHandler.processItems(json.scriptItems,!0),"object"==typeof json.requireJsModules)for(let e of json.requireJsModules)new Function(e)();if(json.scriptCall&&json.scriptCall.length>0)for(const scriptCall of json.scriptCall)eval(scriptCall);return json}}exports.AjaxDispatcher=AjaxDispatcher}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Core/JavaScriptItemProcessor","../../Notification","../../Utility"],(function(require,exports,AjaxRequest,JavaScriptItemProcessor_1,Notification,Utility){"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AjaxDispatcher=void 0;class AjaxDispatcher{constructor(e){this.objectGroup=null,this.objectGroup=e}newRequest(e){return new AjaxRequest(e)}getEndpoint(e){if(void 0!==TYPO3.settings.ajaxUrls[e])return TYPO3.settings.ajaxUrls[e];throw'Undefined endpoint for route "'+e+'"'}send(e,t){const s=e.post(this.createRequestBody(t)).then(async e=>this.processResponse(await e.resolve()));return s.catch(e=>{Notification.error("Error "+e.message)}),s}createRequestBody(e){const t={};for(let s=0;s<e.length;s++)t["ajax["+s+"]"]=e[s];return t["ajax[context]"]=JSON.stringify(this.getContext()),t}getContext(){let e;return void 0!==TYPO3.settings.FormEngineInline.config[this.objectGroup]&&void 0!==TYPO3.settings.FormEngineInline.config[this.objectGroup].context&&(e=TYPO3.settings.FormEngineInline.config[this.objectGroup].context),e}processResponse(json){if(json.hasErrors)for(const e of json.messages)Notification.error(e.title,e.message);if(json.stylesheetFiles)for(const[e,t]of json.stylesheetFiles.entries()){if(!t)break;const s=document.createElement("link");s.rel="stylesheet",s.type="text/css",s.href=t,document.querySelector("head").appendChild(s),delete json.stylesheetFiles[e]}if("object"==typeof json.inlineData&&(TYPO3.settings.FormEngineInline=Utility.mergeDeep(TYPO3.settings.FormEngineInline,json.inlineData)),json.scriptItems instanceof Array&&json.scriptItems.length>0){const e=new JavaScriptItemProcessor_1.JavaScriptItemProcessor;e.processItems(json.scriptItems)}if("object"==typeof json.requireJsModules)for(let e of json.requireJsModules)new Function(e)();if(json.scriptCall&&json.scriptCall.length>0)for(const scriptCall of json.scriptCall)eval(scriptCall);return json}}exports.AjaxDispatcher=AjaxDispatcher}));
\ No newline at end of file
......@@ -24,12 +24,11 @@ class JavaScriptRenderer
{
protected string $handlerUri;
protected JavaScriptItems $items;
protected ?RequireJS $requireJS = null;
public static function create(string $uri = null): self
{
$uri ??= PathUtility::getAbsoluteWebPath(
GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/JavaScript/JavaScriptHandler.js')
GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/JavaScript/JavaScriptItemHandler.js')
);
return GeneralUtility::makeInstance(static::class, $uri);
}
......@@ -40,11 +39,6 @@ class JavaScriptRenderer
$this->items = GeneralUtility::makeInstance(JavaScriptItems::class);
}
public function loadRequireJS(RequireJS $requireJS): void
{
$this->requireJS = $requireJS;
}
public function addGlobalAssignment(array $payload): void
{
$this->items->addGlobalAssignment($payload);
......@@ -64,17 +58,7 @@ class JavaScriptRenderer
if ($this->isEmpty()) {
return [];
}
$items = [];
if ($this->requireJS !== null) {
$items[] = [
'type' => 'loadRequireJs',
'payload' => $this->requireJS,
];
}
foreach ($this->items->toArray() as $item) {
$items[] = $item;
}
return $items;
return $this->items->toArray();
}
public function render(): string
......@@ -84,13 +68,13 @@ class JavaScriptRenderer
}
return $this->createScriptElement([
'src' => $this->handlerUri,
'data-process-text-content' => 'processItems',
'async' => 'async',
], $this->jsonEncode($this->toArray()));
}
protected function isEmpty(): bool
{
return $this->requireJS === null && $this->items->isEmpty();
return $this->items->isEmpty();
}
protected function createScriptElement(array $attributes, string $textContent = ''): string
......
......@@ -1534,26 +1534,24 @@ class PageRenderer implements SingletonInterface
$this->requireJsConfig
);
}
$requireJS = RequireJS::create(
$this->processJsFile($this->requireJsPath . 'require.js'),
$requireJsConfig
);
$requireJsUri = $this->processJsFile($this->requireJsPath . 'require.js');
// add (probably filtered) RequireJS configuration
if ($this->getApplicationType() === 'BE') {
$html .= sprintf(
'<script src="%s"></script>' . "\n",
htmlspecialchars($requireJS->getUri())
htmlspecialchars($requireJsUri)
);
$html .= sprintf(
'<script src="%s">/* %s */</script>' . "\n",
htmlspecialchars($this->processJsFile('EXT:core/Resources/Public/JavaScript/RequireJSConfigHandler.js')),
(string)json_encode($requireJsConfig, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG)
);
// (using dedicated instance of JavaScriptRenderer)
$javaScriptRenderer = JavaScriptRenderer::create();
$javaScriptRenderer->loadRequireJS($requireJS);
$html .= $javaScriptRenderer->render();
} else {
$html .= GeneralUtility::wrapJS('var require = ' . json_encode($requireJS->getConfig())) . LF;
$html .= GeneralUtility::wrapJS('var require = ' . json_encode($requireJsConfig)) . LF;
// directly after that, include the require.js file
$html .= sprintf(
'<script src="%s"></script>' . "\n",
htmlspecialchars($requireJS->getUri())
htmlspecialchars($requireJsUri)
);
}
// use (anonymous require.js loader), e.g. used when not having a valid TYP3 backend user session
......
<?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\Core\Page;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class RequireJS implements \JsonSerializable
{
protected string $uri;
protected array $config;
public static function create(string $uri, array $config): self
{
return GeneralUtility::makeInstance(self::class, $uri, $config);
}
/**
* @param string $uri URI to load require.js implementation
* @param array $config require.js initialization configuration
*/
public function __construct(string $uri, array $config)
{
$this->uri = $uri;
$this->config = $config;
}
public function jsonSerialize(): array
{
return [
'uri' => $this->uri,
'config' => $this->config,
];
}
/**
* @return string
*/
public function getUri(): string
{
return $this->uri;
}
/**
* @return array
*/
public function getConfig(): array
{
return $this->config;
}
}
......@@ -86,7 +86,7 @@ allow to dispatch actions, without actually using inline JavaScript.
* :php:`\TYPO3\CMS\Core\Page\JavaScriptModuleInstruction` rendered using
:php:`\TYPO3\CMS\Core\Page\JavaScriptRenderer::render`, which uses a script helper
that loads JavaScript modules and invokes a method or assigns variables globally, e.g.
:html:`<script src="/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js" ...>`
:html:`<script src="/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptItemHandler.js" ...>`
**Side-note**: Just using markup like :html:`<script>alert(1)</script>`
is **not** considered a good solution as it still contains inline JavaScript.
......
/*
* 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!
*/
/**
* This handler is used as client-side counterpart of `\TYPO3\CMS\Core\Page\JavaScriptRenderer`.
* It either can be used standalone or as requireJS module internally.
*
* @module TYPO3/CMS/Core/JavaScriptHandler
* @internal Use in TYPO3 core only, API can change at any time!
*/
(function() {
"use strict";
// @todo Handle document.currentScript.async
if (!document.currentScript) {
return false;
}
const FLAG_LOAD_REQUIRE_JS = 1;
const FLAG_USE_TOP_WINDOW = 16;
const deniedProperties = ['__proto__', 'prototype', 'constructor'];
const allowedRequireJsItemTypes = ['assign', 'invoke', 'instance'];
const allowedRequireJsNames = ['globalAssignment', 'javaScriptModuleInstruction'];
const allowedDirectNames = ['processTextContent', 'loadRequireJs', 'processItems', 'globalAssignment', 'javaScriptModuleInstruction'];
const scriptElement = document.currentScript;
class JavaScriptHandler {
/**
* @param {any} json
* @param {string} json.name module name
* @param {string} json.exportName? name used internally to export the module
* @param {array<{type: string, assignments?: object, method?: string, args: array}>} json.items
*/
static loadRequireJsModule(json) {
// `name` is required
if (!json.name) {
throw new Error('RequireJS module name is required');
}
const windowRef = (json.flags & FLAG_USE_TOP_WINDOW) === FLAG_USE_TOP_WINDOW ? top.window : window;