Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions packages/components/src/services/hds-theming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,42 @@ export const HDS_THEMING_LOCALSTORAGE_DATA = 'hds-theming-data';
export const DEFAULT_THEMING_OPTION_LIGHT_THEME = HdsModesLightValues.CdsG0;
export const DEFAULT_THEMING_OPTION_DARK_THEME = HdsModesDarkValues.CdsG100;

type StoredThemingData = {
theme: HdsThemes | undefined;
options: HdsThemingOptions;
};

// We use this guard function to check if the data parsed from `localStorage` conforms to the `StoredThemingData` type and so is safe to use.
// This prevents the application from using corrupted, malformed or malicious data, by validating the object structure, theme, and mode values.

function isSafeStoredThemingData(data: unknown): data is StoredThemingData {
if (typeof data !== 'object' || data === null) return false;

const d = data as Record<string, unknown>;

const isSafeThemeData =
// there is no stored `theme` key in the object (eg. the `default` theme was selected)
!('theme' in d) ||
// there is a `theme` value and is one of the valid `HdsThemes`
d['theme'] === undefined ||
THEMES.includes(d['theme'] as HdsThemes);

const options = d['options'] as Record<string, unknown> | undefined;

const isSafeOptionsData =
// there is no stored `options` key in the object (eg. it's the first run of the application)
!('options' in d) ||
// there is an `options` value and has valid entries
(typeof options === 'object' &&
options !== null &&
'lightTheme' in options &&
MODES_LIGHT.includes(options['lightTheme'] as HdsModesLight) &&
'darkTheme' in options &&
MODES_DARK.includes(options['darkTheme'] as HdsModesDark));

return isSafeThemeData && isSafeOptionsData;
}

export default class HdsThemingService extends Service {
@tracked _currentTheme: HdsThemes | undefined = undefined;
@tracked _currentMode: HdsModes | undefined = undefined;
Expand All @@ -77,6 +113,7 @@ export default class HdsThemingService extends Service {
const rawStoredThemingData = localStorage.getItem(
HDS_THEMING_LOCALSTORAGE_DATA
);

if (rawStoredThemingData !== null) {
let storedThemingData: unknown;
try {
Expand All @@ -87,16 +124,18 @@ export default class HdsThemingService extends Service {
`Error while reading local storage '${HDS_THEMING_LOCALSTORAGE_DATA}' for theming`,
error
);
storedThemingData = undefined;
}
if (storedThemingData) {
const { theme, options } = storedThemingData as {
theme: HdsThemes | undefined;
options: HdsThemingOptions;
};

if (isSafeStoredThemingData(storedThemingData)) {
this.setTheme({
theme: storedThemingData.theme,
options: storedThemingData.options,
});
} else {
// if data is not safe or malformed, reset theming to its defaults
this.setTheme({
theme,
options,
theme: undefined,
options: undefined,
});
}
}
Expand Down
22 changes: 16 additions & 6 deletions showcase/app/services/shw-theming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import HdsThemingService from '@hashicorp/design-system-components/services/hds-
// HdsOnSetThemeCallbackArgs,
// } from '@hashicorp/design-system-components/services/hds-theming';

export type ShwStylesheets =
| 'standard'
| 'css-selectors'
| 'css-selectors--migration'
| 'css-selectors--advanced';
const STYLESHEETS = [
'standard',
'css-selectors',
'css-selectors--migration',
'css-selectors--advanced',
] as const;

export type ShwStylesheets = (typeof STYLESHEETS)[number];

const ALL_STYLESHEETS_IDS: string[] = [
'hds-components-stylesheet-default',
Expand Down Expand Up @@ -41,6 +44,10 @@ const STYLESHEETS_MAPPING: Record<ShwStylesheets, string[]> = {

const LOCALSTORAGE_CURRENT_STYLESHEET = 'shw-theming-current-stylesheet';

function isSafeStylesheetData(data: string): data is ShwStylesheets {
return STYLESHEETS.includes(data as ShwStylesheets);
}

export default class ShwThemingService extends HdsThemingService {
@service declare readonly hdsTheming: HdsThemingService;

Expand All @@ -52,8 +59,11 @@ export default class ShwThemingService extends HdsThemingService {
const storedStylesheet = localStorage.getItem(
LOCALSTORAGE_CURRENT_STYLESHEET,
) as ShwStylesheets;
if (storedStylesheet) {
if (storedStylesheet && isSafeStylesheetData(storedStylesheet)) {
this.setStylesheet(storedStylesheet);
} else {
// if data is not safe or malformed, reset stylesheet to its default
this.setStylesheet('standard');
}
}

Expand Down