mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Transition all remaining messaging over (delete the old stuff)
This commit is contained in:
parent
9377306b81
commit
f27071ee64
@ -75,7 +75,6 @@ import {base32} from "rfc4648";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import WidgetStore from "./stores/WidgetStore";
|
||||
import ActiveWidgetStore from "./stores/ActiveWidgetStore";
|
||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
|
||||
|
@ -1,278 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import URL from 'url';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {Capability} from "./widgets/WidgetApi";
|
||||
import {objectClone} from "./utils/objects";
|
||||
|
||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
'0.0.1',
|
||||
'0.0.2',
|
||||
];
|
||||
const INBOUND_API_NAME = 'fromWidget';
|
||||
|
||||
// Listen for and handle incoming requests using the 'fromWidget' postMessage
|
||||
// API and initiate responses
|
||||
export default class FromWidgetPostMessageApi {
|
||||
constructor() {
|
||||
this.widgetMessagingEndpoints = [];
|
||||
this.widgetListeners = {}; // {action: func[]}
|
||||
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.onPostMessage = this.onPostMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
window.addEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for a given action
|
||||
* @param {string} action The action to listen for.
|
||||
* @param {Function} callbackFn A callback function to be called when the action is
|
||||
* encountered. Called with two parameters: the interesting request information and
|
||||
* the raw event received from the postMessage API. The raw event is meant to be used
|
||||
* for sendResponse and similar functions.
|
||||
*/
|
||||
addListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
|
||||
this.widgetListeners[action].push(callbackFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener for a given action.
|
||||
* @param {string} action The action that was subscribed to.
|
||||
* @param {Function} callbackFn The original callback function that was used to subscribe
|
||||
* to updates.
|
||||
*/
|
||||
removeListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) return;
|
||||
|
||||
const idx = this.widgetListeners[action].indexOf(callbackFn);
|
||||
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a widget endpoint for trusted postMessage communication
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||
*/
|
||||
addEndpoint(widgetId, endpointUrl) {
|
||||
const u = URL.parse(endpointUrl);
|
||||
if (!u || !u.protocol || !u.host) {
|
||||
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = u.protocol + '//' + u.host;
|
||||
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
||||
if (this.widgetMessagingEndpoints.some(function(ep) {
|
||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||
})) {
|
||||
// Message endpoint already registered
|
||||
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
||||
return;
|
||||
} else {
|
||||
console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
||||
this.widgetMessagingEndpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* De-register a widget endpoint from trusted communication sources
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||
* @return {boolean} True if endpoint was successfully removed
|
||||
*/
|
||||
removeEndpoint(widgetId, endpointUrl) {
|
||||
const u = URL.parse(endpointUrl);
|
||||
if (!u || !u.protocol || !u.host) {
|
||||
console.warn('Remove widget messaging endpoint - Invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = u.protocol + '//' + u.host;
|
||||
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
||||
const length = this.widgetMessagingEndpoints.length;
|
||||
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
|
||||
.filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
|
||||
return (length > this.widgetMessagingEndpoints.length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle widget postMessage events
|
||||
* Messages are only handled where a valid, registered messaging endpoints
|
||||
* @param {Event} event Event to handle
|
||||
* @return {undefined}
|
||||
*/
|
||||
onPostMessage(event) {
|
||||
if (!event.origin) { // Handle chrome
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
// Event origin is empty string if undefined
|
||||
if (
|
||||
event.origin.length === 0 ||
|
||||
!this.trustedEndpoint(event.origin) ||
|
||||
event.data.api !== INBOUND_API_NAME ||
|
||||
!event.data.widgetId
|
||||
) {
|
||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
// Call any listeners we have registered
|
||||
if (this.widgetListeners[event.data.action]) {
|
||||
for (const fn of this.widgetListeners[event.data.action]) {
|
||||
fn(event.data, event);
|
||||
}
|
||||
}
|
||||
|
||||
// Although the requestId is required, we don't use it. We'll be nice and process the message
|
||||
// if the property is missing, but with a warning for widget developers.
|
||||
if (!event.data.requestId) {
|
||||
console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
|
||||
}
|
||||
|
||||
const action = event.data.action;
|
||||
const widgetId = event.data.widgetId;
|
||||
if (action === 'content_loaded') {
|
||||
console.log('Widget reported content loaded for', widgetId);
|
||||
dis.dispatch({
|
||||
action: 'widget_content_loaded',
|
||||
widgetId: widgetId,
|
||||
});
|
||||
this.sendResponse(event, {success: true});
|
||||
} else if (action === 'supported_api_versions') {
|
||||
this.sendResponse(event, {
|
||||
api: INBOUND_API_NAME,
|
||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||
});
|
||||
} else if (action === 'api_version') {
|
||||
this.sendResponse(event, {
|
||||
api: INBOUND_API_NAME,
|
||||
version: WIDGET_API_VERSION,
|
||||
});
|
||||
} else if (action === 'm.sticker') {
|
||||
// console.warn('Got sticker message from widget', widgetId);
|
||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
||||
const data = event.data.data || event.data.widgetData;
|
||||
dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
|
||||
} else if (action === 'integration_manager_open') {
|
||||
// Close the stickerpicker
|
||||
dis.dispatch({action: 'stickerpicker_close'});
|
||||
// Open the integration manager
|
||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
||||
const data = event.data.data || event.data.widgetData;
|
||||
const integType = (data && data.integType) ? data.integType : null;
|
||||
const integId = (data && data.integId) ? data.integId : null;
|
||||
|
||||
// TODO: Open the right integration manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||
`type_${integType}`,
|
||||
integId,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||
`type_${integType}`,
|
||||
integId,
|
||||
);
|
||||
}
|
||||
} else if (action === 'set_always_on_screen') {
|
||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
||||
const data = event.data.data;
|
||||
const val = data.value;
|
||||
|
||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
||||
}
|
||||
|
||||
// acknowledge
|
||||
this.sendResponse(event, {});
|
||||
} else if (action === 'get_openid') {
|
||||
// Handled by caller
|
||||
} else {
|
||||
console.warn('Widget postMessage event unhandled');
|
||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message origin is registered as trusted
|
||||
* @param {string} origin PostMessage origin to check
|
||||
* @return {boolean} True if trusted
|
||||
*/
|
||||
trustedEndpoint(origin) {
|
||||
if (!origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.widgetMessagingEndpoints.some((endpoint) => {
|
||||
// TODO / FIXME -- Should this also check the widgetId?
|
||||
return endpoint.endpointUrl === origin;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a postmessage response to a postMessage request
|
||||
* @param {Event} event The original postMessage request event
|
||||
* @param {Object} res Response data
|
||||
*/
|
||||
sendResponse(event, res) {
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error response to a postMessage request
|
||||
* @param {Event} event The original postMessage request event
|
||||
* @param {string} msg Error message
|
||||
* @param {Error} nestedError Nested error event (optional)
|
||||
*/
|
||||
sendError(event, msg, nestedError) {
|
||||
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
error: {
|
||||
message: msg,
|
||||
},
|
||||
};
|
||||
if (nestedError) {
|
||||
data.response.error._error = nestedError;
|
||||
}
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 New Vector 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.
|
||||
*/
|
||||
|
||||
// const OUTBOUND_API_NAME = 'toWidget';
|
||||
|
||||
// Initiate requests using the "toWidget" postMessage API and handle responses
|
||||
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
||||
// response field
|
||||
export default class ToWidgetPostMessageApi {
|
||||
constructor(timeoutMs) {
|
||||
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
||||
this._counter = 0;
|
||||
this._requestMap = {
|
||||
// $ID: {resolve, reject}
|
||||
};
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.onPostMessage = this.onPostMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
window.addEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
onPostMessage(ev) {
|
||||
// THIS IS ALL UNSAFE EXECUTION.
|
||||
// We do not verify who the sender of `ev` is!
|
||||
const payload = ev.data;
|
||||
// NOTE: Workaround for running in a mobile WebView where a
|
||||
// postMessage immediately triggers this callback even though it is
|
||||
// not the response.
|
||||
if (payload.response === undefined) {
|
||||
return;
|
||||
}
|
||||
const promise = this._requestMap[payload.requestId];
|
||||
if (!promise) {
|
||||
return;
|
||||
}
|
||||
delete this._requestMap[payload.requestId];
|
||||
promise.resolve(payload);
|
||||
}
|
||||
|
||||
// Initiate outbound requests (toWidget)
|
||||
exec(action, targetWindow, targetOrigin) {
|
||||
targetWindow = targetWindow || window.parent; // default to parent window
|
||||
targetOrigin = targetOrigin || "*";
|
||||
this._counter += 1;
|
||||
action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._requestMap[action.requestId] = {resolve, reject};
|
||||
targetWindow.postMessage(action, targetOrigin);
|
||||
|
||||
if (this._timeoutMs > 0) {
|
||||
setTimeout(() => {
|
||||
if (!this._requestMap[action.requestId]) {
|
||||
return;
|
||||
}
|
||||
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
|
||||
this._requestMap);
|
||||
this._requestMap[action.requestId].reject(new Error("Timed out"));
|
||||
delete this._requestMap[action.requestId];
|
||||
}, this._timeoutMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
||||
* spec. details / documentation.
|
||||
*/
|
||||
|
||||
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||
import Modal from "./Modal";
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import WidgetUtils from "./utils/WidgetUtils";
|
||||
import {KnownWidgetActions} from "./widgets/WidgetApi";
|
||||
|
||||
if (!global.mxFromWidgetMessaging) {
|
||||
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||
global.mxFromWidgetMessaging.start();
|
||||
}
|
||||
if (!global.mxToWidgetMessaging) {
|
||||
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
||||
global.mxToWidgetMessaging.start();
|
||||
}
|
||||
|
||||
const OUTBOUND_API_NAME = 'toWidget';
|
||||
|
||||
export default class WidgetMessaging {
|
||||
/**
|
||||
* @param {string} widgetId The widget's ID
|
||||
* @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
|
||||
* @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
|
||||
* or a different URL of the clients choosing if it is using its own impl).
|
||||
* @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
|
||||
* @param {object} target Where widget messages should be sent (eg. the iframe object)
|
||||
*/
|
||||
constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
|
||||
this.widgetId = widgetId;
|
||||
this.wurl = wurl;
|
||||
this.renderedUrl = renderedUrl;
|
||||
this.isUserWidget = isUserWidget;
|
||||
this.target = target;
|
||||
this.fromWidget = global.mxFromWidgetMessaging;
|
||||
this.toWidget = global.mxToWidgetMessaging;
|
||||
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
|
||||
this.start();
|
||||
}
|
||||
|
||||
messageToWidget(action) {
|
||||
action.widgetId = this.widgetId; // Required to be sent for all outbound requests
|
||||
|
||||
return this.toWidget.exec(action, this.target).then((data) => {
|
||||
// Check for errors and reject if found
|
||||
if (data.response === undefined) { // null is valid
|
||||
throw new Error("Missing 'response' field");
|
||||
}
|
||||
if (data.response && data.response.error) {
|
||||
const err = data.response.error;
|
||||
const msg = String(err.message ? err.message : "An error was returned");
|
||||
if (err._error) {
|
||||
console.error(err._error);
|
||||
}
|
||||
// Potential XSS attack if 'msg' is not appropriately sanitized,
|
||||
// as it is untrusted input by our parent window (which we assume is Element).
|
||||
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
||||
throw new Error(msg);
|
||||
}
|
||||
// Return the response field for the request
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the widget that the client is ready to handle further widget requests.
|
||||
* @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
|
||||
*/
|
||||
flagReadyToContinue() {
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: KnownWidgetActions.ClientReady,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the widget that it should terminate now.
|
||||
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
|
||||
*/
|
||||
terminate() {
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: KnownWidgetActions.Terminate,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the widget to hang up on its call.
|
||||
* @returns {Promise<*>} Resolves when the widget has acknowledged the message.
|
||||
*/
|
||||
hangup() {
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: KnownWidgetActions.Hangup,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a screenshot from a widget
|
||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||
*/
|
||||
getScreenshot() {
|
||||
console.log('Requesting screenshot for', this.widgetId);
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: "screenshot",
|
||||
})
|
||||
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
||||
.then((response) => response.screenshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request capabilities required by the widget
|
||||
* @return {Promise} To be resolved with an array of requested widget capabilities
|
||||
*/
|
||||
getCapabilities() {
|
||||
console.log('Requesting capabilities for', this.widgetId);
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: "capabilities",
|
||||
}).then((response) => {
|
||||
console.log('Got capabilities for', this.widgetId, response.capabilities);
|
||||
return response.capabilities;
|
||||
});
|
||||
}
|
||||
|
||||
sendVisibility(visible) {
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: "visibility",
|
||||
visible,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to send visibility: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
|
||||
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
|
||||
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
|
||||
}
|
||||
|
||||
async _onOpenIdRequest(ev, rawEv) {
|
||||
if (ev.widgetId !== this.widgetId) return; // not interesting
|
||||
|
||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
|
||||
|
||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
||||
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
|
||||
return;
|
||||
}
|
||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
||||
const responseBody = {state: "allowed"};
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
this.fromWidget.sendResponse(rawEv, responseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm that we received the request
|
||||
this.fromWidget.sendResponse(rawEv, {state: "request"});
|
||||
|
||||
// Actually ask for permission to send the user's data
|
||||
Modal.createTrackedDialog("OpenID widget permissions", '',
|
||||
WidgetOpenIDPermissionsDialog, {
|
||||
widgetUrl: this.wurl,
|
||||
widgetId: this.widgetId,
|
||||
isUserWidget: this.isUserWidget,
|
||||
|
||||
onFinished: async (confirm) => {
|
||||
const responseBody = {
|
||||
// Legacy (early draft) fields
|
||||
success: confirm,
|
||||
|
||||
// New style MSC1960 fields
|
||||
state: confirm ? "allowed" : "blocked",
|
||||
original_request_id: ev.requestId, // eslint-disable-line camelcase
|
||||
};
|
||||
if (confirm) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
}
|
||||
this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: "openid_credentials",
|
||||
data: responseBody,
|
||||
}).catch((error) => {
|
||||
console.error("Failed to send OpenID credentials: ", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 New Vector 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.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
||||
*/
|
||||
export default class WidgetMessageEndpoint {
|
||||
/**
|
||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
* @param {string} endpointUrl Widget wurl origin.
|
||||
*/
|
||||
constructor(widgetId, endpointUrl) {
|
||||
if (!widgetId) {
|
||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
||||
}
|
||||
if (!endpointUrl) {
|
||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
||||
}
|
||||
this.widgetId = widgetId;
|
||||
this.endpointUrl = endpointUrl;
|
||||
}
|
||||
}
|
@ -36,12 +36,12 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
@ -305,7 +305,7 @@ export default class AppTile extends React.Component {
|
||||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
@ -562,7 +562,7 @@ export default class AppTile extends React.Component {
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
|
||||
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
|
||||
&& this.props.show;
|
||||
|
||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||
|
@ -36,11 +36,12 @@ import IconizedContextMenu, {
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import classNames from "classnames";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@ -80,7 +81,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
if (menuDisplayed) {
|
||||
let snapshotButton;
|
||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
||||
if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
|
||||
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||
const onSnapshotClick = () => {
|
||||
widgetMessaging.takeScreenshot().then(data => {
|
||||
dis.dispatch({
|
||||
|
@ -66,14 +66,7 @@ class ActiveWidgetStore extends EventEmitter {
|
||||
if (id !== this._persistentWidgetId) return;
|
||||
const toDeleteId = this._persistentWidgetId;
|
||||
|
||||
const result = WidgetMessagingStore.instance.findWidgetById(id);
|
||||
if (result) {
|
||||
if (result.room) {
|
||||
WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
|
||||
} else {
|
||||
WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
|
||||
}
|
||||
}
|
||||
WidgetMessagingStore.instance.stopMessagingById(id);
|
||||
|
||||
this.setWidgetPersistence(toDeleteId, false);
|
||||
this.delRoomId(toDeleteId);
|
||||
|
@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import {
|
||||
ClientWidgetApi, IStickerActionRequest,
|
||||
ClientWidgetApi,
|
||||
IStickerActionRequest,
|
||||
IStickyActionRequest,
|
||||
IWidget, IWidgetApiRequest,
|
||||
IWidget,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiRequestEmptyData,
|
||||
IWidgetData,
|
||||
Widget, WidgetApiFromWidgetAction
|
||||
MatrixCapabilities,
|
||||
Widget,
|
||||
WidgetApiFromWidgetAction
|
||||
} from "matrix-widget-api";
|
||||
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
|
||||
import { EventEmitter } from "events";
|
||||
@ -33,11 +37,9 @@ import WidgetUtils from '../../utils/WidgetUtils';
|
||||
import { IntegrationManagers } from "../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { WidgetType } from "../../widgets/WidgetType";
|
||||
import { Capability } from "../../widgets/WidgetApi";
|
||||
import ActiveWidgetStore from "../ActiveWidgetStore";
|
||||
import { objectShallowClone } from "../../utils/objects";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||
|
||||
// TODO: Destroy all of this code
|
||||
@ -171,7 +173,7 @@ export class StopGapWidget extends EventEmitter {
|
||||
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
||||
this.messaging.addEventListener("action:set_always_on_screen",
|
||||
(ev: CustomEvent<IStickyActionRequest>) => {
|
||||
if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
|
||||
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
|
||||
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
|
||||
ev.preventDefault();
|
||||
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
|
||||
|
@ -61,6 +61,15 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
return this.widgetMap.get(widget.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the widget messaging instance for a given widget ID.
|
||||
* @param {string} widgetId The widget ID.
|
||||
* @deprecated Widget IDs are not globally unique.
|
||||
*/
|
||||
public stopMessagingById(widgetId: string) {
|
||||
this.widgetMap.remove(widgetId)?.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the widget messaging class for a given widget ID.
|
||||
* @param {string} widgetId The widget ID.
|
||||
|
@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000;
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||
import {Capability} from "../widgets/WidgetApi";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {WidgetType} from "../widgets/WidgetType";
|
||||
import {objectClone} from "./objects";
|
||||
import {_t} from "../languageHandler";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
|
||||
export default class WidgetUtils {
|
||||
/* Returns true if user is able to send state events to modify widgets in this room
|
||||
@ -416,13 +416,13 @@ export default class WidgetUtils {
|
||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
||||
|
||||
const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
|
||||
const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
|
||||
|
||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||
// so this doesn't really offer much over the set of domains we load
|
||||
// widgets from at all, but it probably makes sense for sanity.
|
||||
if (WidgetType.JITSI.matches(appType)) {
|
||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
||||
capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
|
||||
}
|
||||
|
||||
return capWhitelist;
|
||||
|
@ -1,222 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// Dev note: This is largely inspired by Dimension. Used with permission.
|
||||
// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
|
||||
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { EventEmitter } from "events";
|
||||
import { objectClone } from "../utils/objects";
|
||||
|
||||
export enum Capability {
|
||||
Screenshot = "m.capability.screenshot",
|
||||
Sticker = "m.sticker",
|
||||
AlwaysOnScreen = "m.always_on_screen",
|
||||
}
|
||||
|
||||
export enum KnownWidgetActions {
|
||||
GetSupportedApiVersions = "supported_api_versions",
|
||||
TakeScreenshot = "screenshot",
|
||||
GetCapabilities = "capabilities",
|
||||
SendEvent = "send_event",
|
||||
UpdateVisibility = "visibility",
|
||||
GetOpenIDCredentials = "get_openid",
|
||||
ReceiveOpenIDCredentials = "openid_credentials",
|
||||
SetAlwaysOnScreen = "set_always_on_screen",
|
||||
ClientReady = "im.vector.ready",
|
||||
Terminate = "im.vector.terminate",
|
||||
Hangup = "im.vector.hangup",
|
||||
}
|
||||
|
||||
export type WidgetAction = KnownWidgetActions | string;
|
||||
|
||||
export enum WidgetApiType {
|
||||
ToWidget = "toWidget",
|
||||
FromWidget = "fromWidget",
|
||||
}
|
||||
|
||||
export interface WidgetRequest {
|
||||
api: WidgetApiType;
|
||||
widgetId: string;
|
||||
requestId: string;
|
||||
data: any;
|
||||
action: WidgetAction;
|
||||
}
|
||||
|
||||
export interface ToWidgetRequest extends WidgetRequest {
|
||||
api: WidgetApiType.ToWidget;
|
||||
}
|
||||
|
||||
export interface FromWidgetRequest extends WidgetRequest {
|
||||
api: WidgetApiType.FromWidget;
|
||||
response: any;
|
||||
}
|
||||
|
||||
export interface OpenIDCredentials {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
matrixServerName: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Element <--> Widget interactions for embedded/standalone widgets.
|
||||
*
|
||||
* Emitted events:
|
||||
* - terminate(wait): client requested the widget to terminate.
|
||||
* Call the argument 'wait(promise)' to postpone the finalization until
|
||||
* the given promise resolves.
|
||||
*/
|
||||
export class WidgetApi extends EventEmitter {
|
||||
private readonly origin: string;
|
||||
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
||||
private readonly readyPromise: Promise<any>;
|
||||
private readyPromiseResolve: () => void;
|
||||
private openIDCredentialsCallback: () => void;
|
||||
public openIDCredentials: OpenIDCredentials;
|
||||
|
||||
/**
|
||||
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
|
||||
*/
|
||||
public expectingExplicitReady = false;
|
||||
|
||||
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
||||
super();
|
||||
|
||||
this.origin = new URL(currentUrl).origin;
|
||||
|
||||
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
if (event.origin !== this.origin) return; // ignore: invalid origin
|
||||
if (!event.data) return; // invalid schema
|
||||
if (event.data.widgetId !== this.widgetId) return; // not for us
|
||||
|
||||
const payload = <WidgetRequest>event.data;
|
||||
if (payload.api === WidgetApiType.ToWidget && payload.action) {
|
||||
console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
|
||||
|
||||
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
||||
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
||||
if (!this.expectingExplicitReady) {
|
||||
this.readyPromiseResolve();
|
||||
}
|
||||
} else if (payload.action === KnownWidgetActions.ClientReady) {
|
||||
this.readyPromiseResolve();
|
||||
|
||||
// Automatically acknowledge so we can move on
|
||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||
} else if (payload.action === KnownWidgetActions.Terminate
|
||||
|| payload.action === KnownWidgetActions.Hangup) {
|
||||
// Finalization needs to be async, so postpone with a promise
|
||||
let finalizePromise = Promise.resolve();
|
||||
const wait = (promise) => {
|
||||
finalizePromise = finalizePromise.then(() => promise);
|
||||
};
|
||||
const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
|
||||
this.emit(emitName, wait);
|
||||
Promise.resolve(finalizePromise).then(() => {
|
||||
// Acknowledge that we're shut down now
|
||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||
});
|
||||
} else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
|
||||
// Save OpenID credentials
|
||||
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||
} else {
|
||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
||||
}
|
||||
} else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
|
||||
console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
|
||||
const handler = this.inFlightRequests[payload.requestId];
|
||||
delete this.inFlightRequests[payload.requestId];
|
||||
handler(<FromWidgetRequest>payload);
|
||||
} else {
|
||||
console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setOpenIDCredentials(value: WidgetRequest) {
|
||||
const data = value.data;
|
||||
if (data.state === 'allowed') {
|
||||
this.openIDCredentials = {
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
matrixServerName: data.matrix_server_name,
|
||||
expiresIn: data.expires_in,
|
||||
}
|
||||
} else if (data.state === 'blocked') {
|
||||
this.openIDCredentials = null;
|
||||
}
|
||||
if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) {
|
||||
this.openIDCredentialsCallback()
|
||||
}
|
||||
}
|
||||
|
||||
public requestOpenIDCredentials(credentialsResponseCallback: () => void) {
|
||||
this.openIDCredentialsCallback = credentialsResponseCallback;
|
||||
this.callAction(
|
||||
KnownWidgetActions.GetOpenIDCredentials,
|
||||
{},
|
||||
this.setOpenIDCredentials,
|
||||
);
|
||||
}
|
||||
|
||||
public waitReady(): Promise<any> {
|
||||
return this.readyPromise;
|
||||
}
|
||||
|
||||
private replyToRequest(payload: ToWidgetRequest, reply: any) {
|
||||
if (!window.parent) return;
|
||||
|
||||
const request: ToWidgetRequest & {response?: any} = objectClone(payload);
|
||||
request.response = reply;
|
||||
|
||||
window.parent.postMessage(request, this.origin);
|
||||
}
|
||||
|
||||
private onCapabilitiesRequest(payload: ToWidgetRequest) {
|
||||
return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
|
||||
}
|
||||
|
||||
public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
|
||||
if (!window.parent) return;
|
||||
|
||||
const request: FromWidgetRequest = {
|
||||
api: WidgetApiType.FromWidget,
|
||||
widgetId: this.widgetId,
|
||||
action: action,
|
||||
requestId: randomString(160),
|
||||
data: payload,
|
||||
response: {}, // Not used at this layer - it's used when the client responds
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
this.inFlightRequests[request.requestId] = callback;
|
||||
}
|
||||
|
||||
console.log(`[WidgetAPI] Sending request: `, request);
|
||||
window.parent.postMessage(request, "*");
|
||||
}
|
||||
|
||||
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
||||
return new Promise<any>(resolve => {
|
||||
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
|
||||
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
||||
});
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Move to matrix-widget-api
|
||||
export class WidgetType {
|
||||
public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
|
||||
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
||||
|
Loading…
Reference in New Issue
Block a user