Skip to content

Commit 461185b

Browse files
committed
Disallow URL Previews if disabled by homeserver.
1 parent 12df09b commit 461185b

4 files changed

Lines changed: 153 additions & 8 deletions

File tree

apps/web/src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@
476476
"description": "Description",
477477
"deselect_all": "Deselect all",
478478
"device": "Device",
479+
"disabled_by_homeserver": "Disabled by homeserver",
479480
"edited": "edited",
480481
"email_address": "Email address",
481482
"emoji": "Emoji",

apps/web/src/settings/Settings.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,16 @@ export const SETTINGS: Settings = {
11501150
supportedLevelsAreOrdered: true,
11511151
displayName: _td("settings|inline_url_previews_default"),
11521152
default: true,
1153-
controller: new UIFeatureController(UIFeature.URLPreviews),
1153+
controller: new RequiresSettingsController([UIFeature.URLPreviews], false, (c) => {
1154+
if (
1155+
c["io.element.msc4452.preview_url"]?.enabled === undefined ||
1156+
c["io.element.msc4452.preview_url"].enabled
1157+
) {
1158+
// If there is no capability, assume true.
1159+
return false;
1160+
}
1161+
return _t("common|disabled_by_homeserver");
1162+
}),
11541163
},
11551164
"urlPreviewsEnabled_e2ee": {
11561165
// Can only be enabled per-device to ensure neither the homeserver nor client config

apps/web/src/settings/controllers/RequiresSettingsController.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,66 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import SettingController from "./SettingController";
8+
import type { Capabilities } from "matrix-js-sdk/src/matrix";
99
import SettingsStore from "../SettingsStore";
1010
import type { BooleanSettingKey } from "../Settings.tsx";
11+
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
1112

1213
/**
1314
* Disables a setting & forces it's value if one or more settings are not enabled
15+
* and/or a capability on the client check does not pass.
1416
*/
15-
export default class RequiresSettingsController extends SettingController {
17+
export default class RequiresSettingsController extends MatrixClientBackedController {
1618
public constructor(
1719
public readonly settingNames: BooleanSettingKey[],
18-
private forcedValue = false,
20+
private readonly forcedValue = false,
21+
/**
22+
* Function to check the capabilites of the client.
23+
* If defined this will be called when the MatrixClient is instantiated to check
24+
* the returned capabilites.
25+
* @returns `true` or a string if the feature is disabled by the feature, otherwise false.
26+
*/
27+
private readonly isCapabilityDisabled?: (caps: Capabilities) => boolean | string,
1928
) {
2029
super();
2130
}
2231

32+
protected initMatrixClient(): void {
33+
if (this.client && this.isCapabilityDisabled) {
34+
// Ensure we fetch capabilies at least once.
35+
void this.client.getCapabilities();
36+
}
37+
}
38+
39+
private get isBlockedByCapabilites(): boolean | string {
40+
if (!this.isCapabilityDisabled) {
41+
return false;
42+
}
43+
// This works because the cached caps are stored forever, and we have made
44+
// at least one call to get capaibilies.
45+
const cachedCaps = this.client?.getCachedCapabilities();
46+
if (!cachedCaps) {
47+
// return forced value if caps aren't available yet.
48+
return true;
49+
}
50+
return this.isCapabilityDisabled(cachedCaps);
51+
}
52+
53+
public get settingDisabled(): boolean | string {
54+
if (this.settingNames.some((s) => !SettingsStore.getValue(s))) {
55+
return true;
56+
}
57+
return this.isBlockedByCapabilites;
58+
}
59+
2360
public getValueOverride(): any {
2461
if (this.settingDisabled) {
2562
// per the docs: we force a disabled state when the feature isn't active
2663
return this.forcedValue;
2764
}
65+
if (this.isBlockedByCapabilites) {
66+
return this.forcedValue;
67+
}
2868
return null; // no override
2969
}
30-
31-
public get settingDisabled(): boolean {
32-
return this.settingNames.some((s) => !SettingsStore.getValue(s));
33-
}
3470
}

apps/web/test/unit-tests/settings/controllers/RequiresSettingsController-test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8+
import type { Capabilities } from "matrix-js-sdk/src/matrix";
89
import RequiresSettingsController from "../../../../src/settings/controllers/RequiresSettingsController";
910
import { SettingLevel } from "../../../../src/settings/SettingLevel";
1011
import SettingsStore from "../../../../src/settings/SettingsStore";
12+
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
13+
import { getMockClientWithEventEmitter, mockClientMethodsServer } from "../../../test-utils";
1114

1215
describe("RequiresSettingsController", () => {
1316
afterEach(() => {
@@ -30,4 +33,100 @@ describe("RequiresSettingsController", () => {
3033
expect(controller.settingDisabled).toEqual(false);
3134
expect(controller.getValueOverride()).toEqual(null);
3235
});
36+
37+
describe("with capabilites", () => {
38+
let caps: Capabilities;
39+
let client: ReturnType<typeof getMockClientWithEventEmitter>;
40+
beforeEach(() => {
41+
client = getMockClientWithEventEmitter({
42+
...mockClientMethodsServer(),
43+
getCachedCapabilities: jest.fn().mockImplementation(() => caps!),
44+
getCapabilities: jest.fn(),
45+
});
46+
MatrixClientBackedController["_matrixClient"] = client;
47+
});
48+
49+
it("will disable setting if capability check is true", async () => {
50+
caps = {
51+
"m.change_password": {
52+
enabled: false,
53+
},
54+
};
55+
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
56+
expect(c).toEqual(caps);
57+
return !c["m.change_password"]?.enabled;
58+
});
59+
60+
// Test that we fetch caps
61+
controller["initMatrixClient"]();
62+
expect(client.getCapabilities).toHaveBeenCalled();
63+
64+
// Test that we check caps.
65+
expect(controller.settingDisabled).toEqual(true);
66+
expect(controller.getValueOverride()).toEqual(false);
67+
expect(client.getCachedCapabilities).toHaveBeenCalled();
68+
});
69+
70+
it("will not disable setting if capability check is false", async () => {
71+
caps = {
72+
"m.change_password": {
73+
enabled: true,
74+
},
75+
};
76+
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
77+
expect(c).toEqual(caps);
78+
return !c["m.change_password"]?.enabled;
79+
});
80+
81+
// Test that we fetch caps
82+
controller["initMatrixClient"]();
83+
expect(client.getCapabilities).toHaveBeenCalled();
84+
85+
// Test that we check caps.
86+
expect(controller.settingDisabled).toEqual(false);
87+
expect(controller.getValueOverride()).toEqual(null);
88+
expect(client.getCachedCapabilities).toHaveBeenCalled();
89+
});
90+
91+
it("will check dependency settings before checking capabilites", async () => {
92+
caps = {
93+
"m.change_password": {
94+
enabled: false,
95+
},
96+
};
97+
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, false);
98+
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => false);
99+
100+
// Test that we fetch caps
101+
controller["initMatrixClient"]();
102+
expect(client.getCapabilities).toHaveBeenCalled();
103+
104+
// Test that we check caps.
105+
expect(controller.settingDisabled).toEqual(true);
106+
expect(controller.getValueOverride()).toEqual(false);
107+
expect(client.getCachedCapabilities).not.toHaveBeenCalled();
108+
});
109+
110+
it("will disable setting if capability check is true and dependency settings are true", async () => {
111+
caps = {
112+
"m.change_password": {
113+
enabled: false,
114+
},
115+
};
116+
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
117+
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => {
118+
expect(c).toEqual(caps);
119+
return !c["m.change_password"]?.enabled;
120+
});
121+
122+
// Test that we fetch caps
123+
controller["initMatrixClient"]();
124+
expect(client.getCapabilities).toHaveBeenCalled();
125+
126+
// Test that we check caps.
127+
expect(controller.settingDisabled).toEqual(true);
128+
expect(controller.getValueOverride()).toEqual(false);
129+
expect(client.getCachedCapabilities).toHaveBeenCalled();
130+
});
131+
});
33132
});

0 commit comments

Comments
 (0)