328 lines
No EOL
8.8 KiB
JavaScript
328 lines
No EOL
8.8 KiB
JavaScript
'use strict';
|
|
|
|
import React, {Component} from "react";
|
|
import PropTypes from "prop-types";
|
|
import {translate} from "react-i18next";
|
|
import {
|
|
requiresAuthenticatedUser,
|
|
withPageHelpers
|
|
} from "./page";
|
|
import {
|
|
withAsyncErrorHandler,
|
|
withErrorHandling
|
|
} from "./error-handling";
|
|
import axios from "./axios";
|
|
import styles from "./styles.scss";
|
|
import {
|
|
getSandboxUrl,
|
|
getTrustedUrl,
|
|
getUrl,
|
|
setRestrictedAccessToken
|
|
} from "./urls";
|
|
|
|
@withPageHelpers
|
|
@withErrorHandling
|
|
@requiresAuthenticatedUser
|
|
export class UntrustedContentHost extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.refreshAccessTokenTimeout = null;
|
|
this.accessToken = null;
|
|
this.contentNodeIsLoaded = false;
|
|
|
|
this.state = {
|
|
hasAccessToken: false,
|
|
};
|
|
|
|
this.receiveMessageHandler = ::this.receiveMessage;
|
|
|
|
this.rpcCounter = 0;
|
|
this.rpcResolves = new Map();
|
|
}
|
|
|
|
static propTypes = {
|
|
contentSrc: PropTypes.string,
|
|
contentProps: PropTypes.object,
|
|
tokenMethod: PropTypes.string,
|
|
tokenParams: PropTypes.object,
|
|
className: PropTypes.string,
|
|
singleToken: PropTypes.bool,
|
|
onMethodAsync: PropTypes.func
|
|
}
|
|
|
|
isInitialized() {
|
|
return !!this.accessToken && !!this.props.contentProps;
|
|
}
|
|
|
|
async receiveMessage(evt) {
|
|
const msg = evt.data;
|
|
|
|
if (msg.type === 'initNeeded') {
|
|
if (this.isInitialized()) {
|
|
this.sendMessage('init', {
|
|
accessToken: this.accessToken,
|
|
contentProps: this.props.contentProps
|
|
});
|
|
}
|
|
} else if (msg.type === 'rpcResponse') {
|
|
const resolve = this.rpcResolves.get(msg.data.msgId);
|
|
resolve(msg.data.ret);
|
|
} else if (msg.type === 'rpcRequest') {
|
|
const ret = await this.props.onMethodAsync(msg.data.method, msg.data.params);
|
|
} else if (msg.type === 'clientHeight') {
|
|
const newHeight = msg.data;
|
|
this.contentNode.height = newHeight;
|
|
}
|
|
}
|
|
|
|
sendMessage(type, data) {
|
|
if (this.contentNodeIsLoaded) { // This is to avoid errors "common.js:45744 Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:8081') does not match the recipient window's origin ('http://localhost:3000')"
|
|
this.contentNode.contentWindow.postMessage({type, data}, getSandboxUrl());
|
|
}
|
|
}
|
|
|
|
async ask(method, params) {
|
|
if (this.contentNodeIsLoaded) {
|
|
this.rpcCounter += 1;
|
|
const msgId = this.rpcCounter;
|
|
|
|
this.sendMessage('rpcRequest', {
|
|
method,
|
|
params,
|
|
msgId
|
|
});
|
|
|
|
return await (new Promise((resolve, reject) => {
|
|
this.rpcResolves.set(msgId, resolve);
|
|
}));
|
|
}
|
|
}
|
|
|
|
@withAsyncErrorHandler
|
|
async refreshAccessToken() {
|
|
if (this.props.singleToken && this.accessToken) {
|
|
await axios.put(getUrl('rest/restricted-access-token'), {
|
|
token: this.accessToken
|
|
});
|
|
} else {
|
|
const result = await axios.post(getUrl('rest/restricted-access-token'), {
|
|
method: this.props.tokenMethod,
|
|
params: this.props.tokenParams
|
|
});
|
|
|
|
this.accessToken = result.data;
|
|
|
|
if (!this.state.hasAccessToken) {
|
|
this.setState({
|
|
hasAccessToken: true
|
|
})
|
|
}
|
|
|
|
this.sendMessage('accessToken', this.accessToken);
|
|
}
|
|
}
|
|
|
|
scheduleRefreshAccessToken() {
|
|
this.refreshAccessTokenTimeout = setTimeout(() => {
|
|
this.refreshAccessToken();
|
|
this.scheduleRefreshAccessToken();
|
|
}, 60 * 1000);
|
|
}
|
|
|
|
handleUpdate() {
|
|
if (this.isInitialized()) {
|
|
this.sendMessage('initAvailable');
|
|
}
|
|
|
|
if (!this.state.hasAccessToken) {
|
|
this.refreshAccessToken();
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.scheduleRefreshAccessToken();
|
|
window.addEventListener('message', this.receiveMessageHandler, false);
|
|
|
|
this.handleUpdate();
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
this.handleUpdate();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearTimeout(this.refreshAccessTokenTimeout);
|
|
window.removeEventListener('message', this.receiveMessageHandler, false);
|
|
}
|
|
|
|
contentNodeLoaded() {
|
|
this.contentNodeIsLoaded = true;
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<iframe className={styles.untrustedContent + ' ' + this.props.className} ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}> </iframe>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
@translate()
|
|
export class UntrustedContentRoot extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
initialized: false,
|
|
};
|
|
|
|
this.receiveMessageHandler = ::this.receiveMessage;
|
|
|
|
this.periodicTimeoutHandler = ::this.periodicTimeoutHandler;
|
|
this.periodicTimeoutId = 0;
|
|
|
|
this.clientHeight = 0;
|
|
}
|
|
|
|
static propTypes = {
|
|
render: PropTypes.func
|
|
}
|
|
|
|
|
|
async periodicTimeoutHandler() {
|
|
const newHeight = document.body.clientHeight;
|
|
if (this.clientHeight !== newHeight) {
|
|
this.clientHeight = newHeight;
|
|
this.sendMessage('clientHeight', newHeight);
|
|
}
|
|
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
|
|
}
|
|
|
|
|
|
async receiveMessage(evt) {
|
|
const msg = evt.data;
|
|
|
|
if (msg.type === 'initAvailable' && !this.state.initialized) {
|
|
this.sendMessage('initNeeded');
|
|
|
|
} else if (msg.type === 'init' && !this.state.initialized) {
|
|
setRestrictedAccessToken(msg.data.accessToken);
|
|
this.setState({
|
|
initialized: true,
|
|
contentProps: msg.data.contentProps
|
|
});
|
|
|
|
} else if (msg.type === 'accessToken') {
|
|
setRestrictedAccessToken(msg.data);
|
|
}
|
|
}
|
|
|
|
sendMessage(type, data) {
|
|
window.parent.postMessage({type, data}, getTrustedUrl());
|
|
}
|
|
|
|
componentDidMount() {
|
|
window.addEventListener('message', this.receiveMessageHandler, false);
|
|
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 0);
|
|
this.sendMessage('initNeeded');
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('message', this.receiveMessageHandler, false);
|
|
clearTimeout(this.periodicTimeoutId);
|
|
}
|
|
|
|
render() {
|
|
const t = this.props.t;
|
|
|
|
if (this.state.initialized) {
|
|
return this.props.render(this.state.contentProps);
|
|
} else {
|
|
return (
|
|
<div>
|
|
{t('Loading...')}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ParentRPC {
|
|
constructor(props) {
|
|
this.receiveMessageHandler = ::this.receiveMessage;
|
|
|
|
this.rpcCounter = 0;
|
|
this.rpcResolves = new Map();
|
|
this.methodHandlers = new Map();
|
|
|
|
this.initialized = false;
|
|
}
|
|
|
|
init() {
|
|
window.addEventListener('message', this.receiveMessageHandler, false);
|
|
this.initialized = true;
|
|
}
|
|
|
|
setMethodHandler(method, handler) {
|
|
this.enforceInitialized();
|
|
this.methodHandlers.set(method, handler);
|
|
}
|
|
|
|
clearMethodHandler(method) {
|
|
this.enforceInitialized();
|
|
this.methodHandlers.delete(method);
|
|
}
|
|
|
|
async ask(method, params) {
|
|
this.enforceInitialized();
|
|
this.rpcCounter += 1;
|
|
const msgId = this.rpcCounter;
|
|
|
|
this.sendMessage('rpcRequest', {
|
|
method,
|
|
params,
|
|
msgId
|
|
});
|
|
|
|
return await (new Promise((resolve, reject) => {
|
|
this.rpcResolves.set(msgId, resolve);
|
|
}));
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private methods
|
|
|
|
enforceInitialized() {
|
|
if (!this.initialized) {
|
|
throw new Error('ParentRPC not initialized');
|
|
}
|
|
}
|
|
|
|
async receiveMessage(evt) {
|
|
const msg = evt.data;
|
|
|
|
if (msg.type === 'rpcResponse') {
|
|
const resolve = this.rpcResolves.get(msg.data.msgId);
|
|
resolve(msg.data.ret);
|
|
|
|
} else if (msg.type === 'rpcRequest') {
|
|
let ret;
|
|
|
|
const method = msg.data.method;
|
|
if (this.methodHandlers.has(method)) {
|
|
const handler = this.methodHandlers.get(method);
|
|
ret = await handler(method, msg.data.params);
|
|
}
|
|
|
|
this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
|
|
}
|
|
}
|
|
|
|
sendMessage(type, data) {
|
|
window.parent.postMessage({type, data}, getTrustedUrl());
|
|
}
|
|
}
|
|
|
|
export const parentRPC = new ParentRPC(); |