b03ee5bd53
When working on making the openCoWebsite URL relative, we introduced a regression. In production, the iframe generated by "script" properties have no "src" and therefore, were treated as invalid messages. This should fix everything in prod.
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
|
|
import { Subject } from "rxjs";
|
|
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
|
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
|
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
|
|
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
|
|
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
|
|
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
|
|
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
|
|
import { scriptUtils } from "./ScriptUtils";
|
|
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
|
|
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
|
|
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
|
|
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
|
|
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
|
|
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
|
|
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
|
|
/**
|
|
* Listens to messages from iframes and turn those messages into easy to use observables.
|
|
* Also allows to send messages to those iframes.
|
|
*/
|
|
class IframeListener {
|
|
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
|
public readonly chatStream = this._chatStream.asObservable();
|
|
|
|
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
|
|
public readonly openPopupStream = this._openPopupStream.asObservable();
|
|
|
|
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
|
|
public readonly openTabStream = this._openTabStream.asObservable();
|
|
|
|
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
|
|
public readonly goToPageStream = this._goToPageStream.asObservable();
|
|
|
|
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
|
|
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
|
|
|
|
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
|
|
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
|
|
|
|
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
|
|
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
|
|
|
|
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
|
|
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
|
|
|
|
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
|
|
public readonly closePopupStream = this._closePopupStream.asObservable();
|
|
|
|
private readonly _displayBubbleStream: Subject<void> = new Subject();
|
|
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
|
|
|
|
private readonly _removeBubbleStream: Subject<void> = new Subject();
|
|
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
|
|
|
|
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
|
|
public readonly playSoundStream = this._playSoundStream.asObservable();
|
|
|
|
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
|
|
public readonly stopSoundStream = this._stopSoundStream.asObservable();
|
|
|
|
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
|
|
public readonly loadSoundStream = this._loadSoundStream.asObservable();
|
|
|
|
private readonly iframes = new Set<HTMLIFrameElement>();
|
|
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
|
|
|
init() {
|
|
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
|
// Do we trust the sender of this message?
|
|
// Let's only accept messages from the iframe that are allowed.
|
|
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
|
let foundSrc: string | undefined;
|
|
|
|
foundSrc = [...this.scripts.keys()].find(key => {
|
|
return this.scripts.get(key)?.contentWindow == message.source
|
|
});
|
|
|
|
if (foundSrc === undefined) {
|
|
for (const iframe of this.iframes) {
|
|
if (iframe.contentWindow === message.source) {
|
|
foundSrc = iframe.src;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundSrc === undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const payload = message.data;
|
|
if (isIframeEventWrapper(payload)) {
|
|
if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
|
this._chatStream.next(payload.data);
|
|
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
|
this._openPopupStream.next(payload.data);
|
|
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
|
this._closePopupStream.next(payload.data);
|
|
}
|
|
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
|
scriptUtils.openTab(payload.data.url);
|
|
}
|
|
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
|
scriptUtils.goToPage(payload.data.url);
|
|
}
|
|
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
|
|
this._playSoundStream.next(payload.data);
|
|
}
|
|
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
|
|
this._stopSoundStream.next(payload.data);
|
|
}
|
|
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
|
|
this._loadSoundStream.next(payload.data);
|
|
}
|
|
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
|
|
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
|
|
}
|
|
|
|
else if (payload.type === 'closeCoWebSite') {
|
|
scriptUtils.closeCoWebSite();
|
|
}
|
|
|
|
else if (payload.type === 'disablePlayerControls') {
|
|
this._disablePlayerControlStream.next();
|
|
}
|
|
else if (payload.type === 'restorePlayerControls') {
|
|
this._enablePlayerControlStream.next();
|
|
}
|
|
else if (payload.type === 'displayBubble') {
|
|
this._displayBubbleStream.next();
|
|
}
|
|
else if (payload.type === 'removeBubble') {
|
|
this._removeBubbleStream.next();
|
|
}
|
|
}
|
|
|
|
|
|
}, false);
|
|
|
|
}
|
|
|
|
/**
|
|
* Allows the passed iFrame to send/receive messages via the API.
|
|
*/
|
|
registerIframe(iframe: HTMLIFrameElement): void {
|
|
this.iframes.add(iframe);
|
|
}
|
|
|
|
unregisterIframe(iframe: HTMLIFrameElement): void {
|
|
this.iframes.delete(iframe);
|
|
}
|
|
|
|
registerScript(scriptUrl: string): void {
|
|
console.log('Loading map related script at ', scriptUrl)
|
|
|
|
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
|
|
// Using external iframe mode (
|
|
const iframe = document.createElement('iframe');
|
|
iframe.id = this.getIFrameId(scriptUrl);
|
|
iframe.style.display = 'none';
|
|
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
|
|
|
|
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
|
iframe.sandbox.add('allow-scripts');
|
|
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
|
|
|
document.body.prepend(iframe);
|
|
|
|
this.scripts.set(scriptUrl, iframe);
|
|
this.registerIframe(iframe);
|
|
} else {
|
|
// production code
|
|
const iframe = document.createElement('iframe');
|
|
iframe.id = this.getIFrameId(scriptUrl);
|
|
iframe.style.display = 'none';
|
|
|
|
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
|
iframe.sandbox.add('allow-scripts');
|
|
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
|
|
|
const html = '<!doctype html>\n' +
|
|
'\n' +
|
|
'<html lang="en">\n' +
|
|
'<head>\n' +
|
|
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
|
|
'<script src="' + scriptUrl + '" ></script>\n' +
|
|
'</head>\n' +
|
|
'</html>\n';
|
|
|
|
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
|
iframe.srcdoc = html;
|
|
|
|
document.body.prepend(iframe);
|
|
|
|
this.scripts.set(scriptUrl, iframe);
|
|
this.registerIframe(iframe);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private getIFrameId(scriptUrl: string): string {
|
|
return 'script' + btoa(scriptUrl);
|
|
}
|
|
|
|
unregisterScript(scriptUrl: string): void {
|
|
const iFrameId = this.getIFrameId(scriptUrl);
|
|
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
|
|
if (!iframe) {
|
|
throw new Error('Unknown iframe for script "' + scriptUrl + '"');
|
|
}
|
|
this.unregisterIframe(iframe);
|
|
iframe.remove();
|
|
|
|
this.scripts.delete(scriptUrl);
|
|
}
|
|
|
|
sendUserInputChat(message: string) {
|
|
this.postMessage({
|
|
'type': 'userInputChat',
|
|
'data': {
|
|
'message': message,
|
|
} as UserInputChatEvent
|
|
});
|
|
}
|
|
|
|
sendEnterEvent(name: string) {
|
|
this.postMessage({
|
|
'type': 'enterEvent',
|
|
'data': {
|
|
"name": name
|
|
} as EnterLeaveEvent
|
|
});
|
|
}
|
|
|
|
sendLeaveEvent(name: string) {
|
|
this.postMessage({
|
|
'type': 'leaveEvent',
|
|
'data': {
|
|
"name": name
|
|
} as EnterLeaveEvent
|
|
});
|
|
}
|
|
|
|
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
|
this.postMessage({
|
|
'type': 'buttonClickedEvent',
|
|
'data': {
|
|
popupId,
|
|
buttonId
|
|
} as ButtonClickedEvent
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends the message... to all allowed iframes.
|
|
*/
|
|
private postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
|
for (const iframe of this.iframes) {
|
|
iframe.contentWindow?.postMessage(message, '*');
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export const iframeListener = new IframeListener();
|