fix(v2): fail-safe usage of browser storage (localStorage/sessionStorage) when access is denied (#4501)

* fix: Fix unsafe uses of localStorage

Puts all uses of localStorage behind an abstraction which doesn't fail
when localStorage isn't available.

* cleanup fail-safe browser storage usage

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
John Knox 2021-04-13 11:38:12 +01:00 committed by GitHub
parent cbb31783d7
commit 2c57f44bd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 52 deletions

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {createStorageSlot} from '../storageUtils';
import {DocsVersionPersistence} from '../useThemeConfig';
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
@ -15,30 +16,18 @@ const DocsPreferredVersionStorage = {
persistence: DocsVersionPersistence,
versionName: string,
): void => {
if (persistence === 'none') {
// noop
} else {
window.localStorage.setItem(storageKey(pluginId), versionName);
}
createStorageSlot(storageKey(pluginId), {persistence}).set(versionName);
},
read: (
pluginId: string,
persistence: DocsVersionPersistence,
): string | null => {
if (persistence === 'none') {
return null;
} else {
return window.localStorage.getItem(storageKey(pluginId));
}
return createStorageSlot(storageKey(pluginId), {persistence}).get();
},
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
if (persistence === 'none') {
// noop
} else {
window.localStorage.removeItem(storageKey(pluginId));
}
createStorageSlot(storageKey(pluginId), {persistence}).del();
},
};

View file

@ -0,0 +1,121 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const StorageTypes = ['localStorage', 'sessionStorage', 'none'] as const;
export type StorageType = typeof StorageTypes[number];
const DefaultStorageType: StorageType = 'localStorage';
// Will return null browser storage is unavailable (like running Docusaurus in iframe)
// See https://github.com/facebook/docusaurus/pull/4501
function getBrowserStorage(
storageType: StorageType = DefaultStorageType,
): Storage | null {
if (typeof window === 'undefined') {
throw new Error(
'Browser storage is not available on NodeJS / Docusaurus SSR process',
);
}
if (storageType === 'none') {
return null;
} else {
try {
return window[storageType];
} catch (e) {
logOnceBrowserStorageNotAvailableWarning(e);
return null;
}
}
}
/**
* Poor man's memoization to avoid logging multiple times the same warning
* Sometimes, localStorage/sessionStorage is unavailable due to browser policies
*/
let hasLoggedBrowserStorageNotAvailableWarning = false;
function logOnceBrowserStorageNotAvailableWarning(error: Error) {
if (!hasLoggedBrowserStorageNotAvailableWarning) {
console.warn(
`Docusaurus browser storage is not available.
Possible reasons: running Docusaurus in an Iframe, in an Incognito browser session, or using too strict browser privacy settings.`,
error,
);
hasLoggedBrowserStorageNotAvailableWarning = true;
}
}
// Convenient storage interface for a single storage key
export interface StorageSlot {
get: () => string | null;
set: (value: string) => void;
del: () => void;
}
const NoopStorageSlot: StorageSlot = {
get: () => null,
set: () => {},
del: () => {},
};
// Fail-fast, as storage APIs should not be used during the SSR process
function createServerStorageSlot(key: string): StorageSlot {
function throwError(): never {
throw new Error(`Illegal storage API usage for storage key=${key}.
Docusaurus storage APIs are not supposed to be called on the server-rendering process.
Please only call storage APIs in effects and event handlers.`);
}
return {
get: throwError,
set: throwError,
del: throwError,
};
}
/**
* Creates an object for accessing a particular key in localStorage.
*/
export const createStorageSlot = (
key: string,
options?: {persistence?: StorageType},
): StorageSlot => {
if (typeof window === 'undefined') {
return createServerStorageSlot(key);
}
const browserStorage = getBrowserStorage(options?.persistence);
if (browserStorage === null) {
return NoopStorageSlot;
}
return {
get: () => browserStorage.getItem(key),
set: (value) => browserStorage.setItem(key, value),
del: () => browserStorage.removeItem(key),
};
};
/**
* Returns a list of all the keys currently stored in browser storage
* or an empty list if browser storage can't be accessed.
*/
export function listStorageKeys(
storageType: StorageType = DefaultStorageType,
): string[] {
const browserStorage = getBrowserStorage(storageType);
if (!browserStorage) {
return [];
}
const keys: string[] = [];
for (let i = 0; i < browserStorage.length; i += 1) {
const key = browserStorage.key(i);
if (key !== null) {
keys.push(key);
}
}
return keys;
}