Commit e4a3762a authored by Oliver Hader's avatar Oliver Hader Committed by Andreas Fernandez
Browse files

[FEATURE] Introduce Broadcast Messaging

This change introduces BroadcastChannel in order to communicate between
frames. Messages are converted to according CustomEvents that can be
handled individually. Event handling happens in the most specific scope
on client side.

A polyfill to support Edge has been installed, executed command:

  yarn add broadcastchannel-polyfill

Resolves: #89244
Releases: master
Change-Id: Iab55bf78ff9324d19d115022464c24eea1b8b78e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61788


Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Frank Nägler's avatarFrank Nägler <frank.naegler@typo3.org>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Frank Nägler's avatarFrank Nägler <frank.naegler@typo3.org>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent 3073826d
......@@ -440,6 +440,7 @@ module.exports = function (grunt) {
/* disabled for removed sourcemap reference in file
'taboverride.min.js': 'taboverride/build/output/taboverride.min.js',
*/
'broadcastchannel-polyfill.js': 'broadcastchannel-polyfill/index.js',
'bootstrap-slider.min.js': 'bootstrap-slider/dist/bootstrap-slider.min.js',
/* disabled until events are not bound to document only
see https://github.com/claviska/jquery-minicolors/issues/192
......@@ -489,6 +490,7 @@ module.exports = function (grunt) {
},
thirdparty: {
files: {
"<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel-polyfill.js": ["<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel-polyfill.js"],
"<%= paths.core %>Public/JavaScript/Contrib/require.js": ["<%= paths.core %>Public/JavaScript/Contrib/require.js"],
"<%= paths.core %>Public/JavaScript/Contrib/nprogress.js": ["<%= paths.core %>Public/JavaScript/Contrib/nprogress.js"],
"<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js"],
......
/*
* 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!
*/
export class BroadcastMessage {
readonly componentName: string;
readonly eventName: string;
readonly payload: any;
public static fromData(data: any): BroadcastMessage {
let payload = Object.assign({}, data);
delete payload.componentName;
delete payload.eventName;
return new BroadcastMessage(
data.componentName,
data.eventName,
payload,
);
}
constructor(componentName: string, eventName: string, payload: any) {
if (!componentName || !eventName) {
throw new Error('Properties componentName and eventName have to be defined');
}
this.componentName = componentName;
this.eventName = eventName;
this.payload = payload || {};
}
public createCustomEvent(scope: string = 'typo3'): CustomEvent {
return new CustomEvent(
[scope, this.componentName, this.eventName].join(':'),
{ detail: this.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!
*/
import 'broadcastchannel';
import {BroadcastMessage} from 'TYPO3/CMS/Backend/BroadcastMessage';
import {MessageUtility} from 'TYPO3/CMS/Backend/Utility/MessageUtility';
class BroadcastService {
private readonly channel: BroadcastChannel;
public constructor() {
this.channel = new BroadcastChannel('typo3');
}
public listen(): void {
this.channel.onmessage = (evt: MessageEvent) => {
if (!MessageUtility.verifyOrigin(evt.origin)) {
throw 'Denied message sent by ' + evt.origin;
}
const message = BroadcastMessage.fromData(evt.data);
document.dispatchEvent(message.createCustomEvent('typo3'));
};
}
public post(message: BroadcastMessage): void {
this.channel.postMessage(message);
}
}
export = new BroadcastService();
......@@ -17,16 +17,15 @@ export class MessageUtility {
*
* @return {string}
*/
public static getUrl(): string {
const url = new URL(window.location.href);
return url.origin;
public static getOrigin(): string {
return window.origin;
}
/**
* @param {string} receivedOrigin
*/
public static verifyOrigin(receivedOrigin: string): boolean {
const currentDomain = MessageUtility.getUrl();
const currentDomain = MessageUtility.getOrigin();
return currentDomain === receivedOrigin;
}
......@@ -36,6 +35,6 @@ export class MessageUtility {
* @param {Window} windowObject
*/
public static send(message: any, windowObject: Window = window): void {
windowObject.postMessage(message, MessageUtility.getUrl());
windowObject.postMessage(message, MessageUtility.getOrigin());
}
}
......@@ -95,6 +95,7 @@
},
"dependencies": {
"anymatch": "^1.3.0",
"broadcastchannel-polyfill": "^1.0.0",
"request": "^2.81.0"
}
}
......@@ -974,6 +974,11 @@ braces@^3.0.2:
dependencies:
fill-range "^7.0.1"
broadcastchannel-polyfill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/broadcastchannel-polyfill/-/broadcastchannel-polyfill-1.0.0.tgz#3596eb8143d2446b349430fe6f146b6463902042"
integrity sha512-tYZ4RhUbWiK9XInSgTEWLzsVi2rERw2AQiPUPvpsXnuV8oNcceWPQJ58eSurQTZ+W3FQiVgp7hooMPlT+DDueA==
browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.7.6:
version "1.7.7"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
......
......@@ -114,6 +114,9 @@ class BackendController
LoginRefresh.initialize();
}');
// load BroadcastService
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/BroadcastService', 'function(service) { service.listen(); }');
// load module menu
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ModuleMenu');
......
/*
* 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});class n{constructor(e,t,n){if(!e||!t)throw new Error("Properties componentName and eventName have to be defined");this.componentName=e,this.eventName=t,this.payload=n||{}}static fromData(e){let t=Object.assign({},e);return delete t.componentName,delete t.eventName,new n(e.componentName,e.eventName,t)}createCustomEvent(e="typo3"){return new CustomEvent([e,this.componentName,this.eventName].join(":"),{detail:this.payload})}}t.BroadcastMessage=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","TYPO3/CMS/Backend/BroadcastMessage","TYPO3/CMS/Backend/Utility/MessageUtility","broadcastchannel"],function(e,t,s,n){"use strict";return new class{constructor(){this.channel=new BroadcastChannel("typo3")}listen(){this.channel.onmessage=(e=>{if(!n.MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;const t=s.BroadcastMessage.fromData(e.data);document.dispatchEvent(t.createCustomEvent("typo3"))})}post(e){this.channel.postMessage(e)}}});
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});class i{static getUrl(){return new URL(window.location.href).origin}static verifyOrigin(e){return i.getUrl()===e}static send(e,t=window){t.postMessage(e,i.getUrl())}}t.MessageUtility=i});
\ No newline at end of file
define(["require","exports"],function(e,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});class t{static getOrigin(){return window.origin}static verifyOrigin(e){return t.getOrigin()===e}static send(e,i=window){i.postMessage(e,t.getOrigin())}}i.MessageUtility=t});
\ No newline at end of file
......@@ -1336,6 +1336,7 @@ class PageRenderer implements SingletonInterface
'jquery/autocomplete' => $corePath . 'jquery.autocomplete',
'd3' => $corePath . 'd3/d3',
'Sortable' => $corePath . 'Sortable.min',
'broadcastchannel' => $corePath . '/broadcastchannel-polyfill',
];
$requireJsConfig['public']['waitSeconds'] = 30;
$requireJsConfig['public']['typo3BaseUrl'] = false;
......
.. include:: ../../Includes.txt
==================================================
Feature: #89244 - Broadcast Channels and Messaging
==================================================
See :issue:`89244`
Description
===========
It is now possible to send broadcast messages from anywhere in TYPO3 that are listened to via JavaScript.
.. warning::
This API is considered internal and may change anytime until declared being stable.
Send a message
--------------
Any backend module may send a message using the :js:`TYPO3/CMS/Backend/BroadcastService` module.
The payload of such message is an object that consists at least of the following properties:
* :js:`componentName` - the name of the component that sends the message (e.g. extension name)
* :js:`eventName` - the event name used to identify the message
A message may contain any other property as necessary. The final event name to listen is a composition of "typo3", the
component name and the event name, e.g. `typo3:my_extension:my_event`.
.. attention::
Since a polyfill is in place to add support for Microsoft Edge, the payload must contain JSON-serializable content
only.
To send a message, the :js:`post()` method must be used.
Example code:
.. code-block:: js
require(['TYPO3/CMS/Backend/BroadcastService'], function (BroadcastService) {
const payload = {
componentName: 'my_extension',
eventName: 'my_event',
hello: 'world',
foo: ['bar', 'baz']
};
BroadcastService.post(payload);
});
Receive a message
-----------------
To receive and thus react on a message, an event handler needs to be registered that listens to the composed event
name (e.g. `typo3:my_component:my_event`) sent to :js:`document`.
The event itself contains a property called `detail` **excluding** the component name and event name.
Example code:
.. code-block:: js
define([], function() {
document.addEventListener('typo3:my_component:my_event', (e) => eventHandler(e.detail));
function eventHandler(detail) {
console.log(detail); // contains 'hello' and 'foo' as sent in the payload
}
});
Hook into :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess']` to load a custom
:php:`BackendController` hook that loads the event handler, e.g. via RequireJS.
Example code:
.. code-block:: php
// ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess'][]
= \Vendor\MyExtension\Hooks\BackendControllerHook::class . '->registerClientSideEventHandler';
// Classes/Hooks/BackendControllerHook.php
class BackendControllerHook
{
public function registerClientSideEventHandler(): void
{
$pageRenderer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Page\PageRenderer::class);
$pageRenderer->loadRequireJsModule('TYPO3/CMS/MyExtension/EventHandler');
}
}
.. index:: Backend, JavaScript, ext:backend
!function(t){var e=[];function s(s){var n=this,r="$BroadcastChannel$"+(s=String(s))+"$";e[r]=e[r]||[],e[r].push(this),this._name=s,this._id=r,this._closed=!1,this._mc=new MessageChannel,this._mc.port1.start(),this._mc.port2.start(),t.addEventListener("storage",function(e){if(e.storageArea===t.localStorage&&null!==e.newValue&&e.key.substring(0,r.length)===r){var s=JSON.parse(e.newValue);n._mc.port2.postMessage(s)}})}s.prototype={get name(){return this._name},postMessage:function(s){var n=this;if(this._closed){var r=new Error;throw r.name="InvalidStateError",r}var i=JSON.stringify(s),o=this._id+String(Date.now())+"$"+String(Math.random());t.localStorage.setItem(o,i),setTimeout(function(){t.localStorage.removeItem(o)},500),e[this._id].forEach(function(t){t!==n&&t._mc.port2.postMessage(JSON.parse(i))})},close:function(){if(!this._closed){this._closed=!0,this._mc.port1.close(),this._mc.port2.close();var t=e[this._id].indexOf(this);e[this._id].splice(t,1)}},get onmessage(){return this._mc.port1.onmessage},set onmessage(t){this._mc.port1.onmessage=t},addEventListener:function(){return this._mc.port1.addEventListener.apply(this._mc.port1,arguments)},removeEventListener:function(){return this._mc.port1.removeEventListener.apply(this._mc.port1,arguments)},dispatchEvent:function(){return this._mc.port1.dispatchEvent.apply(this._mc.port1,arguments)}},t.BroadcastChannel=t.BroadcastChannel||s}(self);
\ No newline at end of file
Supports Markdown
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