From cc14c212b6cf751ced4c4c4c3b7a7c5975296ee5 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 26 Jan 2023 04:48:56 -0800 Subject: [PATCH] fix: Unable to access localStorage in document embedded in iframe with third party cookies blocked (#4777) * fix: Pasting from Microsoft Office pastes image. Closes #3058 * fix: Use Storage wrapper implementation for all editor calls to localStorage closes #4776 --- app/hooks/usePersistedState.ts | 2 +- app/models/Document.ts | 2 +- app/stores/AuthStore.ts | 2 +- app/stores/UiStore.ts | 2 +- app/utils/Storage.ts | 56 ------------------ shared/editor/nodes/CodeFence.ts | 9 +-- shared/editor/nodes/Heading.ts | 5 +- shared/editor/plugins/Folding.tsx | 3 +- shared/utils/Storage.ts | 95 +++++++++++++++++++++++++++++++ 9 files changed, 109 insertions(+), 67 deletions(-) delete mode 100644 app/utils/Storage.ts create mode 100644 shared/utils/Storage.ts diff --git a/app/hooks/usePersistedState.ts b/app/hooks/usePersistedState.ts index c954aec7e..eb0837ec5 100644 --- a/app/hooks/usePersistedState.ts +++ b/app/hooks/usePersistedState.ts @@ -1,7 +1,7 @@ import * as React from "react"; import { Primitive } from "utility-types"; +import Storage from "@shared/utils/Storage"; import Logger from "~/utils/Logger"; -import Storage from "~/utils/Storage"; import useEventListener from "./useEventListener"; type Options = { diff --git a/app/models/Document.ts b/app/models/Document.ts index bea547a95..47e9c257e 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -2,13 +2,13 @@ import { addDays, differenceInDays } from "date-fns"; import { floor } from "lodash"; import { action, autorun, computed, observable, set } from "mobx"; import { ExportContentType } from "@shared/types"; +import Storage from "@shared/utils/Storage"; import parseTitle from "@shared/utils/parseTitle"; import { isRTL } from "@shared/utils/rtl"; import DocumentsStore from "~/stores/DocumentsStore"; import User from "~/models/User"; import type { NavigationNode } from "~/types"; import { client } from "~/utils/ApiClient"; -import Storage from "~/utils/Storage"; import ParanoidModel from "./ParanoidModel"; import View from "./View"; import Field from "./decorators/Field"; diff --git a/app/stores/AuthStore.ts b/app/stores/AuthStore.ts index 9b196323c..5c55faeb4 100644 --- a/app/stores/AuthStore.ts +++ b/app/stores/AuthStore.ts @@ -3,6 +3,7 @@ import invariant from "invariant"; import { observable, action, computed, autorun, runInAction } from "mobx"; import { getCookie, setCookie, removeCookie } from "tiny-cookie"; import { TeamPreferences, UserPreferences } from "@shared/types"; +import Storage from "@shared/utils/Storage"; import { getCookieDomain, parseDomain } from "@shared/utils/domains"; import RootStore from "~/stores/RootStore"; import Policy from "~/models/Policy"; @@ -11,7 +12,6 @@ import User from "~/models/User"; import env from "~/env"; import { client } from "~/utils/ApiClient"; import Desktop from "~/utils/Desktop"; -import Storage from "~/utils/Storage"; const AUTH_STORE = "AUTH_STORE"; const NO_REDIRECT_PATHS = ["/", "/create", "/home"]; diff --git a/app/stores/UiStore.ts b/app/stores/UiStore.ts index e2afb6745..a4da950b4 100644 --- a/app/stores/UiStore.ts +++ b/app/stores/UiStore.ts @@ -1,8 +1,8 @@ import { action, autorun, computed, observable } from "mobx"; import { light as defaultTheme } from "@shared/styles/theme"; +import Storage from "@shared/utils/Storage"; import Document from "~/models/Document"; import type { ConnectionStatus } from "~/scenes/Document/components/MultiplayerEditor"; -import Storage from "~/utils/Storage"; const UI_STORE = "UI_STORE"; diff --git a/app/utils/Storage.ts b/app/utils/Storage.ts deleted file mode 100644 index a6e86719f..000000000 --- a/app/utils/Storage.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Storage is a wrapper class for localStorage that allow safe usage when - * localStorage is not available. - */ -export default class Storage { - /** - * Set a value in localStorage. For efficiency, this method will remove the - * value if it is undefined. - * - * @param key The key to set under. - * @param value The value to set - */ - static set(key: string, value: T) { - try { - if (value === undefined) { - this.remove(key); - } else { - localStorage.setItem(key, JSON.stringify(value)); - } - } catch (error) { - // no-op Safari private mode - } - } - - /** - * Get a value from localStorage. - * - * @param key The key to get. - * @returns The value or undefined if it doesn't exist. - */ - static get(key: string) { - try { - const value = localStorage.getItem(key); - if (typeof value === "string") { - return JSON.parse(value); - } - } catch (error) { - // no-op Safari private mode - } - - return undefined; - } - - /** - * Remove a value from localStorage. - * - * @param key The key to remove. - */ - static remove(key: string) { - try { - localStorage.removeItem(key); - } catch (error) { - // no-op Safari private mode - } - } -} diff --git a/shared/editor/nodes/CodeFence.ts b/shared/editor/nodes/CodeFence.ts index 1f6fef0a3..80f651cb6 100644 --- a/shared/editor/nodes/CodeFence.ts +++ b/shared/editor/nodes/CodeFence.ts @@ -52,8 +52,9 @@ import visualbasic from "refractor/lang/visual-basic"; import yaml from "refractor/lang/yaml"; import zig from "refractor/lang/zig"; -import { UserPreferences } from "@shared/types"; import { Dictionary } from "~/hooks/useDictionary"; +import { UserPreferences } from "../../types"; +import Storage from "../../utils/Storage"; import toggleBlockType from "../commands/toggleBlockType"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; @@ -220,7 +221,7 @@ export default class CodeFence extends Node { commands({ type, schema }: { type: NodeType; schema: Schema }) { return (attrs: Record) => toggleBlockType(type, schema.nodes.paragraph, { - language: localStorage?.getItem(PERSISTENCE_KEY) || DEFAULT_LANGUAGE, + language: Storage.get(PERSISTENCE_KEY, DEFAULT_LANGUAGE), ...attrs, }); } @@ -302,7 +303,7 @@ export default class CodeFence extends Node { view.dispatch(transaction); - localStorage?.setItem(PERSISTENCE_KEY, language); + Storage.set(PERSISTENCE_KEY, language); } }; @@ -364,7 +365,7 @@ export default class CodeFence extends Node { inputRules({ type }: { type: NodeType }) { return [ textblockTypeInputRule(/^```$/, type, () => ({ - language: localStorage?.getItem(PERSISTENCE_KEY) || DEFAULT_LANGUAGE, + language: Storage.get(PERSISTENCE_KEY, DEFAULT_LANGUAGE), })), ]; } diff --git a/shared/editor/nodes/Heading.ts b/shared/editor/nodes/Heading.ts index e7f92cd30..4b50b9cad 100644 --- a/shared/editor/nodes/Heading.ts +++ b/shared/editor/nodes/Heading.ts @@ -8,6 +8,7 @@ import { } from "prosemirror-model"; import { Plugin, Selection } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; +import Storage from "../../utils/Storage"; import backspaceToParagraph from "../commands/backspaceToParagraph"; import splitHeading from "../commands/splitHeading"; import toggleBlockType from "../commands/toggleBlockType"; @@ -151,9 +152,9 @@ export default class Heading extends Node { const persistKey = headingToPersistenceKey(node, this.editor.props.id); if (collapsed) { - localStorage?.setItem(persistKey, "collapsed"); + Storage.set(persistKey, "collapsed"); } else { - localStorage?.removeItem(persistKey); + Storage.remove(persistKey); } view.dispatch(transaction); diff --git a/shared/editor/plugins/Folding.tsx b/shared/editor/plugins/Folding.tsx index ef287bfc1..99f5c9dec 100644 --- a/shared/editor/plugins/Folding.tsx +++ b/shared/editor/plugins/Folding.tsx @@ -1,6 +1,7 @@ import { Plugin } from "prosemirror-state"; import { findBlockNodes } from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; +import Storage from "../../utils/Storage"; import Extension from "../lib/Extension"; import { headingToPersistenceKey } from "../lib/headingToSlug"; import findCollapsedNodes from "../queries/findCollapsedNodes"; @@ -40,7 +41,7 @@ export default class Folding extends Extension { block.node, this.editor.props.id ); - const persistedState = localStorage?.getItem(persistKey); + const persistedState = Storage.get(persistKey); if (persistedState === "collapsed") { tr.setNodeMarkup(block.pos, undefined, { diff --git a/shared/utils/Storage.ts b/shared/utils/Storage.ts new file mode 100644 index 000000000..af82e49c0 --- /dev/null +++ b/shared/utils/Storage.ts @@ -0,0 +1,95 @@ +/** + * Storage is a wrapper class for localStorage that allow safe usage when + * localStorage is not available. + */ +class Storage { + interface: typeof localStorage | MemoryStorage; + + public constructor() { + try { + localStorage.setItem("test", "test"); + localStorage.removeItem("test"); + this.interface = localStorage; + } catch (_err) { + this.interface = new MemoryStorage(); + } + } + + /** + * Set a value in storage. For efficiency, this method will remove the + * value if it is undefined. + * + * @param key The key to set under. + * @param value The value to set + */ + public set(key: string, value: T) { + try { + if (value === undefined) { + this.remove(key); + } else { + this.interface.setItem(key, JSON.stringify(value)); + } + } catch (_err) { + // Ignore errors + } + } + + /** + * Get a value from storage. + * + * @param key The key to get. + * @param fallback The fallback value if the key doesn't exist. + * @returns The value or undefined if it doesn't exist. + */ + public get(key: string, fallback?: any) { + try { + const value = this.interface.getItem(key); + if (typeof value === "string") { + return JSON.parse(value); + } + } catch (_err) { + // Ignore errors + } + + return fallback; + } + + /** + * Remove a value from storage. + * + * @param key The key to remove. + */ + public remove(key: string) { + try { + this.interface.removeItem(key); + } catch (_err) { + // Ignore errors + } + } +} + +/** + * MemoryStorage is a simple in-memory storage implementation that is used + * when localStorage is not available. + */ +class MemoryStorage { + private data = {}; + + getItem(key: string) { + return this.data[key] || null; + } + + setItem(key: string, value: any) { + return (this.data[key] = String(value)); + } + + removeItem(key: string) { + return delete this.data[key]; + } + + clear() { + return (this.data = {}); + } +} + +export default new Storage();