Merge commit from fork
and migrate existing account-level settings once-only for existing sessions.dbkr/sss
							parent
							
								
									5dda51f95c
								
							
						
					
					
						commit
						c7bbc1c045
					
				| 
						 | 
				
			
			@ -102,7 +102,7 @@ dis.register((payload) => {
 | 
			
		|||
        // If we unset the client and the component is updated,  the render will fail and unmount everything.
 | 
			
		||||
        // (The module dialog closes and fires a `aria_unhide_main_app` that will trigger a re-render)
 | 
			
		||||
        stopMatrixClient(false);
 | 
			
		||||
        doSetLoggedIn(typed.credentials, true).catch((e) => {
 | 
			
		||||
        doSetLoggedIn(typed.credentials, true, true).catch((e) => {
 | 
			
		||||
            // XXX we might want to fire a new event here to let the app know that the login failed ?
 | 
			
		||||
            // The module api could use it to display a message to the user.
 | 
			
		||||
            logger.warn("Failed to overwrite login", e);
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +208,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
 | 
			
		|||
                    guest: true,
 | 
			
		||||
                },
 | 
			
		||||
                true,
 | 
			
		||||
                false,
 | 
			
		||||
            ).then(() => true);
 | 
			
		||||
        }
 | 
			
		||||
        const success = await restoreFromLocalStorage({
 | 
			
		||||
| 
						 | 
				
			
			@ -465,6 +466,7 @@ function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName
 | 
			
		|||
                        guest: true,
 | 
			
		||||
                    },
 | 
			
		||||
                    true,
 | 
			
		||||
                    true,
 | 
			
		||||
                ).then(() => true);
 | 
			
		||||
            },
 | 
			
		||||
            (err) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -610,6 +612,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
 | 
			
		|||
                freshLogin: freshLogin,
 | 
			
		||||
            },
 | 
			
		||||
            false,
 | 
			
		||||
            false,
 | 
			
		||||
        );
 | 
			
		||||
        return true;
 | 
			
		||||
    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -663,7 +666,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
 | 
			
		|||
        logger.log("Pickle key not created");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true);
 | 
			
		||||
    return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -700,7 +703,7 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise<M
 | 
			
		|||
            (await PlatformPeg.get()?.getPickleKey(credentials.userId, credentials.deviceId)) ?? undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return doSetLoggedIn(credentials, overwrite);
 | 
			
		||||
    return doSetLoggedIn(credentials, overwrite, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -746,12 +749,17 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis
 | 
			
		|||
 * optionally clears localstorage, persists new credentials
 | 
			
		||||
 * to localstorage, starts the new client.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {IMatrixClientCreds} credentials
 | 
			
		||||
 * @param {Boolean} clearStorageEnabled
 | 
			
		||||
 * @param {IMatrixClientCreds} credentials The credentials to use
 | 
			
		||||
 * @param {Boolean} clearStorageEnabled True to clear storage before starting the new client
 | 
			
		||||
 * @param {Boolean} isFreshLogin True if this is a fresh login, false if it is previous session being restored
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
 | 
			
		||||
 */
 | 
			
		||||
async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise<MatrixClient> {
 | 
			
		||||
async function doSetLoggedIn(
 | 
			
		||||
    credentials: IMatrixClientCreds,
 | 
			
		||||
    clearStorageEnabled: boolean,
 | 
			
		||||
    isFreshLogin: boolean,
 | 
			
		||||
): Promise<MatrixClient> {
 | 
			
		||||
    checkSessionLock();
 | 
			
		||||
    credentials.guest = Boolean(credentials.guest);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -840,6 +848,9 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable
 | 
			
		|||
        clientPegOpts.rustCryptoStoreKey?.fill(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run the migrations after the MatrixClientPeg has been assigned
 | 
			
		||||
    SettingsStore.runMigrations(isFreshLogin);
 | 
			
		||||
 | 
			
		||||
    return client;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1020,9 +1031,6 @@ async function startMatrixClient(
 | 
			
		|||
 | 
			
		||||
    checkSessionLock();
 | 
			
		||||
 | 
			
		||||
    // Run the migrations after the MatrixClientPeg has been assigned
 | 
			
		||||
    SettingsStore.runMigrations();
 | 
			
		||||
 | 
			
		||||
    // This needs to be started after crypto is set up
 | 
			
		||||
    DeviceListener.sharedInstance().start(client);
 | 
			
		||||
    // Similarly, don't start sending presence updates until we've started
 | 
			
		||||
| 
						 | 
				
			
			@ -1165,5 +1173,6 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
 | 
			
		|||
            userId,
 | 
			
		||||
        },
 | 
			
		||||
        true,
 | 
			
		||||
        false,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
 | 
			
		|||
            (
 | 
			
		||||
                <SettingsFlag
 | 
			
		||||
                    name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
 | 
			
		||||
                    level={SettingLevel.ROOM_ACCOUNT}
 | 
			
		||||
                    level={SettingLevel.ROOM_DEVICE}
 | 
			
		||||
                    roomId={roomId}
 | 
			
		||||
                />
 | 
			
		||||
            );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -900,7 +900,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
 | 
			
		|||
        controller: new UIFeatureController(UIFeature.URLPreviews),
 | 
			
		||||
    },
 | 
			
		||||
    "urlPreviewsEnabled_e2ee": {
 | 
			
		||||
        supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_ACCOUNT],
 | 
			
		||||
        supportedLevels: [SettingLevel.ROOM_DEVICE],
 | 
			
		||||
        displayName: {
 | 
			
		||||
            "room-account": _td("settings|inline_url_previews_room_account"),
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
import { logger } from "matrix-js-sdk/src/logger";
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix";
 | 
			
		||||
 | 
			
		||||
import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
 | 
			
		||||
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +37,7 @@ import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayl
 | 
			
		|||
import { Action } from "../dispatcher/actions";
 | 
			
		||||
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
 | 
			
		||||
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
 | 
			
		||||
import { MatrixClientPeg } from "../MatrixClientPeg";
 | 
			
		||||
 | 
			
		||||
// Convert the settings to easier to manage objects for the handlers
 | 
			
		||||
const defaultSettings: Record<string, any> = {};
 | 
			
		||||
| 
						 | 
				
			
			@ -637,10 +639,61 @@ export default class SettingsStore {
 | 
			
		|||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Migrate the setting for URL previews in e2e rooms from room account
 | 
			
		||||
     * data to the room device level.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isFreshLogin True if the user has just logged in, false if a previous session is being restored.
 | 
			
		||||
     */
 | 
			
		||||
    private static async migrateURLPreviewsE2EE(isFreshLogin: boolean): Promise<void> {
 | 
			
		||||
        const MIGRATION_DONE_FLAG = "url_previews_e2ee_migration_done";
 | 
			
		||||
        if (localStorage.getItem(MIGRATION_DONE_FLAG)) return;
 | 
			
		||||
        if (isFreshLogin) return;
 | 
			
		||||
 | 
			
		||||
        const client = MatrixClientPeg.safeGet();
 | 
			
		||||
 | 
			
		||||
        const doMigration = async (): Promise<void> => {
 | 
			
		||||
            logger.info("Performing one-time settings migration of URL previews in E2EE rooms");
 | 
			
		||||
 | 
			
		||||
            const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT];
 | 
			
		||||
 | 
			
		||||
            for (const room of client.getRooms()) {
 | 
			
		||||
                // We need to use the handler directly because this setting is no longer supported
 | 
			
		||||
                // at this level at all
 | 
			
		||||
                const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId);
 | 
			
		||||
 | 
			
		||||
                if (val !== undefined) {
 | 
			
		||||
                    await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            localStorage.setItem(MIGRATION_DONE_FLAG, "true");
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const onSync = (state: SyncState): void => {
 | 
			
		||||
            if (state === SyncState.Prepared) {
 | 
			
		||||
                client.removeListener(ClientEvent.Sync, onSync);
 | 
			
		||||
 | 
			
		||||
                doMigration().catch((e) => {
 | 
			
		||||
                    logger.error("Failed to migrate URL previews in E2EE rooms:", e);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        client.on(ClientEvent.Sync, onSync);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Runs or queues any setting migrations needed.
 | 
			
		||||
     */
 | 
			
		||||
    public static runMigrations(): void {
 | 
			
		||||
    public static runMigrations(isFreshLogin: boolean): void {
 | 
			
		||||
        // This can be removed once enough users have run a version of Element with
 | 
			
		||||
        // this migration. A couple of months after its release should be sufficient
 | 
			
		||||
        // (so around October 2024).
 | 
			
		||||
        // The consequences of missing the migration are only that URL previews will
 | 
			
		||||
        // be disabled in E2EE rooms.
 | 
			
		||||
        SettingsStore.migrateURLPreviewsE2EE(isFreshLogin);
 | 
			
		||||
 | 
			
		||||
        // Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
 | 
			
		||||
        // add a comment to note when it can be removed.
 | 
			
		||||
        return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import { ClientEvent, MatrixClient, Room, SyncState } from "matrix-js-sdk";
 | 
			
		||||
import { localstorage } from "modernizr";
 | 
			
		||||
 | 
			
		||||
import BasePlatform from "../../src/BasePlatform";
 | 
			
		||||
import SdkConfig from "../../src/SdkConfig";
 | 
			
		||||
import { SettingLevel } from "../../src/settings/SettingLevel";
 | 
			
		||||
import SettingsStore from "../../src/settings/SettingsStore";
 | 
			
		||||
import { mockPlatformPeg } from "../test-utils";
 | 
			
		||||
import { mkStubRoom, mockPlatformPeg, stubClient } from "../test-utils";
 | 
			
		||||
 | 
			
		||||
const TEST_DATA = [
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,4 +87,65 @@ describe("SettingsStore", () => {
 | 
			
		|||
            expect(SettingsStore.getValueAt(SettingLevel.DEVICE, SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("runMigrations", () => {
 | 
			
		||||
        let client: MatrixClient;
 | 
			
		||||
        let room: Room;
 | 
			
		||||
        let localStorageSetItemSpy: jest.SpyInstance;
 | 
			
		||||
        let localStorageSetPromise: Promise<void>;
 | 
			
		||||
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            client = stubClient();
 | 
			
		||||
            room = mkStubRoom("!room:example.org", "Room", client);
 | 
			
		||||
            room.getAccountData = jest.fn().mockReturnValue({
 | 
			
		||||
                getContent: jest.fn().mockReturnValue({
 | 
			
		||||
                    urlPreviewsEnabled_e2ee: true,
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
            client.getRooms = jest.fn().mockReturnValue([room]);
 | 
			
		||||
            client.getRoom = jest.fn().mockReturnValue(room);
 | 
			
		||||
 | 
			
		||||
            localStorageSetPromise = new Promise((resolve) => {
 | 
			
		||||
                localStorageSetItemSpy = jest
 | 
			
		||||
                    .spyOn(localStorage.__proto__, "setItem")
 | 
			
		||||
                    .mockImplementation(() => resolve());
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(() => {
 | 
			
		||||
            jest.restoreAllMocks();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("migrates URL previews setting for e2ee rooms", async () => {
 | 
			
		||||
            SettingsStore.runMigrations(false);
 | 
			
		||||
            client.emit(ClientEvent.Sync, SyncState.Prepared, null);
 | 
			
		||||
 | 
			
		||||
            expect(room.getAccountData).toHaveBeenCalled();
 | 
			
		||||
 | 
			
		||||
            await localStorageSetPromise;
 | 
			
		||||
 | 
			
		||||
            expect(localStorageSetItemSpy!).toHaveBeenCalledWith(
 | 
			
		||||
                `mx_setting_urlPreviewsEnabled_e2ee_${room.roomId}`,
 | 
			
		||||
                JSON.stringify({ value: true }),
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("does not migrate e2ee URL previews on a fresh login", async () => {
 | 
			
		||||
            SettingsStore.runMigrations(true);
 | 
			
		||||
            client.emit(ClientEvent.Sync, SyncState.Prepared, null);
 | 
			
		||||
 | 
			
		||||
            expect(room.getAccountData).not.toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("does not migrate if the device is flagged as migrated", async () => {
 | 
			
		||||
            jest.spyOn(localStorage.__proto__, "getItem").mockImplementation((key: unknown): string | undefined => {
 | 
			
		||||
                if (key === "url_previews_e2ee_migration_done") return JSON.stringify({ value: true });
 | 
			
		||||
                return undefined;
 | 
			
		||||
            });
 | 
			
		||||
            SettingsStore.runMigrations(false);
 | 
			
		||||
            client.emit(ClientEvent.Sync, SyncState.Prepared, null);
 | 
			
		||||
 | 
			
		||||
            expect(room.getAccountData).not.toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue