Commit 8a8f1413 authored by Oliver Hader's avatar Oliver Hader Committed by Oliver Bartsch
Browse files

[TASK] Avoid inline JavaScript in backend update signals

BackendUtility::getUpdateSignalCode() returned plain inline JavaScript
code which got replaced by new BackendUtility::getUpdateSignalDetails().

The new function is capable of handling inline JavaScript code (for
backward compatibility reasons) and HTML markup using corresponding
`<typo3-immediate-action>` custom element.

OpendocsToolbarItem::updateNumberOfOpenDocsHook has been adjusted as
well to use the script-less implementation.

Resolves: #96002
Resolves: #96012
Releases: master, 11.5
Change-Id: I248db207600ae5c7452471a093f84f3802050c59
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72206

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
parent bdf9728a
......@@ -12,6 +12,7 @@
*/
import Utility = require('TYPO3/CMS/Backend/Utility');
import {EventDispatcher} from 'TYPO3/CMS/Backend/Event/EventDispatcher';
/**
* Module: TYPO3/CMS/Backend/Element/ImmediateActionElement
......@@ -41,6 +42,8 @@ export class ImmediateActionElement extends HTMLElement {
return (await import('TYPO3/CMS/Backend/Storage/ModuleStateStorage')).ModuleStateStorage.update;
case 'TYPO3.Backend.Storage.ModuleStateStorage.updateWithCurrentMount':
return (await import('TYPO3/CMS/Backend/Storage/ModuleStateStorage')).ModuleStateStorage.updateWithCurrentMount;
case 'TYPO3.Backend.Event.EventDispatcher.dispatchCustomEvent':
return EventDispatcher.dispatchCustomEvent;
default:
throw Error('Unknown action "' + action + '"');
}
......
/*
* 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/Event/EventDispatcher
*/
export class EventDispatcher {
static dispatchCustomEvent(name: string, detail: any = null, useTop: boolean = false): void {
const event = new CustomEvent(name, {detail: detail});
if (!useTop) {
document.dispatchEvent(event);
} else if (typeof top !== 'undefined') {
top.document.dispatchEvent(event);
}
}
}
......@@ -47,6 +47,10 @@ class OpendocsMenu {
}
constructor() {
document.addEventListener(
'typo3:opendocs:updateRequested',
(evt: CustomEvent) => this.updateMenu(),
);
Viewport.Topbar.Toolbar.registerEvent((): void => {
this.initializeEvents();
this.updateMenu();
......
......@@ -31,6 +31,11 @@ class ImmediateActionElement
protected string $action;
protected ?array $args = null;
public static function forAction(string $action): self
{
return new self($action, null);
}
public static function moduleStateUpdate(string $module, $identifier, bool $select = null): self
{
return new self(
......@@ -47,6 +52,14 @@ class ImmediateActionElement
);
}
public static function dispatchCustomEvent(string $name, array $details = null, bool $useTop = false): self
{
return new self(
'TYPO3.Backend.Event.EventDispatcher.dispatchCustomEvent',
[$name, $details, $useTop]
);
}
private function __construct(string $action, ?array $args)
{
$this->action = $action;
......
......@@ -410,7 +410,20 @@ class ModuleTemplate
$this->view->assign('uiBlock', $this->uiBlock);
$this->view->assign('flashMessageQueueIdentifier', $this->flashMessageQueue->getIdentifier());
$this->pageRenderer->addBodyContent($this->bodyTag . $this->view->render());
$this->pageRenderer->addJsFooterInlineCode('updateSignals', BackendUtility::getUpdateSignalCode());
$updateSignalDetails = BackendUtility::getUpdateSignalDetails();
if (!empty($updateSignalDetails['html'])) {
$this->pageRenderer->addHeaderData(
implode("\n", $updateSignalDetails['html'])
);
}
// @todo deprecate inline JavaScript in TYPO3 v12.0
if (!empty($updateSignalDetails['script'])) {
$this->pageRenderer->addJsFooterInlineCode(
'updateSignals',
implode("\n", $updateSignalDetails['script'])
);
}
return $this->pageRenderer->render();
}
......
......@@ -19,6 +19,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
......@@ -2488,13 +2489,13 @@ class BackendUtility
*
* @param string $set Key to set the update signal. When setting, this value contains strings telling WHAT to set. At this point it seems that the value "updatePageTree" is the only one it makes sense to set. If empty, all update signals will be removed.
* @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
* @see BackendUtility::getUpdateSignalCode()
* @see BackendUtility::getUpdateSignalDetails()
*/
public static function setUpdateSignal($set = '', $params = '')
{
$beUser = static::getBackendUserAuthentication();
$modData = $beUser->getModuleData(
\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
BackendUtility::class . '::getUpdateSignal',
'ses'
);
if ($set) {
......@@ -2506,7 +2507,7 @@ class BackendUtility
// clear the module data
$modData = [];
}
$beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
$beUser->pushModuleData(BackendUtility::class . '::getUpdateSignal', $modData);
}
/**
......@@ -2515,12 +2516,13 @@ class BackendUtility
*
* @return string HTML javascript code
* @see BackendUtility::setUpdateSignal()
* @internal use getUpdateSignalDetails() instead, will be deprecated in TYPO3 v12.0
*/
public static function getUpdateSignalCode()
{
$signals = [];
$modData = static::getBackendUserAuthentication()->getModuleData(
\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
BackendUtility::class . '::getUpdateSignal',
'ses'
);
if (empty($modData)) {
......@@ -2570,6 +2572,76 @@ class BackendUtility
return $content;
}
/**
* Gets instructions for update signals (e.g. page tree shall be refreshed,
* since some page title has been modified during the current HTTP request).
*
* @return array{html: list<string>, script: list<string>}
* @see BackendUtility::setUpdateSignal()
*/
public static function getUpdateSignalDetails(): array
{
$details = [
'html' => [],
// @todo deprecate inline JavaScript in TYPO3 v12.0
'script' => [],
];
$modData = static::getBackendUserAuthentication()->getModuleData(
BackendUtility::class . '::getUpdateSignal',
'ses'
);
if (empty($modData)) {
return $details;
}
// Hook: Allows to let TYPO3 execute your JS code
$updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
// Loop through all setUpdateSignals and get the JS code
foreach ($modData as $set => $val) {
if (isset($updateSignals[$set])) {
$params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => '', 'html' => ''];
$ref = null;
GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
// @todo verify and adjust documentation
if (!empty($params['html'])) {
$details['html'][] = $params['html'];
} elseif (!empty($params['JScode'])) {
// @todo deprecate in TYPO3 v12.0, avoid inline JavaScript
$details['script'][] = $params['JScode'];
}
} else {
switch ($set) {
case 'updatePageTree':
$details['html'][] = ImmediateActionElement::dispatchCustomEvent(
'typo3:pagetree:refresh',
null,
true
);
break;
case 'updateFolderTree':
$details['html'][] = ImmediateActionElement::dispatchCustomEvent(
'typo3:filestoragetree:refresh',
null,
true
);
break;
case 'updateModuleMenu':
$details['html'][] = ImmediateActionElement::forAction(
'TYPO3.ModuleMenu.App.refreshMenu',
);
break;
case 'updateTopbar':
$details['html'][] = ImmediateActionElement::forAction(
'TYPO3.Backend.Topbar.refresh'
);
break;
}
}
}
// reset update signals
self::setUpdateSignal();
return $details;
}
/**
* Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
* This is kind of session variable management framework for the backend users.
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __createBinding=this&&this.__createBinding||(Object.create?function(e,t,a,r){void 0===r&&(r=a),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[a]}})}:function(e,t,a,r){void 0===r&&(r=a),e[r]=t[a]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),__importStar=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)"default"!==a&&Object.prototype.hasOwnProperty.call(e,a)&&__createBinding(t,e,a);return __setModuleDefault(t,e),t};define(["require","exports","TYPO3/CMS/Backend/Utility"],(function(e,t,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ImmediateActionElement=void 0;class r extends HTMLElement{constructor(){super(...arguments),this.args=[]}static async getDelegate(t){switch(t){case"TYPO3.ModuleMenu.App.refreshMenu":const a=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/ModuleMenu"],t,a)}).then(__importStar);return a.App.refreshMenu.bind(a.App);case"TYPO3.Backend.Topbar.refresh":const r=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Viewport"],t,a)}).then(__importStar);return r.Topbar.refresh.bind(r.Topbar);case"TYPO3.WindowManager.localOpen":const n=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/WindowManager"],t,a)}).then(__importStar);return n.localOpen.bind(n);case"TYPO3.Backend.Storage.ModuleStateStorage.update":return(await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Storage/ModuleStateStorage"],t,a)}).then(__importStar)).ModuleStateStorage.update;case"TYPO3.Backend.Storage.ModuleStateStorage.updateWithCurrentMount":return(await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Storage/ModuleStateStorage"],t,a)}).then(__importStar)).ModuleStateStorage.updateWithCurrentMount;default:throw Error('Unknown action "'+t+'"')}}static get observedAttributes(){return["action","args","args-list"]}attributeChangedCallback(e,t,r){if("action"===e)this.action=r;else if("args"===e){const e=r.replace(/&quot;/g,'"'),t=JSON.parse(e);this.args=t instanceof Array?a.trimItems(t):[]}else if("args-list"===e){const e=r.split(",");this.args=a.trimItems(e)}}connectedCallback(){if(!this.action)throw new Error("Missing mandatory action attribute");r.getDelegate(this.action).then(e=>e.apply(null,this.args))}}t.ImmediateActionElement=r,window.customElements.define("typo3-immediate-action",r)}));
\ No newline at end of file
var __createBinding=this&&this.__createBinding||(Object.create?function(e,t,a,r){void 0===r&&(r=a),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[a]}})}:function(e,t,a,r){void 0===r&&(r=a),e[r]=t[a]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),__importStar=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)"default"!==a&&Object.prototype.hasOwnProperty.call(e,a)&&__createBinding(t,e,a);return __setModuleDefault(t,e),t};define(["require","exports","TYPO3/CMS/Backend/Utility","TYPO3/CMS/Backend/Event/EventDispatcher"],(function(e,t,a,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ImmediateActionElement=void 0;class n extends HTMLElement{constructor(){super(...arguments),this.args=[]}static async getDelegate(t){switch(t){case"TYPO3.ModuleMenu.App.refreshMenu":const a=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/ModuleMenu"],t,a)}).then(__importStar);return a.App.refreshMenu.bind(a.App);case"TYPO3.Backend.Topbar.refresh":const n=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Viewport"],t,a)}).then(__importStar);return n.Topbar.refresh.bind(n.Topbar);case"TYPO3.WindowManager.localOpen":const i=await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/WindowManager"],t,a)}).then(__importStar);return i.localOpen.bind(i);case"TYPO3.Backend.Storage.ModuleStateStorage.update":return(await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Storage/ModuleStateStorage"],t,a)}).then(__importStar)).ModuleStateStorage.update;case"TYPO3.Backend.Storage.ModuleStateStorage.updateWithCurrentMount":return(await new Promise((t,a)=>{e(["TYPO3/CMS/Backend/Storage/ModuleStateStorage"],t,a)}).then(__importStar)).ModuleStateStorage.updateWithCurrentMount;case"TYPO3.Backend.Event.EventDispatcher.dispatchCustomEvent":return r.EventDispatcher.dispatchCustomEvent;default:throw Error('Unknown action "'+t+'"')}}static get observedAttributes(){return["action","args","args-list"]}attributeChangedCallback(e,t,r){if("action"===e)this.action=r;else if("args"===e){const e=r.replace(/&quot;/g,'"'),t=JSON.parse(e);this.args=t instanceof Array?a.trimItems(t):[]}else if("args-list"===e){const e=r.split(",");this.args=a.trimItems(e)}}connectedCallback(){if(!this.action)throw new Error("Missing mandatory action attribute");n.getDelegate(this.action).then(e=>e.apply(null,this.args))}}t.ImmediateActionElement=n,window.customElements.define("typo3-immediate-action",n)}));
\ No newline at end of file
/*
* 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!
*/
define(["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.EventDispatcher=void 0;t.EventDispatcher=class{static dispatchCustomEvent(e,t=null,n=!1){const s=new CustomEvent(e,{detail:t});n?"undefined"!=typeof top&&top.document.dispatchEvent(s):document.dispatchEvent(s)}}}));
\ No newline at end of file
......@@ -17,6 +17,7 @@ declare(strict_types=1);
namespace TYPO3\CMS\Opendocs\Backend\ToolbarItems;
use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
......@@ -121,13 +122,13 @@ class OpendocsToolbarItem implements ToolbarItemInterface
*
* @param array $params
*/
public function updateNumberOfOpenDocsHook(&$params)
public function updateNumberOfOpenDocsHook(array &$params)
{
$params['JScode'] = '
if (top && top.TYPO3.OpendocsMenu) {
top.TYPO3.OpendocsMenu.updateMenu();
}
';
$params['html'] = ImmediateActionElement::dispatchCustomEvent(
'typo3:opendocs:updateRequested',
null,
true
);
}
/**
......
......@@ -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","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Backend/Icons","TYPO3/CMS/Backend/Viewport","TYPO3/CMS/Backend/Storage/ModuleStateStorage"],(function(e,t,o,n,r,c,a){"use strict";var l;o=__importDefault(o),function(e){e.containerSelector="#typo3-cms-opendocs-backend-toolbaritems-opendocstoolbaritem",e.closeSelector=".t3js-topbar-opendocs-close",e.menuContainerSelector=".dropdown-menu",e.toolbarIconSelector=".toolbar-item-icon .t3js-icon",e.openDocumentsItemsSelector=".t3js-topbar-opendocs-item",e.counterSelector="#tx-opendocs-counter",e.entrySelector=".t3js-open-doc"}(l||(l={}));class s{constructor(){this.hashDataAttributeName="opendocsidentifier",this.toggleMenu=()=>{o.default(".scaffold").removeClass("scaffold-toolbar-expanded"),o.default(l.containerSelector).toggleClass("open")},c.Topbar.Toolbar.registerEvent(()=>{this.initializeEvents(),this.updateMenu()})}static updateNumberOfDocs(){const e=o.default(l.containerSelector).find(l.openDocumentsItemsSelector).length;o.default(l.counterSelector).text(e).toggle(e>0)}updateMenu(){let e=o.default(l.toolbarIconSelector,l.containerSelector),t=e.clone();r.getIcon("spinner-circle-light",r.sizes.small).done(t=>{e.replaceWith(t)}),new n(TYPO3.settings.ajaxUrls.opendocs_menu).get().then(async e=>{o.default(l.containerSelector).find(l.menuContainerSelector).html(await e.resolve()),s.updateNumberOfDocs()}).finally(()=>{o.default(l.toolbarIconSelector,l.containerSelector).replaceWith(t)})}initializeEvents(){o.default(l.containerSelector).on("click",l.closeSelector,e=>{e.preventDefault();const t=o.default(e.currentTarget).data(this.hashDataAttributeName);this.closeDocument(t)}).on("click",l.entrySelector,e=>{e.preventDefault();const t=o.default(e.currentTarget);this.toggleMenu(),a.ModuleStateStorage.updateWithCurrentMount("web",t.data("pid"),!0);document.querySelector("typo3-backend-module-router").setAttribute("endpoint",t.attr("href"))})}closeDocument(e){const t={};e&&(t.md5sum=e),new n(TYPO3.settings.ajaxUrls.opendocs_closedoc).post(t).then(async e=>{o.default(l.menuContainerSelector,l.containerSelector).html(await e.resolve()),s.updateNumberOfDocs(),o.default(l.containerSelector).toggleClass("open")})}}let i;return i=new s,"undefined"!=typeof TYPO3&&(TYPO3.OpendocsMenu=i),i}));
\ No newline at end of file
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","TYPO3/CMS/Backend/Icons","TYPO3/CMS/Backend/Viewport","TYPO3/CMS/Backend/Storage/ModuleStateStorage"],(function(e,t,o,n,r,a,c){"use strict";var l;o=__importDefault(o),function(e){e.containerSelector="#typo3-cms-opendocs-backend-toolbaritems-opendocstoolbaritem",e.closeSelector=".t3js-topbar-opendocs-close",e.menuContainerSelector=".dropdown-menu",e.toolbarIconSelector=".toolbar-item-icon .t3js-icon",e.openDocumentsItemsSelector=".t3js-topbar-opendocs-item",e.counterSelector="#tx-opendocs-counter",e.entrySelector=".t3js-open-doc"}(l||(l={}));class s{constructor(){this.hashDataAttributeName="opendocsidentifier",this.toggleMenu=()=>{o.default(".scaffold").removeClass("scaffold-toolbar-expanded"),o.default(l.containerSelector).toggleClass("open")},document.addEventListener("typo3:opendocs:updateRequested",e=>this.updateMenu()),a.Topbar.Toolbar.registerEvent(()=>{this.initializeEvents(),this.updateMenu()})}static updateNumberOfDocs(){const e=o.default(l.containerSelector).find(l.openDocumentsItemsSelector).length;o.default(l.counterSelector).text(e).toggle(e>0)}updateMenu(){let e=o.default(l.toolbarIconSelector,l.containerSelector),t=e.clone();r.getIcon("spinner-circle-light",r.sizes.small).done(t=>{e.replaceWith(t)}),new n(TYPO3.settings.ajaxUrls.opendocs_menu).get().then(async e=>{o.default(l.containerSelector).find(l.menuContainerSelector).html(await e.resolve()),s.updateNumberOfDocs()}).finally(()=>{o.default(l.toolbarIconSelector,l.containerSelector).replaceWith(t)})}initializeEvents(){o.default(l.containerSelector).on("click",l.closeSelector,e=>{e.preventDefault();const t=o.default(e.currentTarget).data(this.hashDataAttributeName);this.closeDocument(t)}).on("click",l.entrySelector,e=>{e.preventDefault();const t=o.default(e.currentTarget);this.toggleMenu(),c.ModuleStateStorage.updateWithCurrentMount("web",t.data("pid"),!0);document.querySelector("typo3-backend-module-router").setAttribute("endpoint",t.attr("href"))})}closeDocument(e){const t={};e&&(t.md5sum=e),new n(TYPO3.settings.ajaxUrls.opendocs_closedoc).post(t).then(async e=>{o.default(l.menuContainerSelector,l.containerSelector).html(await e.resolve()),s.updateNumberOfDocs(),o.default(l.containerSelector).toggleClass("open")})}}let u;return u=new s,"undefined"!=typeof TYPO3&&(TYPO3.OpendocsMenu=u),u}));
\ No newline at end of file
......@@ -30,6 +30,7 @@ final class DispatchNotificationHook
*/
public function dispatchNotification(&$params)
{
// @todo https://forge.typo3.org/issues/96003
$code = '
// Ensure the event handler is ready and listening to events
top.window.require(["TYPO3/CMS/Redirects/EventHandler"], function() {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment