Skip to content

Commit

Permalink
Harden Settings using mapped types (#28775)
Browse files Browse the repository at this point in the history
* Harden Settings using mapped types

Signed-off-by: Michael Telatynski <[email protected]>

* Fix issues found during hardening

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Remove oidc native flow stale key

Signed-off-by: Michael Telatynski <[email protected]>

---------

Signed-off-by: Michael Telatynski <[email protected]>
  • Loading branch information
t3chguy authored Dec 23, 2024
1 parent 4e1bd69 commit 1e42f28
Show file tree
Hide file tree
Showing 90 changed files with 576 additions and 274 deletions.
4 changes: 4 additions & 0 deletions scripts/analyse_unused_exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ ignore.push("/OpenSpotlightPayload.ts");
ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.ts");
ignore.push("/json.ts");
ignore.push("/ReleaseAnnouncementStore.ts");
ignore.push("/WidgetLayoutStore.ts");
ignore.push("/common.ts");

// We ignore js-sdk by default as it may export for other non element-web projects
if (!includeJSSDK) ignore.push("matrix-js-sdk");
Expand Down
8 changes: 8 additions & 0 deletions src/@types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ type DeepReadonlyObject<T> = {
};

export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];

/**
* Returns a union type of the keys of the input Object type whose values are assignable to the given Item type.
* Based on https://stackoverflow.com/a/57862073
*/
export type Assignable<Object, Item> = {
[Key in keyof Object]: Object[Key] extends Item ? Key : never;
}[keyof Object];
13 changes: 13 additions & 0 deletions src/@types/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

export type JsonValue = null | string | number | boolean;
export type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
export interface JsonObject {
[key: string]: JsonObject | JsonArray | JsonValue;
}
export type Json = JsonArray | JsonObject;
2 changes: 1 addition & 1 deletion src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
url: string;
name: string;
type: string;
size: string;
size: number;
} | null {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class LoggedInView extends React.Component<IProps, IState> {
} else {
backgroundImage = OwnProfileStore.instance.getHttpAvatarUrl();
}
this.setState({ backgroundImage });
this.setState({ backgroundImage: backgroundImage ?? undefined });
};

public canResetTimelineInRoom = (roomId: string): boolean => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/beta/BetaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import SettingsFlag from "../elements/SettingsFlag";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import InlineSpinner from "../elements/InlineSpinner";
import { shouldShowFeedback } from "../../../utils/Feedback";
import { FeatureSettingKey } from "../../../settings/Settings.tsx";

// XXX: Keep this around for re-use in future Betas

interface IProps {
title?: string;
featureId: string;
featureId: FeatureSettingKey;
}

interface IBetaPillProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
}
})();

const developerModeEnabled = useSettingValue<boolean>("developerMode");
const developerModeEnabled = useSettingValue("developerMode");
const developerToolsOption = developerModeEnabled ? (
<DeveloperToolsOption onFinished={onFinished} roomId={room.roomId} />
) : null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/context_menus/WidgetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() =>

const showSnapshotButton = (widgetMessaging: ClientWidgetApi | undefined): boolean => {
return (
SettingsStore.getValue<boolean>("enableWidgetScreenshots") &&
SettingsStore.getValue("enableWidgetScreenshots") &&
!!widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
const msc3946ProcessDynamicPredecessor = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors");
const visibleRooms = useMemo(
() =>
cli
Expand Down
5 changes: 3 additions & 2 deletions src/components/views/dialogs/BetaFeedbackDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
import { SettingKey } from "../../../settings/Settings.tsx";

// XXX: Keep this around for re-use in future Betas

interface IProps {
featureId: string;
featureId: SettingKey;
onFinished(sendFeedback?: boolean): void;
}

Expand All @@ -35,7 +36,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
rageshakeLabel={info.feedbackLabel}
rageshakeData={Object.fromEntries(
(SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map((k) => {
return SettingsStore.getValue(k);
return [k, SettingsStore.getValue(k)];
}),
)}
>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/ForwardDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();

const previewLayout = useSettingValue<Layout>("layout");
const msc3946DynamicRoomPredecessors = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const previewLayout = useSettingValue("layout");
const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors");

let rooms = useMemo(
() =>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ type ShareDialogProps = XOR<Props, EventProps>;
* A dialog to share a link to a room, user, room member or a matrix event.
*/
export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element {
const showQrCode = useSettingValue<boolean>(UIFeature.ShareQRCode);
const showSocials = useSettingValue<boolean>(UIFeature.ShareSocial);
const showQrCode = useSettingValue(UIFeature.ShareQRCode);
const showSocials = useSettingValue(UIFeature.ShareSocial);

const timeoutIdRef = useRef<number>();
const [isCopied, setIsCopied] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/UserSettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
}

export default function UserSettingsDialog(props: IProps): JSX.Element {
const voipEnabled = useSettingValue<boolean>(UIFeature.Voip);
const mjolnirEnabled = useSettingValue<boolean>("feature_mjolnir");
const voipEnabled = useSettingValue(UIFeature.Voip);
const mjolnirEnabled = useSettingValue("feature_mjolnir");
// store this prop in state as changing tabs back and forth should clear it
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);

Expand Down
20 changes: 10 additions & 10 deletions src/components/views/dialogs/devtools/SettingExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsStore, { LEVEL_ORDER } from "../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../settings/SettingLevel";
import { SETTINGS } from "../../../../settings/Settings";
import { SettingKey, SETTINGS, SettingValueType } from "../../../../settings/Settings";
import Field from "../../elements/Field";

const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
const [setting, setSetting] = useState<string | null>(null);
const [setting, setSetting] = useState<SettingKey | null>(null);
const [editing, setEditing] = useState(false);

if (setting && editing) {
Expand All @@ -36,10 +36,10 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
};
return <ViewSetting setting={setting} onBack={onBack} onEdit={onEdit} />;
} else {
const onView = (setting: string): void => {
const onView = (setting: SettingKey): void => {
setSetting(setting);
};
const onEdit = (setting: string): void => {
const onEdit = (setting: SettingKey): void => {
setSetting(setting);
setEditing(true);
};
Expand All @@ -50,7 +50,7 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
export default SettingExplorer;

interface ICanEditLevelFieldProps {
setting: string;
setting: SettingKey;
level: SettingLevel;
roomId?: string;
}
Expand All @@ -65,8 +65,8 @@ const CanEditLevelField: React.FC<ICanEditLevelFieldProps> = ({ setting, roomId,
);
};

function renderExplicitSettingValues(setting: string, roomId?: string): string {
const vals: Record<string, number | null> = {};
function renderExplicitSettingValues(setting: SettingKey, roomId?: string): string {
const vals: Record<string, SettingValueType> = {};
for (const level of LEVEL_ORDER) {
try {
vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
Expand All @@ -81,7 +81,7 @@ function renderExplicitSettingValues(setting: string, roomId?: string): string {
}

interface IEditSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
}

const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
Expand Down Expand Up @@ -191,7 +191,7 @@ const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
};

interface IViewSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
onEdit(): Promise<void>;
}

Expand Down Expand Up @@ -258,7 +258,7 @@ const SettingsList: React.FC<ISettingsListProps> = ({ onBack, onView, onEdit })
const [query, setQuery] = useState("");

const allSettings = useMemo(() => {
let allSettings = Object.keys(SETTINGS);
let allSettings = Object.keys(SETTINGS) as SettingKey[];
if (query) {
const lcQuery = query.toLowerCase();
allSettings = allSettings.filter((setting) => setting.toLowerCase().includes(lcQuery));
Expand Down
40 changes: 30 additions & 10 deletions src/components/views/directory/NetworkDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import TextInputDialog from "../dialogs/TextInputDialog";
import AccessibleButton from "../elements/AccessibleButton";
import withValidation from "../elements/Validation";
import { SettingKey, Settings } from "../../../settings/Settings.tsx";

const SETTING_NAME = "room_directory_servers";

Expand Down Expand Up @@ -67,15 +68,32 @@ const validServer = withValidation<undefined, { error?: unknown }>({
memoize: true,
});

function useSettingsValueWithSetter<T>(
settingName: string,
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null,
excludeDefault: true,
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId?: string | null,
excludeDefault?: false,
): [Settings[S]["default"], (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null = null,
excludeDefault = false,
): [T, (value: T) => Promise<void>] {
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId ?? undefined, excludeDefault));
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>] {
const [value, setValue] = useState(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
const setter = useCallback(
async (value: T): Promise<void> => {
async (value: Settings[S]["default"]): Promise<void> => {
setValue(value);
SettingsStore.setValue(settingName, roomId, level, value);
},
Expand All @@ -84,7 +102,12 @@ function useSettingsValueWithSetter<T>(

useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
setValue(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
});
// clean-up
return () => {
Expand All @@ -109,10 +132,7 @@ function removeAll<T>(target: Set<T>, ...toRemove: T[]): void {
}

function useServers(): ServerList {
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter<string[]>(
SETTING_NAME,
SettingLevel.ACCOUNT,
);
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter(SETTING_NAME, SettingLevel.ACCOUNT);

const homeServer = MatrixClientPeg.safeGet().getDomain()!;
const configServers = new Set<string>(SdkConfig.getObject("room_directory")?.get("servers") ?? []);
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/LanguageDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {

// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/elements/SettingsFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { _t } from "../../../languageHandler";
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
import { defaultWatchManager } from "../../../settings/Settings";
import { BooleanSettingKey, defaultWatchManager } from "../../../settings/Settings";

interface IProps {
// The setting must be a boolean
name: string;
name: BooleanSettingKey;
level: SettingLevel;
roomId?: string; // for per-room settings
label?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<

// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ const ExpandCollapseButton: React.FC<{
};

const CodeBlock: React.FC<Props> = ({ children, onHeightChanged }) => {
const enableSyntaxHighlightLanguageDetection = useSettingValue<boolean>("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue<boolean>("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue<boolean>("expandCodeByDefault");
const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue("expandCodeByDefault");
const [expanded, setExpanded] = useState(expandCodeByDefault);

let expandCollapseButton: JSX.Element | undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/TextualBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);

const htmlOpts = {
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above

const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
let isRichTextEnabled = true;
let initialComposerContent = "";
if (isWysiwygLabEnabled) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/MessageComposerButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient = useContext(MatrixClientContext);
const { room, narrow } = useScopedRoomContext("room", "narrow");

const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer");

if (!matrixClient || !room || props.haveRecording) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function PlainTextComposer({
rightComponent,
eventRelation,
}: PlainTextComposerProps): JSX.Element {
const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const {
ref: editorRef,
autocompleteRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({

const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);

const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const emojiSuggestions = useMemo(() => getEmojiSuggestions(isAutoReplaceEmojiEnabled), [isAutoReplaceEmojiEnabled]);

const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useInputEventProcessor(
const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType");
const composerContext = useComposerContext();
const mxClient = useMatrixClientContext();
const isCtrlEnterToSend = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");

return useCallback(
(event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => {
Expand Down
Loading

0 comments on commit 1e42f28

Please sign in to comment.