mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Fix problem with localStorage in some cross-origin embed situations
Summary: - Handle the possibility that any access to localStorage causes error. - Move getStorage() and getSessionStorage() safe functions to a separate file. - Use these safe functions in more places. Test Plan: Added a test case, using a webdriver instance that blocks third-party cookies, to enforce third-party restrictions. Added to gristUtil a way to override the webdriver instance. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3719
This commit is contained in:
@@ -1,68 +1,6 @@
|
||||
import {safeJsonParse} from 'app/common/gutil';
|
||||
import {Observable} from 'grainjs';
|
||||
|
||||
/**
|
||||
* Returns true if storage is functional. In some cass (e.g. when embedded), localStorage may
|
||||
* throw errors. If so, we return false. This implementation is the approach taken by store.js.
|
||||
*/
|
||||
function testStorage(storage: Storage) {
|
||||
try {
|
||||
const testStr = '__localStorage_test';
|
||||
storage.setItem(testStr, testStr);
|
||||
const ok = (storage.getItem(testStr) === testStr);
|
||||
storage.removeItem(testStr);
|
||||
return ok;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns localStorage if functional, or sessionStorage, or an in-memory storage. The fallbacks
|
||||
* help with tests, and may help when Grist is embedded.
|
||||
*/
|
||||
export function getStorage(): Storage {
|
||||
return _storage || (_storage = createStorage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `getStorage`, but always returns sessionStorage (or an in-memory equivalent).
|
||||
*/
|
||||
export function getSessionStorage(): Storage {
|
||||
return _sessionStorage || (_sessionStorage = createSessionStorage());
|
||||
}
|
||||
|
||||
let _storage: Storage|undefined;
|
||||
let _sessionStorage: Storage|undefined;
|
||||
|
||||
function createStorage(): Storage {
|
||||
if (typeof localStorage !== 'undefined' && testStorage(localStorage)) {
|
||||
return localStorage;
|
||||
} else {
|
||||
return createSessionStorage();
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionStorage(): Storage {
|
||||
if (typeof sessionStorage !== 'undefined' && testStorage(sessionStorage)) {
|
||||
return sessionStorage;
|
||||
} else {
|
||||
// Fall back to a Map-based implementation of (non-persistent) sessionStorage.
|
||||
return createInMemoryStorage();
|
||||
}
|
||||
}
|
||||
|
||||
function createInMemoryStorage(): Storage {
|
||||
const values = new Map<string, string>();
|
||||
return {
|
||||
setItem(key: string, val: string) { values.set(key, val); },
|
||||
getItem(key: string) { return values.get(key) ?? null; },
|
||||
removeItem(key: string) { values.delete(key); },
|
||||
clear() { values.clear(); },
|
||||
get length() { return values.size; },
|
||||
key(index: number): string|null { throw new Error('Not implemented'); },
|
||||
};
|
||||
}
|
||||
import {getSessionStorage, getStorage} from 'app/client/lib/storage';
|
||||
|
||||
function getStorageBoolObs(store: Storage, key: string, defValue: boolean) {
|
||||
const storedNegation = defValue ? 'false' : 'true';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import {safeJsonParse} from 'app/common/gutil';
|
||||
import {IDisposableOwner, Observable} from 'grainjs';
|
||||
import {getSessionStorage} from 'app/client/lib/storage';
|
||||
|
||||
export interface SessionObs<T> extends Observable<T> {
|
||||
pauseSaving(yesNo: boolean): void;
|
||||
@@ -45,14 +46,15 @@ export function createSessionObs<T>(
|
||||
return value === _default || !isValid(value) ? null : JSON.stringify(value);
|
||||
}
|
||||
let _pauseSaving = false;
|
||||
const obs = Observable.create<T>(owner, fromString(window.sessionStorage.getItem(key)));
|
||||
const storage = getSessionStorage();
|
||||
const obs = Observable.create<T>(owner, fromString(storage.getItem(key)));
|
||||
obs.addListener((value: T) => {
|
||||
if (_pauseSaving) { return; }
|
||||
const stored = toString(value);
|
||||
if (stored == null) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
storage.removeItem(key);
|
||||
} else {
|
||||
window.sessionStorage.setItem(key, stored);
|
||||
storage.setItem(key, stored);
|
||||
}
|
||||
});
|
||||
return Object.assign(obs, {pauseSaving(yesNo: boolean) { _pauseSaving = yesNo; }});
|
||||
|
||||
65
app/client/lib/storage.ts
Normal file
65
app/client/lib/storage.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Expose localStorage and sessionStorage with fallbacks for cases when they don't work (e.g.
|
||||
* cross-domain embeds in Firefox and Safari).
|
||||
*
|
||||
* Usage:
|
||||
* import {getStorage, getSessionStorage} from 'app/client/lib/storage';
|
||||
* ... use getStorage() in place of localStorage...
|
||||
* ... use getSessionStorage() in place of sessionStorage...
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns localStorage if functional, or sessionStorage, or an in-memory storage. The fallbacks
|
||||
* help with tests, and when Grist is embedded.
|
||||
*/
|
||||
export function getStorage(): Storage {
|
||||
_storage ??= testStorage('localStorage') || getSessionStorage();
|
||||
return _storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return window.sessionStorage, or when not available, an in-memory storage.
|
||||
*/
|
||||
export function getSessionStorage(): Storage {
|
||||
// If can't use sessionStorage, fall back to a Map-based non-persistent implementation.
|
||||
_sessionStorage ??= testStorage('sessionStorage') || createInMemoryStorage();
|
||||
return _sessionStorage;
|
||||
}
|
||||
|
||||
|
||||
let _storage: Storage|undefined;
|
||||
let _sessionStorage: Storage|undefined;
|
||||
|
||||
/**
|
||||
* Returns the result of window[storageName] if storage is functional, or null otherwise. In some
|
||||
* cases (e.g. when embedded), using localStorage may throw errors, in which case we return null.
|
||||
* This is similar to the approach taken by store.js.
|
||||
*/
|
||||
function testStorage(storageName: 'localStorage'|'sessionStorage'): Storage|null {
|
||||
try {
|
||||
const testStr = '__localStorage_test';
|
||||
const storage = window[storageName];
|
||||
storage.setItem(testStr, testStr);
|
||||
const ok = (storage.getItem(testStr) === testStr);
|
||||
storage.removeItem(testStr);
|
||||
if (ok) {
|
||||
return storage;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through
|
||||
}
|
||||
console.warn(`${storageName} is not available; will use fallback`);
|
||||
return null;
|
||||
}
|
||||
|
||||
function createInMemoryStorage(): Storage {
|
||||
const values = new Map<string, string>();
|
||||
return {
|
||||
setItem(key: string, val: string) { values.set(key, val); },
|
||||
getItem(key: string) { return values.get(key) ?? null; },
|
||||
removeItem(key: string) { values.delete(key); },
|
||||
clear() { values.clear(); },
|
||||
get length() { return values.size; },
|
||||
key(index: number): string|null { throw new Error('Not implemented'); },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user