Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 328e4ac

Browse files
committed
Merge branch 'refs/heads/develop' into florianduros/remove-accessibletooltipbutton
# Conflicts: # playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png # src/components/views/right_panel/RoomSummaryCard.tsx
2 parents 092c0f5 + 95ee297 commit 328e4ac

23 files changed

+524
-209
lines changed

playwright/e2e/timeline/timeline.spec.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ const sendEvent = async (client: Client, roomId: string, html = false): Promise<
7070
return client.sendEvent(roomId, null, "m.room.message" as EventType, content);
7171
};
7272

73+
const sendImage = async (
74+
client: Client,
75+
roomId: string,
76+
pngBytes: Buffer,
77+
additionalContent?: any,
78+
): Promise<ISendEventResponse> => {
79+
const upload = await client.uploadContent(pngBytes, { name: "image.png", type: "image/png" });
80+
return client.sendEvent(roomId, null, "m.room.message" as EventType, {
81+
...(additionalContent ?? {}),
82+
83+
msgtype: "m.image" as MsgType,
84+
body: "image.png",
85+
url: upload.content_uri,
86+
});
87+
};
88+
7389
test.describe("Timeline", () => {
7490
test.use({
7591
displayName: OLD_NAME,
@@ -1136,5 +1152,91 @@ test.describe("Timeline", () => {
11361152
screenshotOptions,
11371153
);
11381154
});
1155+
1156+
async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) {
1157+
await app.viewRoomById(room.roomId);
1158+
1159+
// Reinstall the service workers to clear their implicit caches (global-level stuff)
1160+
await page.evaluate(async () => {
1161+
const registrations = await window.navigator.serviceWorker.getRegistrations();
1162+
registrations.forEach((r) => r.update());
1163+
});
1164+
1165+
await sendImage(app.client, room.roomId, NEW_AVATAR);
1166+
await expect(page.locator(".mx_MImageBody").first()).toBeVisible();
1167+
1168+
// Exclude timestamp and read marker from snapshot
1169+
const screenshotOptions = {
1170+
mask: [page.locator(".mx_MessageTimestamp")],
1171+
css: `
1172+
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
1173+
display: none !important;
1174+
}
1175+
`,
1176+
};
1177+
1178+
await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot(
1179+
"image-in-timeline-default-layout.png",
1180+
screenshotOptions,
1181+
);
1182+
}
1183+
1184+
test("should render images in the timeline", async ({ page, app, room, context }) => {
1185+
await testImageRendering(page, app, room);
1186+
});
1187+
1188+
// XXX: This test doesn't actually work because the service worker relies on IndexedDB, which Playwright forces
1189+
// to be a localstorage implementation, which service workers cannot access.
1190+
// See https://github.com/microsoft/playwright/issues/11164
1191+
// See https://github.com/microsoft/playwright/issues/15684#issuecomment-2070862042
1192+
//
1193+
// In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested
1194+
// above (unless of course the above tests are also broken).
1195+
test.describe("MSC3916 - Authenticated Media", () => {
1196+
test("should render authenticated images in the timeline", async ({ page, app, room, context }) => {
1197+
// Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events.
1198+
// See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing
1199+
1200+
// Install our mocks and preventative measures
1201+
await context.route("**/_matrix/client/versions", async (route) => {
1202+
// Force enable MSC3916, which may require the service worker's internal cache to be cleared later.
1203+
const json = await (await route.fetch()).json();
1204+
if (!json["unstable_features"]) json["unstable_features"] = {};
1205+
json["unstable_features"]["org.matrix.msc3916"] = true;
1206+
await route.fulfill({ json });
1207+
});
1208+
await context.route("**/_matrix/media/*/download/**", async (route) => {
1209+
// should not be called. We don't use `abort` so that it's clearer in the logs what happened.
1210+
await route.fulfill({
1211+
status: 500,
1212+
json: { errcode: "M_UNKNOWN", error: "Unexpected route called." },
1213+
});
1214+
});
1215+
await context.route("**/_matrix/media/*/thumbnail/**", async (route) => {
1216+
// should not be called. We don't use `abort` so that it's clearer in the logs what happened.
1217+
await route.fulfill({
1218+
status: 500,
1219+
json: { errcode: "M_UNKNOWN", error: "Unexpected route called." },
1220+
});
1221+
});
1222+
await context.route("**/_matrix/client/unstable/org.matrix.msc3916/download/**", async (route) => {
1223+
expect(route.request().headers()["Authorization"]).toBeDefined();
1224+
// we can't use route.continue() because no configured homeserver supports MSC3916 yet
1225+
await route.fulfill({
1226+
body: NEW_AVATAR,
1227+
});
1228+
});
1229+
await context.route("**/_matrix/client/unstable/org.matrix.msc3916/thumbnail/**", async (route) => {
1230+
expect(route.request().headers()["Authorization"]).toBeDefined();
1231+
// we can't use route.continue() because no configured homeserver supports MSC3916 yet
1232+
await route.fulfill({
1233+
body: NEW_AVATAR,
1234+
});
1235+
});
1236+
1237+
// We check the same screenshot because there should be no user-visible impact to using authentication.
1238+
await testImageRendering(page, app, room);
1239+
});
1240+
});
11391241
});
11401242
});

playwright/element-web-test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import { Bot, CreateBotOpts } from "./pages/bot";
3333
import { ProxyInstance, SlidingSyncProxy } from "./plugins/sliding-sync-proxy";
3434
import { Webserver } from "./plugins/webserver";
3535

36+
// Enable experimental service worker support
37+
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable
38+
process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1";
39+
3640
const CONFIG_JSON: Partial<IConfigOptions> = {
3741
// This is deliberately quite a minimal config.json, so that we can test that the default settings
3842
// actually work.
Loading

src/BasePlatform.ts

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ import { CheckUpdatesPayload } from "./dispatcher/payloads/CheckUpdatesPayload";
3434
import { Action } from "./dispatcher/actions";
3535
import { hideToast as hideUpdateToast } from "./toasts/UpdateToast";
3636
import { MatrixClientPeg } from "./MatrixClientPeg";
37-
import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager";
37+
import { idbLoad, idbSave, idbDelete } from "./utils/StorageAccess";
3838
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
3939
import { IConfigOptions } from "./IConfigOptions";
4040
import SdkConfig from "./SdkConfig";
41+
import { buildAndEncodePickleKey, getPickleAdditionalData } from "./utils/tokens/pickling";
4142

4243
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
4344
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
@@ -352,55 +353,21 @@ export default abstract class BasePlatform {
352353

353354
/**
354355
* Get a previously stored pickle key. The pickle key is used for
355-
* encrypting libolm objects.
356+
* encrypting libolm objects and react-sdk-crypto data.
356357
* @param {string} userId the user ID for the user that the pickle key is for.
357-
* @param {string} userId the device ID that the pickle key is for.
358+
* @param {string} deviceId the device ID that the pickle key is for.
358359
* @returns {string|null} the previously stored pickle key, or null if no
359360
* pickle key has been stored.
360361
*/
361362
public async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
362-
if (!window.crypto || !window.crypto.subtle) {
363-
return null;
364-
}
365-
let data;
363+
let data: { encrypted?: BufferSource; iv?: BufferSource; cryptoKey?: CryptoKey } | undefined;
366364
try {
367365
data = await idbLoad("pickleKey", [userId, deviceId]);
368366
} catch (e) {
369367
logger.error("idbLoad for pickleKey failed", e);
370368
}
371-
if (!data) {
372-
return null;
373-
}
374-
if (!data.encrypted || !data.iv || !data.cryptoKey) {
375-
logger.error("Badly formatted pickle key");
376-
return null;
377-
}
378-
379-
const additionalData = this.getPickleAdditionalData(userId, deviceId);
380369

381-
try {
382-
const key = await crypto.subtle.decrypt(
383-
{ name: "AES-GCM", iv: data.iv, additionalData },
384-
data.cryptoKey,
385-
data.encrypted,
386-
);
387-
return encodeUnpaddedBase64(key);
388-
} catch (e) {
389-
logger.error("Error decrypting pickle key");
390-
return null;
391-
}
392-
}
393-
394-
private getPickleAdditionalData(userId: string, deviceId: string): Uint8Array {
395-
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
396-
for (let i = 0; i < userId.length; i++) {
397-
additionalData[i] = userId.charCodeAt(i);
398-
}
399-
additionalData[userId.length] = 124; // "|"
400-
for (let i = 0; i < deviceId.length; i++) {
401-
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
402-
}
403-
return additionalData;
370+
return (await buildAndEncodePickleKey(data, userId, deviceId)) ?? null;
404371
}
405372

406373
/**
@@ -424,7 +391,7 @@ export default abstract class BasePlatform {
424391
const iv = new Uint8Array(32);
425392
crypto.getRandomValues(iv);
426393

427-
const additionalData = this.getPickleAdditionalData(userId, deviceId);
394+
const additionalData = getPickleAdditionalData(userId, deviceId);
428395
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray);
429396

430397
try {

src/Lifecycle.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import ActiveWidgetStore from "./stores/ActiveWidgetStore";
3737
import PlatformPeg from "./PlatformPeg";
3838
import { sendLoginRequest } from "./Login";
3939
import * as StorageManager from "./utils/StorageManager";
40+
import * as StorageAccess from "./utils/StorageAccess";
4041
import SettingsStore from "./settings/SettingsStore";
4142
import { SettingLevel } from "./settings/SettingLevel";
4243
import ToastStore from "./stores/ToastStore";
@@ -493,7 +494,7 @@ export interface IStoredSession {
493494
async function getStoredToken(storageKey: string): Promise<string | undefined> {
494495
let token: string | undefined;
495496
try {
496-
token = await StorageManager.idbLoad("account", storageKey);
497+
token = await StorageAccess.idbLoad("account", storageKey);
497498
} catch (e) {
498499
logger.error(`StorageManager.idbLoad failed for account:${storageKey}`, e);
499500
}
@@ -502,7 +503,7 @@ async function getStoredToken(storageKey: string): Promise<string | undefined> {
502503
if (token) {
503504
try {
504505
// try to migrate access token to IndexedDB if we can
505-
await StorageManager.idbSave("account", storageKey, token);
506+
await StorageAccess.idbSave("account", storageKey, token);
506507
localStorage.removeItem(storageKey);
507508
} catch (e) {
508509
logger.error(`migration of token ${storageKey} to IndexedDB failed`, e);
@@ -1064,7 +1065,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
10641065
AbstractLocalStorageSettingsHandler.clear();
10651066

10661067
try {
1067-
await StorageManager.idbDelete("account", ACCESS_TOKEN_STORAGE_KEY);
1068+
await StorageAccess.idbDelete("account", ACCESS_TOKEN_STORAGE_KEY);
10681069
} catch (e) {
10691070
logger.error("idbDelete failed for account:mx_access_token", e);
10701071
}

src/SlidingSyncManager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,11 @@ export class SlidingSyncManager {
378378
*/
379379
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
380380
try {
381-
await client.http.authedRequest<void>(Method.Post, "/sync", undefined, undefined, {
381+
// We use OPTIONS to avoid causing a real sync to happen, as that may be intensive or encourage
382+
// middleware software to start polling as our access token (thus stealing our to-device messages).
383+
// See https://github.com/element-hq/element-web/issues/27426
384+
// XXX: Using client.http is a bad thing - it's meant to be private access. See `client.http` for details.
385+
await client.http.authedRequest<void>(Method.Options, "/sync", undefined, undefined, {
382386
localTimeoutMs: 10 * 1000, // 10s
383387
prefix: "/_matrix/client/unstable/org.matrix.msc3575",
384388
});

src/components/views/right_panel/RoomSummaryCard.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,8 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
187187
className="mx_RoomSummaryCard_icon_app"
188188
onClick={onOpenWidgetClick}
189189
// only show a tooltip if the widget is pinned
190-
title={openTitle}
190+
title={!(isPinned || isMaximised) ? undefined : openTitle}
191191
disabled={isPinned || isMaximised}
192-
placement="right"
193192
>
194193
<WidgetAvatar app={app} size="20px" />
195194
<span>{name}</span>
@@ -210,13 +209,11 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
210209
onClick={togglePin}
211210
title={pinTitle}
212211
disabled={cannotPin}
213-
placement="right"
214212
/>
215213
<AccessibleButton
216214
className="mx_RoomSummaryCard_app_maximiseToggle"
217215
onClick={toggleMaximised}
218216
title={maximiseTitle}
219-
placement="right"
220217
/>
221218

222219
{contextMenu}

0 commit comments

Comments
 (0)