mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-31 18:07:00 +02:00
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:
parent
cbb31783d7
commit
2c57f44bd6
8 changed files with 160 additions and 52 deletions
|
@ -17,6 +17,7 @@
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.12.16",
|
"@babel/plugin-proposal-optional-chaining": "^7.12.16",
|
||||||
"@babel/preset-env": "^7.12.16",
|
"@babel/preset-env": "^7.12.16",
|
||||||
"@docusaurus/core": "2.0.0-alpha.72",
|
"@docusaurus/core": "2.0.0-alpha.72",
|
||||||
|
"@docusaurus/theme-common": "2.0.0-alpha.72",
|
||||||
"@docusaurus/utils-validation": "2.0.0-alpha.72",
|
"@docusaurus/utils-validation": "2.0.0-alpha.72",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
import {createStorageSlot} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
// First: read the env variables (provided by Webpack)
|
// First: read the env variables (provided by Webpack)
|
||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
@ -17,7 +18,10 @@ const PWA_DEBUG = process.env.PWA_DEBUG;
|
||||||
const debug = PWA_DEBUG; // shortcut
|
const debug = PWA_DEBUG; // shortcut
|
||||||
|
|
||||||
const MAX_MOBILE_WIDTH = 940;
|
const MAX_MOBILE_WIDTH = 940;
|
||||||
const APP_INSTALLED_EVENT_FIRED_KEY = 'docusaurus.pwa.event.appInstalled.fired';
|
|
||||||
|
const AppInstalledEventFiredStorage = createStorageSlot(
|
||||||
|
'docusaurus.pwa.event.appInstalled.fired',
|
||||||
|
);
|
||||||
|
|
||||||
async function clearRegistrations() {
|
async function clearRegistrations() {
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
@ -58,7 +62,7 @@ https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed
|
||||||
- display-mode: standalone is not exactly the same concept, but looks like a decent fallback https://petelepage.com/blog/2019/07/is-my-pwa-installed/
|
- display-mode: standalone is not exactly the same concept, but looks like a decent fallback https://petelepage.com/blog/2019/07/is-my-pwa-installed/
|
||||||
*/
|
*/
|
||||||
async function isAppInstalledEventFired() {
|
async function isAppInstalledEventFired() {
|
||||||
return localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY) === 'true';
|
return AppInstalledEventFiredStorage.get() === 'true';
|
||||||
}
|
}
|
||||||
async function isAppInstalledRelatedApps() {
|
async function isAppInstalledRelatedApps() {
|
||||||
if ('getInstalledRelatedApps' in window.navigator) {
|
if ('getInstalledRelatedApps' in window.navigator) {
|
||||||
|
@ -229,10 +233,10 @@ function addLegacyAppInstalledEventsListeners() {
|
||||||
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
|
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(APP_INSTALLED_EVENT_FIRED_KEY, 'true');
|
AppInstalledEventFiredStorage.set('true');
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(
|
console.log(
|
||||||
"[Docusaurus-PWA][registerSw]: localStorage.setItem(APP_INSTALLED_EVENT_FIRED_KEY, 'true');",
|
"[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.set('true')",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,15 +259,15 @@ function addLegacyAppInstalledEventsListeners() {
|
||||||
// event.preventDefault();
|
// event.preventDefault();
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(
|
console.log(
|
||||||
'[Docusaurus-PWA][registerSw]: localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY)',
|
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.get()',
|
||||||
localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY),
|
AppInstalledEventFiredStorage.get(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY)) {
|
if (AppInstalledEventFiredStorage.get()) {
|
||||||
localStorage.removeItem(APP_INSTALLED_EVENT_FIRED_KEY);
|
AppInstalledEventFiredStorage.del();
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(
|
console.log(
|
||||||
'[Docusaurus-PWA][registerSw]: localStorage.removeItem(APP_INSTALLED_EVENT_FIRED_KEY)',
|
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.del()',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// After uninstalling the app, if the user doesn't clear all data, then
|
// After uninstalling the app, if the user doesn't clear all data, then
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useState, useEffect, useCallback} from 'react';
|
import {useState, useEffect, useCallback} from 'react';
|
||||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
import {useThemeConfig, createStorageSlot} from '@docusaurus/theme-common';
|
||||||
import type {useAnnouncementBarReturns} from '@theme/hooks/useAnnouncementBar';
|
import type {useAnnouncementBarReturns} from '@theme/hooks/useAnnouncementBar';
|
||||||
|
|
||||||
const STORAGE_DISMISS_KEY = 'docusaurus.announcement.dismiss';
|
const DismissStorage = createStorageSlot('docusaurus.announcement.dismiss');
|
||||||
const STORAGE_ID_KEY = 'docusaurus.announcement.id';
|
const IdStorage = createStorageSlot('docusaurus.announcement.id');
|
||||||
|
|
||||||
const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
||||||
const {announcementBar} = useThemeConfig();
|
const {announcementBar} = useThemeConfig();
|
||||||
|
@ -18,7 +18,7 @@ const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
||||||
const [isClosed, setClosed] = useState(true);
|
const [isClosed, setClosed] = useState(true);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
localStorage.setItem(STORAGE_DISMISS_KEY, 'true');
|
DismissStorage.set('true');
|
||||||
setClosed(true);
|
setClosed(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
||||||
}
|
}
|
||||||
const {id} = announcementBar;
|
const {id} = announcementBar;
|
||||||
|
|
||||||
let viewedId = localStorage.getItem(STORAGE_ID_KEY);
|
let viewedId = IdStorage.get();
|
||||||
|
|
||||||
// retrocompatibility due to spelling mistake of default id
|
// retrocompatibility due to spelling mistake of default id
|
||||||
// see https://github.com/facebook/docusaurus/issues/3338
|
// see https://github.com/facebook/docusaurus/issues/3338
|
||||||
|
@ -38,16 +38,13 @@ const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
||||||
|
|
||||||
const isNewAnnouncement = id !== viewedId;
|
const isNewAnnouncement = id !== viewedId;
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_ID_KEY, id);
|
IdStorage.set(id);
|
||||||
|
|
||||||
if (isNewAnnouncement) {
|
if (isNewAnnouncement) {
|
||||||
localStorage.setItem(STORAGE_DISMISS_KEY, 'false');
|
DismissStorage.set('false');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isNewAnnouncement || DismissStorage.get() === 'false') {
|
||||||
isNewAnnouncement ||
|
|
||||||
localStorage.getItem(STORAGE_DISMISS_KEY) === 'false'
|
|
||||||
) {
|
|
||||||
setClosed(false);
|
setClosed(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import {useState, useCallback, useEffect} from 'react';
|
import {useState, useCallback, useEffect} from 'react';
|
||||||
import type {useTabGroupChoiceReturns} from '@theme/hooks/useTabGroupChoice';
|
import type {useTabGroupChoiceReturns} from '@theme/hooks/useTabGroupChoice';
|
||||||
|
import {createStorageSlot, listStorageKeys} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
|
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
|
||||||
|
|
||||||
|
@ -15,21 +16,16 @@ const useTabGroupChoice = (): useTabGroupChoiceReturns => {
|
||||||
readonly [groupId: string]: string;
|
readonly [groupId: string]: string;
|
||||||
}>({});
|
}>({});
|
||||||
const setChoiceSyncWithLocalStorage = useCallback((groupId, newChoice) => {
|
const setChoiceSyncWithLocalStorage = useCallback((groupId, newChoice) => {
|
||||||
try {
|
createStorageSlot(`${TAB_CHOICE_PREFIX}${groupId}`).set(newChoice);
|
||||||
localStorage.setItem(`${TAB_CHOICE_PREFIX}${groupId}`, newChoice);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const localStorageChoices = {};
|
const localStorageChoices = {};
|
||||||
for (let i = 0; i < localStorage.length; i += 1) {
|
for (const storageKey of listStorageKeys()) {
|
||||||
const storageKey = localStorage.key(i) as string;
|
|
||||||
if (storageKey.startsWith(TAB_CHOICE_PREFIX)) {
|
if (storageKey.startsWith(TAB_CHOICE_PREFIX)) {
|
||||||
const groupId = storageKey.substring(TAB_CHOICE_PREFIX.length);
|
const groupId = storageKey.substring(TAB_CHOICE_PREFIX.length);
|
||||||
localStorageChoices[groupId] = localStorage.getItem(storageKey);
|
localStorageChoices[groupId] = createStorageSlot(storageKey).get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setChoices(localStorageChoices);
|
setChoices(localStorageChoices);
|
||||||
|
|
|
@ -9,7 +9,9 @@ import {useState, useCallback, useEffect} from 'react';
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
import type {useThemeReturns} from '@theme/hooks/useTheme';
|
import type {useThemeReturns} from '@theme/hooks/useTheme';
|
||||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
import {useThemeConfig, createStorageSlot} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
|
const ThemeStorage = createStorageSlot('theme');
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
light: 'light',
|
light: 'light',
|
||||||
|
@ -31,11 +33,7 @@ const getInitialTheme = (defaultMode: Themes | undefined): Themes => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeTheme = (newTheme: Themes) => {
|
const storeTheme = (newTheme: Themes) => {
|
||||||
try {
|
createStorageSlot('theme').set(coerceToTheme(newTheme));
|
||||||
localStorage.setItem('theme', coerceToTheme(newTheme));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useTheme = (): useThemeReturns => {
|
const useTheme = (): useThemeReturns => {
|
||||||
|
@ -63,9 +61,9 @@ const useTheme = (): useThemeReturns => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const localStorageTheme = localStorage.getItem('theme');
|
const storedTheme = ThemeStorage.get();
|
||||||
if (localStorageTheme !== null) {
|
if (storedTheme !== null) {
|
||||||
setTheme(coerceToTheme(localStorageTheme));
|
setTheme(coerceToTheme(storedTheme));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
@ -16,6 +16,8 @@ export {
|
||||||
FooterLinkItem,
|
FooterLinkItem,
|
||||||
} from './utils/useThemeConfig';
|
} from './utils/useThemeConfig';
|
||||||
|
|
||||||
|
export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
|
||||||
|
|
||||||
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
||||||
|
|
||||||
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {createStorageSlot} from '../storageUtils';
|
||||||
import {DocsVersionPersistence} from '../useThemeConfig';
|
import {DocsVersionPersistence} from '../useThemeConfig';
|
||||||
|
|
||||||
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
|
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
|
||||||
|
@ -15,30 +16,18 @@ const DocsPreferredVersionStorage = {
|
||||||
persistence: DocsVersionPersistence,
|
persistence: DocsVersionPersistence,
|
||||||
versionName: string,
|
versionName: string,
|
||||||
): void => {
|
): void => {
|
||||||
if (persistence === 'none') {
|
createStorageSlot(storageKey(pluginId), {persistence}).set(versionName);
|
||||||
// noop
|
|
||||||
} else {
|
|
||||||
window.localStorage.setItem(storageKey(pluginId), versionName);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
read: (
|
read: (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
persistence: DocsVersionPersistence,
|
persistence: DocsVersionPersistence,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
if (persistence === 'none') {
|
return createStorageSlot(storageKey(pluginId), {persistence}).get();
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return window.localStorage.getItem(storageKey(pluginId));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
|
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
|
||||||
if (persistence === 'none') {
|
createStorageSlot(storageKey(pluginId), {persistence}).del();
|
||||||
// noop
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem(storageKey(pluginId));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
121
packages/docusaurus-theme-common/src/utils/storageUtils.ts
Normal file
121
packages/docusaurus-theme-common/src/utils/storageUtils.ts
Normal 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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue