fix: detect private browsing and safari blocked cookies (#733)

* WIP: detect private browsing and safari blocked cookies

* just check for indexeddb

* just check for indexeddb

* change warning text

* change text

* change text again

* change text again

fixes #444
This commit is contained in:
Nolan Lawson 2018-12-05 21:34:30 -08:00 committed by GitHub
parent c0f857336a
commit 0e524f3e9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 111 additions and 12 deletions

View file

@ -3,7 +3,17 @@
// the build process and write it to inline-script-checksum.json. // the build process and write it to inline-script-checksum.json.
window.__themeColors = process.env.THEME_COLORS window.__themeColors = process.env.THEME_COLORS
if (localStorage.store_currentInstance && localStorage.store_instanceThemes) { const hasLocalStorage = (() => {
try {
// iOS safari throws here if cookies are disabled
let unused = localStorage.length // eslint-disable-line
return true
} catch (e) {
return false
}
})()
if (hasLocalStorage && localStorage.store_currentInstance && localStorage.store_instanceThemes) {
let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str) let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str)
let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)] let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]
if (theme && theme !== 'default') { if (theme && theme !== 'default') {
@ -18,14 +28,14 @@ if (localStorage.store_currentInstance && localStorage.store_instanceThemes) {
} }
} }
if (!localStorage.store_currentInstance) { if (!hasLocalStorage || !localStorage.store_currentInstance) {
// if not logged in, show all these 'hidden-from-ssr' elements // if not logged in, show all these 'hidden-from-ssr' elements
let style = document.createElement('style') let style = document.createElement('style')
style.textContent = '.hidden-from-ssr { opacity: 1 !important; }' style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
document.head.appendChild(style) document.head.appendChild(style)
} }
if (localStorage.store_disableCustomScrollbars === 'true') { if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
// if user has disabled custom scrollbars, remove this style // if user has disabled custom scrollbars, remove this style
let theScrollbarStyle = document.getElementById('theScrollbarStyle') let theScrollbarStyle = document.getElementById('theScrollbarStyle')
theScrollbarStyle.setAttribute('media', 'only x') // disables the style theScrollbarStyle.setAttribute('media', 'only x') // disables the style

5
package-lock.json generated
View file

@ -6003,6 +6003,11 @@
"json5": "^0.5.0" "json5": "^0.5.0"
} }
}, },
"localstorage-memory": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/localstorage-memory/-/localstorage-memory-1.0.3.tgz",
"integrity": "sha512-t9P8WB6DcVttbw/W4PIE8HOqum8Qlvx5SjR6oInwR9Uia0EEmyUeBh7S+weKByW+l/f45Bj4L/dgZikGFDM6ng=="
},
"locate-path": { "locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",

View file

@ -72,6 +72,7 @@
"idb-keyval": "^3.1.0", "idb-keyval": "^3.1.0",
"indexeddb-getall-shim": "^1.3.5", "indexeddb-getall-shim": "^1.3.5",
"intersection-observer": "^0.5.1", "intersection-observer": "^0.5.1",
"localstorage-memory": "^1.0.3",
"lodash-es": "^4.17.11", "lodash-es": "^4.17.11",
"lodash-webpack-plugin": "^0.11.5", "lodash-webpack-plugin": "^0.11.5",
"mini-css-extract-plugin": "^0.4.5", "mini-css-extract-plugin": "^0.4.5",

View file

@ -3,6 +3,14 @@
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1"> <form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
{#if !hasIndexedDB}
<div class="form-error form-error-user-error" role="alert">
Your browser doesn't support IndexedDB, possibly because it's in private browsing mode
or blocking cookies. Pinafore stores all data locally, and requires IndexedDB to work
correctly.
</div>
{/if}
{#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch} {#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}
<div class="form-error form-error-user-error" role="alert"> <div class="form-error form-error-user-error" role="alert">
Error: {$logInToInstanceError} Error: {$logInToInstanceError}
@ -69,19 +77,25 @@
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { logInToInstance, handleOauthCode } from '../../../_actions/addInstance' import { logInToInstance, handleOauthCode } from '../../../_actions/addInstance'
import ExternalLink from '../../../_components/ExternalLink.html' import ExternalLink from '../../../_components/ExternalLink.html'
import { testHasIndexedDB } from '../../../_utils/testStorage'
export default { export default {
async oncreate () { async oncreate () {
let codeMatch = location.search.match(/code=([^&]+)/) let codeMatch = location.search.match(/code=([^&]+)/)
if (codeMatch) { if (codeMatch) {
handleOauthCode(codeMatch[1]) return handleOauthCode(codeMatch[1])
} }
let hasIndexedDB = await testHasIndexedDB()
this.set({ hasIndexedDB })
}, },
components: { components: {
SettingsLayout, SettingsLayout,
ExternalLink ExternalLink
}, },
store: () => store, store: () => store,
data: () => ({
hasIndexedDB: true
}),
methods: { methods: {
onSubmit (event) { onSubmit (event) {
event.preventDefault() event.preventDefault()

View file

@ -1,11 +1,11 @@
import { Store } from 'svelte/store' import { Store } from 'svelte/store'
import { safeLocalStorage as LS } from '../_utils/safeLocalStorage'
let lifecycle let lifecycle
if (process.browser) { if (process.browser) {
lifecycle = require('page-lifecycle/dist/lifecycle.mjs').default lifecycle = require('page-lifecycle/dist/lifecycle.mjs').default
} }
const LS = process.browser && localStorage
function safeParse (str) { function safeParse (str) {
return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str)) return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
} }

View file

@ -0,0 +1,6 @@
import localStorageMemory from 'localstorage-memory'
import { testHasLocalStorage } from './testStorage'
const safeLocalStorage = testHasLocalStorage() ? localStorage : localStorageMemory
export { safeLocalStorage }

View file

@ -0,0 +1,42 @@
// LocalStorage and IDB may be disabled in private mode, when "blocking cookies" in Safari,
// or other cases
import { thunk } from './thunk'
const testKey = '__test__'
export const testHasLocalStorage = thunk(() => {
try {
localStorage.setItem(testKey, testKey)
if (!localStorage.length || localStorage.getItem(testKey) !== testKey) {
return false
}
localStorage.removeItem(testKey)
} catch (e) {
return false
}
return true
})
export const testHasIndexedDB = thunk(async () => {
if (typeof indexedDB === 'undefined') {
return false
}
try {
let idbFailed = await new Promise(resolve => {
let db = indexedDB.open(testKey)
db.onerror = () => resolve(true)
db.onsuccess = () => {
indexedDB.deleteDatabase(testKey)
resolve(false)
}
})
if (idbFailed) {
return false
}
} catch (e) {
return false
}
return true
})

11
routes/_utils/thunk.js Normal file
View file

@ -0,0 +1,11 @@
export function thunk (func) {
let cached
let runOnce
return () => {
if (!runOnce) {
cached = func()
runOnce = true
}
return cached
}
}

View file

@ -52,7 +52,17 @@ html{scrollbar-face-color:var(--scrollbar-face-color);scrollbar-track-color:var(
// the build process and write it to inline-script-checksum.json. // the build process and write it to inline-script-checksum.json.
window.__themeColors = {"default":"royalblue","scarlet":"#e04e41","seafoam":"#177380","hotpants":"hotpink","oaken":"saddlebrown","majesty":"blueviolet","gecko":"#4ab92f","ozark":"#5263af","cobalt":"#08439b","sorcery":"#ae91e8","punk":"#e04e41","riot":"hotpink","hacker":"#4ab92f","pitchblack":"#000"} window.__themeColors = {"default":"royalblue","scarlet":"#e04e41","seafoam":"#177380","hotpants":"hotpink","oaken":"saddlebrown","majesty":"blueviolet","gecko":"#4ab92f","ozark":"#5263af","cobalt":"#08439b","sorcery":"#ae91e8","punk":"#e04e41","riot":"hotpink","hacker":"#4ab92f","pitchblack":"#000"}
if (localStorage.store_currentInstance && localStorage.store_instanceThemes) { const hasLocalStorage = (() => {
try {
// iOS safari throws here if cookies are disabled
let unused = localStorage.length // eslint-disable-line
return true
} catch (e) {
return false
}
})()
if (hasLocalStorage && localStorage.store_currentInstance && localStorage.store_instanceThemes) {
let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str) let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str)
let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)] let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]
if (theme && theme !== 'default') { if (theme && theme !== 'default') {
@ -67,14 +77,14 @@ if (localStorage.store_currentInstance && localStorage.store_instanceThemes) {
} }
} }
if (!localStorage.store_currentInstance) { if (!hasLocalStorage || !localStorage.store_currentInstance) {
// if not logged in, show all these 'hidden-from-ssr' elements // if not logged in, show all these 'hidden-from-ssr' elements
let style = document.createElement('style') let style = document.createElement('style')
style.textContent = '.hidden-from-ssr { opacity: 1 !important; }' style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
document.head.appendChild(style) document.head.appendChild(style)
} }
if (localStorage.store_disableCustomScrollbars === 'true') { if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
// if user has disabled custom scrollbars, remove this style // if user has disabled custom scrollbars, remove this style
let theScrollbarStyle = document.getElementById('theScrollbarStyle') let theScrollbarStyle = document.getElementById('theScrollbarStyle')
theScrollbarStyle.setAttribute('media', 'only x') // disables the style theScrollbarStyle.setAttribute('media', 'only x') // disables the style