Disable URL Preview setting if disabled on the homeserver (#33279)
* Disallow URL Previews if disabled by homeserver. * Cleanup nonsense * cleanup * cleanup * fixes * Remove import * Add an error handler * cleanup
This commit is contained in:
@@ -476,6 +476,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"deselect_all": "Deselect all",
|
"deselect_all": "Deselect all",
|
||||||
"device": "Device",
|
"device": "Device",
|
||||||
|
"disabled_by_homeserver": "Disabled by homeserver",
|
||||||
"edited": "edited",
|
"edited": "edited",
|
||||||
"email_address": "Email address",
|
"email_address": "Email address",
|
||||||
"emoji": "Emoji",
|
"emoji": "Emoji",
|
||||||
|
|||||||
@@ -1126,7 +1126,13 @@ export const SETTINGS: Settings = {
|
|||||||
supportedLevelsAreOrdered: true,
|
supportedLevelsAreOrdered: true,
|
||||||
displayName: _td("settings|inline_url_previews_default"),
|
displayName: _td("settings|inline_url_previews_default"),
|
||||||
default: true,
|
default: true,
|
||||||
controller: new UIFeatureController(UIFeature.URLPreviews),
|
controller: new RequiresSettingsController([UIFeature.URLPreviews], false, (c) => {
|
||||||
|
if (c["io.element.msc4452.preview_url"]?.enabled !== false) {
|
||||||
|
// If the capability is not listed, or explicitly true then do not disable.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _t("common|disabled_by_homeserver");
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
"urlPreviewsEnabled_e2ee": {
|
"urlPreviewsEnabled_e2ee": {
|
||||||
// Can only be enabled per-device to ensure neither the homeserver nor client config
|
// Can only be enabled per-device to ensure neither the homeserver nor client config
|
||||||
|
|||||||
@@ -5,30 +5,75 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingController from "./SettingController";
|
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import type { Capabilities } from "matrix-js-sdk/src/matrix";
|
||||||
import SettingsStore from "../SettingsStore";
|
import SettingsStore from "../SettingsStore";
|
||||||
import type { BooleanSettingKey } from "../Settings.tsx";
|
import type { BooleanSettingKey } from "../Settings.tsx";
|
||||||
|
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
|
||||||
|
|
||||||
|
const logger = rootLogger.getChild("RequiresSettingsController");
|
||||||
/**
|
/**
|
||||||
* Disables a setting & forces it's value if one or more settings are not enabled
|
* Disables a setting & forces it's value if one or more settings are not enabled
|
||||||
|
* and/or a capability on the client check does not pass.
|
||||||
*/
|
*/
|
||||||
export default class RequiresSettingsController extends SettingController {
|
export default class RequiresSettingsController extends MatrixClientBackedController {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly settingNames: BooleanSettingKey[],
|
public readonly settingNames: BooleanSettingKey[],
|
||||||
private forcedValue = false,
|
private readonly forcedValue = false,
|
||||||
|
/**
|
||||||
|
* Function to check the capabilites of the client.
|
||||||
|
* If defined this will be called when the MatrixClient is instantiated to check
|
||||||
|
* the returned capabilites.
|
||||||
|
* @returns `true` or a string if the feature is disabled by the feature, otherwise false.
|
||||||
|
*/
|
||||||
|
private readonly isCapabilityDisabled?: (caps: Capabilities) => boolean | string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected initMatrixClient(): void {
|
||||||
|
if (this.client && this.isCapabilityDisabled) {
|
||||||
|
// Ensure we fetch capabilies at least once.
|
||||||
|
this.client.getCapabilities().catch((ex) => {
|
||||||
|
logger.warn("Failed to fetch capabilities", ex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the `isCapabilityDisabled` function blocks the setting.
|
||||||
|
* @returns `true` or a string if the feature is disabled by the feature, otherwise false.
|
||||||
|
*/
|
||||||
|
private get isBlockedByCapabilites(): boolean | string {
|
||||||
|
if (!this.isCapabilityDisabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// This works because the cached caps are stored forever, and we have made
|
||||||
|
// at least one call to get capaibilies.
|
||||||
|
const cachedCaps = this.client?.getCachedCapabilities();
|
||||||
|
if (!cachedCaps) {
|
||||||
|
// If we do not have any capabilites yet, then assume the setting IS blocked.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.isCapabilityDisabled(cachedCaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get settingDisabled(): boolean | string {
|
||||||
|
if (this.settingNames.some((s) => !SettingsStore.getValue(s))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.isBlockedByCapabilites;
|
||||||
|
}
|
||||||
|
|
||||||
public getValueOverride(): any {
|
public getValueOverride(): any {
|
||||||
if (this.settingDisabled) {
|
if (this.settingDisabled) {
|
||||||
// per the docs: we force a disabled state when the feature isn't active
|
// per the docs: we force a disabled state when the feature isn't active
|
||||||
return this.forcedValue;
|
return this.forcedValue;
|
||||||
}
|
}
|
||||||
|
if (this.isBlockedByCapabilites) {
|
||||||
|
return this.forcedValue;
|
||||||
|
}
|
||||||
return null; // no override
|
return null; // no override
|
||||||
}
|
}
|
||||||
|
|
||||||
public get settingDisabled(): boolean {
|
|
||||||
return this.settingNames.some((s) => !SettingsStore.getValue(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
|
|||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: jest.fn(),
|
||||||
getHomeserverUrl: jest.fn(),
|
getHomeserverUrl: jest.fn(),
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
|
getCachedCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||||
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
|
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
|
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
||||||
supportsThreads: jest.fn().mockReturnValue(false),
|
supportsThreads: jest.fn().mockReturnValue(false),
|
||||||
supportsIntentionalMentions: jest.fn().mockReturnValue(false),
|
supportsIntentionalMentions: jest.fn().mockReturnValue(false),
|
||||||
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
|
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog.ts
|
|||||||
import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents";
|
import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents";
|
||||||
import { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
import { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||||
import { ModuleApi } from "../../../../src/modules/Api";
|
import { ModuleApi } from "../../../../src/modules/Api";
|
||||||
|
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController.ts";
|
||||||
import { type ComposerInsertPayload, ComposerType } from "../../../../src/dispatcher/payloads/ComposerInsertPayload.ts";
|
import { type ComposerInsertPayload, ComposerType } from "../../../../src/dispatcher/payloads/ComposerInsertPayload.ts";
|
||||||
|
|
||||||
// Used by group calls
|
// Used by group calls
|
||||||
@@ -93,6 +94,7 @@ describe("RoomView", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockPlatformPeg({ reload: () => {} });
|
mockPlatformPeg({ reload: () => {} });
|
||||||
cli = mocked(stubClient());
|
cli = mocked(stubClient());
|
||||||
|
MatrixClientBackedController.matrixClient = cli;
|
||||||
|
|
||||||
const roomName = (expect.getState().currentTestName ?? "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
const roomName = (expect.getState().currentTestName ?? "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -12,7 +12,13 @@ import userEvent from "@testing-library/user-event";
|
|||||||
|
|
||||||
import PreferencesUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab";
|
import PreferencesUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab";
|
||||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||||
import { mockPlatformPeg, stubClient } from "../../../../../../test-utils";
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mockClientMethodsServer,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
mockPlatformPeg,
|
||||||
|
stubClient,
|
||||||
|
} from "../../../../../../test-utils";
|
||||||
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
|
||||||
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
|
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
|
||||||
@@ -29,6 +35,10 @@ describe("PreferencesUserSettingsTab", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("should render", () => {
|
it("should render", () => {
|
||||||
|
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
...mockClientMethodsUser(),
|
||||||
|
});
|
||||||
const { asFragment } = renderTab();
|
const { asFragment } = renderTab();
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Capabilities } from "matrix-js-sdk/src/matrix";
|
||||||
import RequiresSettingsController from "../../../../src/settings/controllers/RequiresSettingsController";
|
import RequiresSettingsController from "../../../../src/settings/controllers/RequiresSettingsController";
|
||||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
|
||||||
|
import { getMockClientWithEventEmitter, mockClientMethodsServer } from "../../../test-utils";
|
||||||
|
|
||||||
describe("RequiresSettingsController", () => {
|
describe("RequiresSettingsController", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -30,4 +33,107 @@ describe("RequiresSettingsController", () => {
|
|||||||
expect(controller.settingDisabled).toEqual(false);
|
expect(controller.settingDisabled).toEqual(false);
|
||||||
expect(controller.getValueOverride()).toEqual(null);
|
expect(controller.getValueOverride()).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("with capabilites", () => {
|
||||||
|
let client: ReturnType<typeof getMockClientWithEventEmitter>;
|
||||||
|
beforeEach(() => {
|
||||||
|
client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getCachedCapabilities: jest.fn().mockImplementation(() => {}),
|
||||||
|
getCapabilities: jest.fn().mockRejectedValue({}),
|
||||||
|
});
|
||||||
|
MatrixClientBackedController["_matrixClient"] = client;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will disable setting if capability check is true", async () => {
|
||||||
|
const caps = {
|
||||||
|
"m.change_password": {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getCachedCapabilities.mockImplementation(() => caps);
|
||||||
|
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
|
||||||
|
expect(c).toEqual(caps);
|
||||||
|
return !c["m.change_password"]?.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that we fetch caps
|
||||||
|
controller["initMatrixClient"]();
|
||||||
|
expect(client.getCapabilities).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Test that we check caps.
|
||||||
|
expect(controller.settingDisabled).toEqual(true);
|
||||||
|
expect(controller.getValueOverride()).toEqual(false);
|
||||||
|
expect(client.getCachedCapabilities).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will not disable setting if capability check is false", async () => {
|
||||||
|
const caps = {
|
||||||
|
"m.change_password": {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getCachedCapabilities.mockImplementation(() => caps);
|
||||||
|
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
|
||||||
|
expect(c).toEqual(caps);
|
||||||
|
return !c["m.change_password"]?.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that we fetch caps
|
||||||
|
controller["initMatrixClient"]();
|
||||||
|
expect(client.getCapabilities).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Test that we check caps.
|
||||||
|
expect(controller.settingDisabled).toEqual(false);
|
||||||
|
expect(controller.getValueOverride()).toEqual(null);
|
||||||
|
expect(client.getCachedCapabilities).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will check dependency settings before checking capabilites", async () => {
|
||||||
|
const caps = {
|
||||||
|
"m.change_password": {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getCachedCapabilities.mockImplementation(() => caps);
|
||||||
|
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, false);
|
||||||
|
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => false);
|
||||||
|
|
||||||
|
// Test that we fetch caps
|
||||||
|
controller["initMatrixClient"]();
|
||||||
|
expect(client.getCapabilities).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Test that we check caps.
|
||||||
|
expect(controller.settingDisabled).toEqual(true);
|
||||||
|
expect(controller.getValueOverride()).toEqual(false);
|
||||||
|
expect(client.getCachedCapabilities).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will disable setting if capability check is true and dependency settings are true", async () => {
|
||||||
|
const caps = {
|
||||||
|
"m.change_password": {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getCachedCapabilities.mockImplementation(() => caps);
|
||||||
|
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
|
||||||
|
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => {
|
||||||
|
expect(c).toEqual(caps);
|
||||||
|
return !c["m.change_password"]?.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that we fetch caps
|
||||||
|
controller["initMatrixClient"]();
|
||||||
|
expect(client.getCapabilities).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Test that we check caps.
|
||||||
|
expect(controller.settingDisabled).toEqual(true);
|
||||||
|
expect(controller.getValueOverride()).toEqual(false);
|
||||||
|
expect(client.getCachedCapabilities).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user