diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js
index f25d8fd506..f7d242caac 100644
--- a/src/components/views/toasts/SetupEncryptionToast.js
+++ b/src/components/views/toasts/SetupEncryptionToast.js
@@ -20,7 +20,9 @@ import Modal from '../../../Modal';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
+import Modal from '../../../Modal';
import DeviceListener from '../../../DeviceListener';
+import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
@@ -57,13 +59,18 @@ export default class SetupEncryptionToast extends React.PureComponent {
}
_onSetupClick = async () => {
- const Spinner = sdk.getComponent("elements.Spinner");
- const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
- try {
- await accessSecretStorage();
- await this._waitForCompletion();
- } finally {
- modal.close();
+ if (this.props.kind === "verify_this_session") {
+ Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
+ {}, null, /* priority = */ false, /* static = */ true);
+ } else {
+ const Spinner = sdk.getComponent("elements.Spinner");
+ const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
+ try {
+ await accessSecretStorage();
+ await this._waitForCompletion();
+ } finally {
+ modal.close();
+ }
}
};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 57b39309b0..5d923e0a24 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -582,6 +582,10 @@
"not found": "not found",
"Cross-signing private keys:": "Cross-signing private keys:",
"in secret storage": "in secret storage",
+ "Self signing private key:": "Self signing private key:",
+ "cached locally": "cached locally",
+ "not found locally": "not found locally",
+ "User signing private key:": "User signing private key:",
"Secret storage public key:": "Secret storage public key:",
"in account data": "in account data",
"Homeserver feature support:": "Homeserver feature support:",
@@ -2006,14 +2010,7 @@
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Could not load user profile": "Could not load user profile",
"Complete security": "Complete security",
- "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.",
- "Waiting…": "Waiting…",
- "If you can’t access one, ": "If you can’t access one, ",
"Session verified": "Session verified",
- "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
- "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
- "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
- "Go Back": "Go Back",
"Failed to send email": "Failed to send email",
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
"A new password must be entered.": "A new password must be entered.",
@@ -2063,6 +2060,13 @@
"You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.",
"Registration Successful": "Registration Successful",
"Create your account": "Create your account",
+ "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.",
+ "Waiting…": "Waiting…",
+ "If you can’t access one, ": "If you can’t access one, ",
+ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
+ "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
+ "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
+ "Go Back": "Go Back",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Failed to re-authenticate": "Failed to re-authenticate",
"Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.",
diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index cff7a93d08..ee9f703136 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -200,13 +200,17 @@ matrixLinkify.options = {
switch (type) {
case "url": {
// intercept local permalinks to users and show them like userids (in userinfo of current room)
- const permalink = parsePermalink(href);
- if (permalink && permalink.userId) {
- return {
- click: function(e) {
- matrixLinkify.onUserClick(e, permalink.userId);
- },
- };
+ try {
+ const permalink = parsePermalink(href);
+ if (permalink && permalink.userId) {
+ return {
+ click: function(e) {
+ matrixLinkify.onUserClick(e, permalink.userId);
+ },
+ };
+ }
+ } catch (e) {
+ // OK fine, it's not actually a permalink
}
break;
}
diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js
index 53e9f24788..00ef87f89c 100644
--- a/src/rageshake/submit-rageshake.js
+++ b/src/rageshake/submit-rageshake.js
@@ -118,6 +118,10 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
try {
body.append("storageManager_persisted", await navigator.storage.persisted());
} catch (e) {}
+ } else if (document.hasStorageAccess) { // Safari
+ try {
+ body.append("storageManager_persisted", await document.hasStorageAccess());
+ } catch (e) {}
}
if (navigator.storage && navigator.storage.estimate) {
try {
diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
new file mode 100644
index 0000000000..7b42e1552d
--- /dev/null
+++ b/src/stores/SetupEncryptionStore.js
@@ -0,0 +1,144 @@
+/*
+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.
+*/
+
+import EventEmitter from 'events';
+import { MatrixClientPeg } from '../MatrixClientPeg';
+import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManager';
+
+export const PHASE_INTRO = 0;
+export const PHASE_BUSY = 1;
+export const PHASE_DONE = 2; //final done stage, but still showing UX
+export const PHASE_CONFIRM_SKIP = 3;
+export const PHASE_FINISHED = 4; //UX can be closed
+
+export class SetupEncryptionStore extends EventEmitter {
+ static sharedInstance() {
+ if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
+ return global.mx_SetupEncryptionStore;
+ }
+
+ start() {
+ if (this._started) {
+ return;
+ }
+ this._started = true;
+ this.phase = PHASE_INTRO;
+ this.verificationRequest = null;
+ this.backupInfo = null;
+ MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
+ }
+
+ stop() {
+ if (!this._started) {
+ return;
+ }
+ this._started = false;
+ if (this.verificationRequest) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ }
+ if (MatrixClientPeg.get()) {
+ MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
+ }
+ }
+
+ async usePassPhrase() {
+ this.phase = PHASE_BUSY;
+ this.emit("update");
+ const cli = MatrixClientPeg.get();
+ try {
+ const backupInfo = await cli.getKeyBackupVersion();
+ this.backupInfo = backupInfo;
+ this.emit("update");
+ // The control flow is fairly twisted here...
+ // For the purposes of completing security, we only wait on getting
+ // as far as the trust check and then show a green shield.
+ // We also begin the key backup restore as well, which we're
+ // awaiting inside `accessSecretStorage` only so that it keeps your
+ // passphase cached for that work. This dialog itself will only wait
+ // on the first trust check, and the key backup restore will happen
+ // in the background.
+ await new Promise((resolve, reject) => {
+ try {
+ accessSecretStorage(async () => {
+ await cli.checkOwnCrossSigningTrust();
+ resolve();
+ if (backupInfo) {
+ // A complete restore can take many minutes for large
+ // accounts / slow servers, so we allow the dialog
+ // to advance before this.
+ await cli.restoreKeyBackupWithSecretStorage(backupInfo);
+ }
+ }).catch(reject);
+ } catch (e) {
+ console.error(e);
+ reject(e);
+ }
+ });
+
+ if (cli.getCrossSigningId()) {
+ this.phase = PHASE_DONE;
+ this.emit("update");
+ }
+ } catch (e) {
+ if (!(e instanceof AccessCancelledError)) {
+ console.log(e);
+ }
+ // this will throw if the user hits cancel, so ignore
+ this.phase = PHASE_INTRO;
+ this.emit("update");
+ }
+ }
+
+ onVerificationRequest = async (request) => {
+ if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
+
+ if (this.verificationRequest) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ }
+ this.verificationRequest = request;
+ await request.accept();
+ request.on("change", this.onVerificationRequestChange);
+ this.emit("update");
+ }
+
+ onVerificationRequestChange = () => {
+ if (this.verificationRequest.cancelled) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ this.verificationRequest = null;
+ this.emit("update");
+ }
+ }
+
+ skip() {
+ this.phase = PHASE_CONFIRM_SKIP;
+ this.emit("update");
+ }
+
+ skipConfirm() {
+ this.phase = PHASE_FINISHED;
+ this.emit("update");
+ }
+
+ returnAfterSkip() {
+ this.phase = PHASE_INTRO;
+ this.emit("update");
+ }
+
+ done() {
+ this.phase = PHASE_FINISHED;
+ this.emit("update");
+ }
+}
diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js
index 4ed118da8a..e29b6d9b0e 100644
--- a/src/utils/StorageManager.js
+++ b/src/utils/StorageManager.js
@@ -48,6 +48,11 @@ export function tryPersistStorage() {
navigator.storage.persist().then(persistent => {
console.log("StorageManager: Persistent?", persistent);
});
+ } else if (document.requestStorageAccess) { // Safari
+ document.requestStorageAccess().then(
+ () => console.log("StorageManager: Persistent?", true),
+ () => console.log("StorageManager: Persistent?", false),
+ );
} else {
console.log("StorageManager: Persistence unsupported");
}
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 45ca7602bd..eea995cfea 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -28,6 +28,7 @@ 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";
/**
* Encodes a URI according to a set of template variables. Variables will be
@@ -422,6 +423,22 @@ export default class WidgetUtils {
app.eventId = eventId;
app.name = app.name || app.type;
+ if (app.type === 'jitsi') {
+ console.log("Replacing Jitsi widget URL with local wrapper");
+ if (!app.data || !app.data.conferenceId) {
+ // Assumed to be a v1 widget: add a data object for visibility on the wrapper
+ // TODO: Remove this once mobile supports v2 widgets
+ console.log("Replacing v1 Jitsi widget with v2 equivalent");
+ const parsed = new URL(app.url);
+ app.data = {
+ conferenceId: parsed.searchParams.get("confId"),
+ domain: "jitsi.riot.im", // v1 widgets have this hardcoded
+ };
+ }
+
+ app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
+ }
+
if (app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
@@ -430,11 +447,6 @@ export default class WidgetUtils {
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
}
- if (app.type === 'jitsi') {
- console.log("Replacing Jitsi widget URL with local wrapper");
- app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
- }
-
app.url = encodeUri(app.url, params);
return app;
@@ -443,12 +455,15 @@ export default class WidgetUtils {
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
- const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : [];
+ const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
// 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 (appType == 'jitsi') capWhitelist.push("m.always_on_screen");
+ if (appType === 'jitsi') {
+ capWhitelist.push(Capability.AlwaysOnScreen);
+ capWhitelist.push(Capability.GetRiotWebConfig);
+ }
return capWhitelist;
}
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index c19e34ae43..d6d1c79a99 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -23,6 +23,7 @@ export enum Capability {
Screenshot = "m.capability.screenshot",
Sticker = "m.sticker",
AlwaysOnScreen = "m.always_on_screen",
+ GetRiotWebConfig = "im.vector.web.riot_config",
}
export enum KnownWidgetActions {
@@ -33,7 +34,10 @@ export enum KnownWidgetActions {
UpdateVisibility = "visibility",
ReceiveOpenIDCredentials = "openid_credentials",
SetAlwaysOnScreen = "set_always_on_screen",
+ GetRiotWebConfig = "im.vector.web.riot_config",
+ ClientReady = "im.vector.ready",
}
+
export type WidgetAction = KnownWidgetActions | string;
export enum WidgetApiType {
@@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest {
*/
export class WidgetApi {
private origin: string;
- private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {};
+ private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
private readyPromise: Promise;
private readyPromiseResolve: () => void;
+ /**
+ * 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[]) {
this.origin = new URL(currentUrl).origin;
@@ -83,7 +92,14 @@ export class WidgetApi {
if (payload.action === KnownWidgetActions.GetCapabilities) {
this.onCapabilitiesRequest(payload);
+ if (!this.expectingExplicitReady) {
+ this.readyPromiseResolve();
+ }
+ } else if (payload.action === KnownWidgetActions.ClientReady) {
this.readyPromiseResolve();
+
+ // Automatically acknowledge so we can move on
+ this.replyToRequest(payload, {});
} else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
}
@@ -126,7 +142,10 @@ export class WidgetApi {
data: payload,
response: {}, // Not used at this layer - it's used when the client responds
};
- this.inFlightRequests[request.requestId] = callback;
+
+ if (callback) {
+ this.inFlightRequests[request.requestId] = callback;
+ }
console.log(`[WidgetAPI] Sending request: `, request);
window.parent.postMessage(request, "*");
@@ -134,7 +153,16 @@ export class WidgetApi {
public setAlwaysOnScreen(onScreen: boolean): Promise {
return new Promise(resolve => {
- this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve);
+ this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
+ resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
+ });
+ }
+
+ public getRiotConfig(): Promise {
+ return new Promise(resolve => {
+ this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => {
+ resolve(response.response.config);
+ });
});
}
}