From b571ab02dcac23bb9f842277377cdbe062bf9270 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 29 Nov 2017 18:11:03 +0000 Subject: [PATCH 01/18] Add widget messaging stub. --- src/WidgetMessaging.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/WidgetMessaging.js diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js new file mode 100644 index 0000000000..c6cd392f7f --- /dev/null +++ b/src/WidgetMessaging.js @@ -0,0 +1,41 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export default class WidgetMessaging { + constructor() { + } + + /** + * Register event listeners for the widget instance + * @param {string} widgetId Unique widget identifier + * @return {undefined} + */ + registerListeners(widgetId) { + if (widgetId) { + console.log("Register widget instance postmessage listeners"); + } else { + console.error("Register widget event listeners - No widget ID specified!"); + } + } + + derigisterListeners(widgetId) { + if (widgetId) { + console.log("Register widget instance postmessage listeners"); + } else { + console.error("Deregister widget event listerns - No widget ID specified!"); + } + } +} From 4f5f44ff3889c6ac35c2e4b98c1223669b319db3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 29 Nov 2017 22:16:22 +0000 Subject: [PATCH 02/18] Add widget postmessage API stub. --- src/WidgetMessaging.js | 46 ++++++++--- src/components/views/elements/AppTile.js | 101 +++++++++++++++++++++-- 2 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c6cd392f7f..0552be86e4 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -16,26 +16,48 @@ limitations under the License. export default class WidgetMessaging { constructor() { + this.listenerCount = 0; } /** - * Register event listeners for the widget instance - * @param {string} widgetId Unique widget identifier + * Register widget message event listeners * @return {undefined} */ - registerListeners(widgetId) { - if (widgetId) { - console.log("Register widget instance postmessage listeners"); - } else { - console.error("Register widget event listeners - No widget ID specified!"); + registerListeners() { + if (this.listenerCount === 0) { + window.addEventListener("message", this.onMessage, false); + } + 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) { - if (widgetId) { - console.log("Register widget instance postmessage listeners"); - } else { - console.error("Deregister widget event listerns - No widget ID specified!"); + onMessage(event) { + console.warn("Checking for widget event", event); + if (!event.origin) { // Handle chrome + event.origin = event.originalEvent.origin; } + + // 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); } } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index a005406133..c3c31ff8b5 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,6 +22,7 @@ import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; +import WidgetMessaging from '../../../WidgetMessaging'; import TintableSvgButton from './TintableSvgButton'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -71,16 +72,46 @@ export default React.createClass({ return { initialising: true, // True while we are mangling the widget URL loading: true, // True while the iframe content is loading - widgetUrl: newProps.url, + widgetUrl: this._addWurlParams(newProps.url), widgetPermissionId: widgetPermissionId, // 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 hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, error: null, 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() { return this._getNewState(this.props); }, @@ -122,6 +153,8 @@ export default React.createClass({ }, componentWillMount() { + this.widgetMessagingClient = new WidgetMessaging(); + this.widgetMessagingClient.registerListeners(); window.addEventListener('message', this._onMessage, false); this.setScalarToken(); }, @@ -137,7 +170,7 @@ export default React.createClass({ console.warn('Non-scalar widget, not setting scalar token!', url); this.setState({ error: null, - widgetUrl: this.props.url, + widgetUrl: this._addWurlParams(this.props.url), initialising: false, }); return; @@ -150,7 +183,7 @@ export default React.createClass({ this._scalarClient.getScalarToken().done((token) => { // Append scalar_token as a query param if not already present 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); if (!params.scalar_token) { params.scalar_token = encodeURIComponent(token); @@ -256,8 +289,36 @@ export default React.createClass({ } }, + /** + * Called when widget iframe has finished loading + */ _onLoaded() { 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 @@ -290,6 +351,21 @@ export default React.createClass({ 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) { 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() { let appTileBody; @@ -320,11 +405,6 @@ export default React.createClass({ // a link to it. const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ "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) { const loadingElement = ( @@ -347,7 +427,7 @@ export default React.createClass({ { this.state.loading && loadingElement }