Add widget postmessage API stub.

This commit is contained in:
Richard Lewis 2017-11-29 22:16:22 +00:00
parent b571ab02dc
commit 4f5f44ff38
2 changed files with 126 additions and 21 deletions

View file

@ -16,26 +16,48 @@ limitations under the License.
export default class WidgetMessaging { export default class WidgetMessaging {
constructor() { constructor() {
this.listenerCount = 0;
} }
/** /**
* Register event listeners for the widget instance * Register widget message event listeners
* @param {string} widgetId Unique widget identifier
* @return {undefined} * @return {undefined}
*/ */
registerListeners(widgetId) { registerListeners() {
if (widgetId) { if (this.listenerCount === 0) {
console.log("Register widget instance postmessage listeners"); window.addEventListener("message", this.onMessage, false);
} else { }
console.error("Register widget event listeners - No widget ID specified!"); this.listenerCount += 1;
}
derigisterListeners() {
this.listenerCount -= 1;
if (this.listenerCount === 0) {
window.removeEventListener("message", this.onMessage);
}
if (this.listenerCount < 0) {
// Make an error so we get a stack trace
const e = new Error(
"WidgetMessaging: mismatched startListening / stopListening detected." +
" Negative count",
);
console.error(e);
} }
} }
derigisterListeners(widgetId) { onMessage(event) {
if (widgetId) { console.warn("Checking for widget event", event);
console.log("Register widget instance postmessage listeners"); if (!event.origin) { // Handle chrome
} else { event.origin = event.originalEvent.origin;
console.error("Deregister widget event listerns - No widget ID specified!");
} }
// Event origin is empty string if undefined
if (event.origin.length === 0 || !event.data.widgetData) {
// TODO / FIXME -- check for valid origin URLs!!
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
}
// TODO -- handle widget actions
alert(event.data.widgetData);
} }
} }

View file

@ -22,6 +22,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton'; import TintableSvgButton from './TintableSvgButton';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -71,16 +72,46 @@ export default React.createClass({
return { return {
initialising: true, // True while we are mangling the widget URL initialising: true, // True while we are mangling the widget URL
loading: true, // True while the iframe content is loading loading: true, // True while the iframe content is loading
widgetUrl: newProps.url, widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId, widgetPermissionId: widgetPermissionId,
// Assume that widget has permission to load if we are the user who // Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user // added it to the room, or if explicitly granted by the user
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
error: null, error: null,
deleting: false, deleting: false,
widgetPageTitle: null,
}; };
}, },
/**
* Add widget instance specific parameters to pass in wUrl
* Properties passed to widget instance:
* - widgetId
* - origin / parent URL
* @param {string} urlString Url string to modify
* @return {string}
* Url string with parameters appended.
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
// Append current / parent URL
params.parentUrl = window.location.href;
u.search = undefined;
u.query = params;
console.log("_addWurlParams", "Modified URL", u.format(), params);
return u.format();
},
getInitialState() { getInitialState() {
return this._getNewState(this.props); return this._getNewState(this.props);
}, },
@ -122,6 +153,8 @@ export default React.createClass({
}, },
componentWillMount() { componentWillMount() {
this.widgetMessagingClient = new WidgetMessaging();
this.widgetMessagingClient.registerListeners();
window.addEventListener('message', this._onMessage, false); window.addEventListener('message', this._onMessage, false);
this.setScalarToken(); this.setScalarToken();
}, },
@ -137,7 +170,7 @@ export default React.createClass({
console.warn('Non-scalar widget, not setting scalar token!', url); console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({ this.setState({
error: null, error: null,
widgetUrl: this.props.url, widgetUrl: this._addWurlParams(this.props.url),
initialising: false, initialising: false,
}); });
return; return;
@ -150,7 +183,7 @@ export default React.createClass({
this._scalarClient.getScalarToken().done((token) => { this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present // Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token; this._scalarClient.scalarToken = token;
const u = url.parse(this.props.url); const u = url.parse(this._addWurlParams(this.props.url));
const params = qs.parse(u.query); const params = qs.parse(u.query);
if (!params.scalar_token) { if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token); params.scalar_token = encodeURIComponent(token);
@ -256,8 +289,36 @@ export default React.createClass({
} }
}, },
/**
* Called when widget iframe has finished loading
*/
_onLoaded() { _onLoaded() {
this.setState({loading: false}); this.setState({loading: false});
// Get page title and update widget panel
// this._updateWidgetTitle();
},
/**
* Fetch remote content title and update app tile
*/
_updateWidgetTitle() {
const safeUrl = this._getSafeUrl();
console.warn("widget title safeurl:", safeUrl);
if (safeUrl) {
let title = null;
try {
// title = yield this.getUrlTitle(safeUrl);
// console.log("Foo");
} catch (e) {
console.error("Failed to get title for:", safeUrl);
}
console.warn("widget title:", title);
this.setState({widgetPageTitle: title});
return;
}
console.warn("widget title: no url");
this.setState({widgetPageTitle: null});
}, },
// Widget labels to render, depending upon user permissions // Widget labels to render, depending upon user permissions
@ -290,6 +351,21 @@ export default React.createClass({
return appTileName; return appTileName;
}, },
/**
* Get the HTML title for a given URL
* @param {string} url URL to process
* @return {string} Title of the HTML page, or null
*/
getUrlTitle(url) {
return fetch(url)
.then((response) => response.text())
.then((html) => {
const doc = new DOMParser().parseFromString(html, "text/html");
const title = doc.querySelectorAll('title')[0];
return title.innerText;
});
},
onClickMenuBar(ev) { onClickMenuBar(ev) {
ev.preventDefault(); ev.preventDefault();
@ -305,6 +381,15 @@ export default React.createClass({
}); });
}, },
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
},
render() { render() {
let appTileBody; let appTileBody;
@ -320,11 +405,6 @@ export default React.createClass({
// a link to it. // a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation"; "allow-same-origin allow-scripts allow-presentation";
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
if (this.props.show) { if (this.props.show) {
const loadingElement = ( const loadingElement = (
@ -347,7 +427,7 @@ export default React.createClass({
{ this.state.loading && loadingElement } { this.state.loading && loadingElement }
<iframe <iframe
ref="appFrame" ref="appFrame"
src={safeWidgetUrl} src={this._getSafeUrl()}
allowFullScreen="true" allowFullScreen="true"
sandbox={sandboxFlags} sandbox={sandboxFlags}
onLoad={this._onLoaded} onLoad={this._onLoaded}
@ -383,6 +463,9 @@ export default React.createClass({
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}> <div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}> <div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
<b>{ this.formatAppTileName() }</b> <b>{ this.formatAppTileName() }</b>
{ this.state.widgetPageTitle && (
<span>&nbsp;-&nbsp;{ this.state.widgetPageTitle }</span>
) }
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ /* Edit widget */ } { /* Edit widget */ }
{ showEditButton && <TintableSvgButton { showEditButton && <TintableSvgButton