diff --git a/package.json b/package.json index 778fc6292..5110a5a9d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "cross-dirname": "^0.1.0", "deepmerge": "^4.2.2", "fast-deep-equal": "^3.1.3", - "idb": "^7.0.2", + "idb": "^8.0.0", "immer": "^9.0.12", "js-cookie": "^2.2.1", "letter-generator": "^2.2.1", diff --git a/src/DataType/SavedIdData.ts b/src/DataType/SavedIdData.ts index cdfcb61e2..5fea99e58 100644 --- a/src/DataType/SavedIdData.ts +++ b/src/DataType/SavedIdData.ts @@ -1,12 +1,22 @@ import type { IdDataElement, Signature } from '../types/request'; import type { SetOptional } from 'type-fest'; -import { rethrow, WarningException } from '../Utility/errors'; +import { rethrow, WarningException, ErrorException, GenericException, NoticeException } from '../Utility/errors'; import Cookie from 'js-cookie'; import LocalForage from 'localforage'; import { produce, nothing } from 'immer'; import { isAddress, EMTPY_ADDRESS } from '../Utility/requests'; +const catchUnavailableStorage = (rethrowCallback: (err: GenericException) => void) => (error: Error) => { + // This just means that the localStorage is disabled and we still went into SavedIdData somehow. We shouldn’t annoy the user and this doesn’t break anything. + if (error.message.includes('No available storage method found')) { + rethrow(NoticeException.fromError(error)); + return; + } + rethrowCallback(ErrorException.fromError(error)); +}; + export class SavedIdData { + // Get rid of localforage at some point. localforage_instance: LocalForage; constructor() { @@ -27,7 +37,9 @@ export class SavedIdData { // '::' is a special character and disallowed in the database for user inputs. The user will not encounter that as the description will be saved in the original state with the data object. return this.localforage_instance .setItem(data.desc.replace('/::/g', '__'), to_store) - .catch((error) => rethrow(error, 'Saving id_data failed.', { desc: to_store['desc'] })); + .catch( + catchUnavailableStorage((error) => rethrow(error, 'Saving id_data failed.', { desc: to_store['desc'] })) + ); } storeFixed(data: IdDataElement) { @@ -40,7 +52,9 @@ export class SavedIdData { return this.localforage_instance .setItem(data.type + '::fixed', to_store) - .catch((error) => rethrow(error, 'Saving id_data failed.', { desc: to_store.desc })); + .catch( + catchUnavailableStorage((error) => rethrow(error, 'Saving id_data failed.', { desc: to_store.desc })) + ); } storeArray(array: IdDataElement[], fixed_only = true) { @@ -54,31 +68,31 @@ export class SavedIdData { storeSignature(signature: Signature) { return this.localforage_instance .setItem('::signature', signature) - .catch((error) => rethrow(error, 'Saving signature failed.', { signature })); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Saving signature failed.', { signature }))); } getByDesc(desc: string) { return this.localforage_instance .getItem(desc.replace(/::/g, '__')) - .catch((error) => rethrow(error, 'Could not retrieve id_data.', { desc })); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not retrieve id_data.', { desc }))); } getFixed(type: 'name' | 'birthdate' | 'email' | 'address') { return this.localforage_instance .getItem(type + '::fixed') - .catch((error) => rethrow(error, 'Could not retrieve fixed id_data.', { type })); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not retrieve fixed id_data.', { type }))); } getSignature() { return this.localforage_instance .getItem('::signature') - .catch((error) => rethrow(error, 'Could not retrieve signature.')); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not retrieve signature.'))); } removeByDesc(desc: string) { return this.localforage_instance .removeItem(desc.replace(/::/g, '__')) - .catch((error) => rethrow(error, 'Could not delete id_data.', { desc })); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not delete id_data.', { desc }))); } getAllFixed() { @@ -88,7 +102,7 @@ export class SavedIdData { if (desc.match(/.*?::fixed$/)) id_data.push(data); }) .then(() => id_data) - .catch((error) => rethrow(error, 'Could not retrieve all fixed id_data')); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not retrieve all fixed id_data'))); } getAll(exclude_fixed = true) { @@ -99,7 +113,7 @@ export class SavedIdData { id_data.push(data); }) .then(() => id_data) - .catch((error) => rethrow(error, 'Could not retrieve all id_data')); + .catch(catchUnavailableStorage((error) => rethrow(error, 'Could not retrieve all id_data'))); } clear() { diff --git a/src/Utility/Privacy.ts b/src/Utility/Privacy.ts index c6592822d..9a4940a0e 100644 --- a/src/Utility/Privacy.ts +++ b/src/Utility/Privacy.ts @@ -15,12 +15,12 @@ export const PRIVACY_ACTIONS: Record = { SAVE_MY_REQUESTS: { id: 'save_my_requests', default: navigator.cookieEnabled, - dnt: true, + dnt: navigator.cookieEnabled, }, SAVE_ID_DATA: { id: 'save_id_data', default: navigator.cookieEnabled, - dnt: true, + dnt: navigator.cookieEnabled, }, // TELEMETRY: { // 'id': 'telemetry', diff --git a/src/Utility/PrivacyAsyncStorage.ts b/src/Utility/PrivacyAsyncStorage.ts index 860078893..f7552f0c0 100644 --- a/src/Utility/PrivacyAsyncStorage.ts +++ b/src/Utility/PrivacyAsyncStorage.ts @@ -14,6 +14,15 @@ export type PrivacyAsyncStorageOption = { type KeyValueDatabase = IDBPDatabase<{ [key: string]: string }>; +const errorFilter = (e: Error) => + // These migh be caused if IndexedDB is disabled in Firefox + e.name === 'InvalidStateError' || + e.name === 'SecurityError' || + // We couldn’t identify the cause for this error, but it seems to be caused by problem in the browser, so there is + // no need to tell the user about it (we should fail gracefully anyway). + // See also: https://github.com/datenanfragen/website/issues/1014 + e.message === 'Internal Error'; + export class PrivacyAsyncStorage { #db?: typeof localStorage | KeyValueDatabase; #options: PrivacyAsyncStorageOption; @@ -49,7 +58,7 @@ export class PrivacyAsyncStorage { }, }) .catch((e: DOMException) => { - if (e.name === 'InvalidStateError') { + if (errorFilter(e)) { // Database is not writable, we are probably in Firefox' private browsing mode this.#storageType = 'localStorage'; this.#db = localStorage; @@ -144,20 +153,29 @@ export class PrivacyAsyncStorage { } static async doesStoreExist(name: string, storeName: string) { - const db: IDBPDatabase | void = await openDB(name, undefined, { blocking: () => db?.close() }).catch((e) => { - if (e.name === 'InvalidStateError' && e.name === 'VersionError') { - db?.close(); + try { + const db: IDBPDatabase | void = await openDB(name, undefined, { blocking: () => db?.close() }).catch( + (e) => { + if (errorFilter(e) || e.name === 'VersionError') { + db?.close(); + return; + } + rethrow(e, 'Error in doesStoreExist', { name, storeName, db }, t('indexeddb-error', 'error-msg')); + } + ); + + if (db) { + const result = db.objectStoreNames.contains(storeName); + db.close(); + return result; + } + } catch (e) { + if (e instanceof Error && errorFilter(e)) { return; } - rethrow(e, 'Error in doesStoreExist', { name, storeName, db }, t('indexeddb-error', 'error-msg')); - }); - - if (db) { - const result = db.objectStoreNames.contains(storeName); - db.close(); - return result; } return ( + localStorage && typeof Object.keys(localStorage).find((key) => new RegExp(`^${name}/${storeName}/`).test(key)) === 'string' ); } diff --git a/src/Utility/errors.ts b/src/Utility/errors.ts index 4dd1f64bd..82cf2b031 100644 --- a/src/Utility/errors.ts +++ b/src/Utility/errors.ts @@ -17,7 +17,7 @@ export function rethrow( }, 0); } -class GenericException extends Error { +export class GenericException extends Error { code = -1; description?: string; context?: Record; diff --git a/src/i18n/de.json b/src/i18n/de.json index c0cbe6696..fd89549a3 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -516,6 +516,7 @@ "privacy-controls": { "title": "Datenschutzeinstellungen", "explanation": "

Uns ist Dein Recht auf Datenschutz sehr wichtig, deshalb geben wir uns Mühe, unsere Datenerhebung und -verarbeitung so weit wie möglich zu beschränken. Die allermeisten Funktionen auf ${site_name} werden direkt auf Deinem Computer ausgeführt und die Daten, die Du eingibst, erreichen überhaupt nie unsere Server. Es gibt allerdings auch einige Funktionen, die wir leider nicht anbieten können, ohne einige Daten zu erheben (das trifft aber selbst auf die meisten Funktionen auf dieser Seite nicht zu).
Hier hast Du die Möglichkeit, selbst zu entscheiden, welche Funktionen Du aktivieren möchtest.

Wenn Du eine der folgenden Optionen setzt, wird Deine Entscheidung in einem Cookie gespeichert. Sollten wir für eine Option keinen entsprechenden Cookie finden, nutzen wir einen Standardwert, der unserer Meinung nach sinnvoll ist.

", + "explanation-cookies-disabled": "Wir speichern Deine Einstellungen als Cookies auf Deinem Gerät. Du hast Cookies deaktiviert, also haben wird datenschutzfreundliche Standardwerte gewählt.", "clear-cookies": "Alle Cookies löschen", "clear-my-requests": "Alle gespeicherten Anfragen löschen", "confirm-delete-my-requests": "Du hast die „Meine Anfragen“-Funktion deaktiviert. Willst Du auch alle gespeicherten Anfragen löschen?", diff --git a/src/i18n/en.json b/src/i18n/en.json index 8aced7c6f..39d39a73d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -515,6 +515,7 @@ "privacy-controls": { "title": "Privacy controls", "explanation": "

We deeply value your right to privacy and try to limit data collection and processing as much as possible. Most of the features on ${site_name} will be run directly on your computer and the data you enter will never even reach our servers. There are however some features we cannot offer without collecting some data (most settings even on this page do not require that, though).
Here, you have the option to decide yourself which features you want to enable.

When you set any of the options below, your choice will be saved in a cookie. If we don’t find a cookie for an option, we use a default value which we have decided on.

", + "explanation-cookies-disabled": "We store your settings in cookies on your device. You have disabled cookies, so we defaulted to privacy sensible defaults.", "clear-cookies": "Clear all cookies", "clear-my-requests": "Clear all saved requests", "confirm-delete-my-requests": "You have disabled the “my requests” feature. Do you also want to delete all saved requests?", diff --git a/src/privacy-controls.tsx b/src/privacy-controls.tsx index 925107338..abb0981f9 100644 --- a/src/privacy-controls.tsx +++ b/src/privacy-controls.tsx @@ -24,6 +24,7 @@ const PrivacyControl = (props: PrivacyControlProps) => ( checked={Privacy.isAllowed(PRIVACY_ACTIONS[props.privacyAction])} type="checkbox" className="form-element" + disabled={!navigator.cookieEnabled} onChange={(event) => { Privacy.setAllowed(PRIVACY_ACTIONS[props.privacyAction], event.currentTarget.checked); flash( @@ -91,6 +92,14 @@ const PrivacyControls = () => { + {!navigator.cookieEnabled ? ( +
+ +
+ ) : ( + <> + )} + {Object.keys(PRIVACY_ACTIONS).map((action) => ( = 2.1.2 < 3.0.0" -idb@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.2.tgz#7a067e20dd16539938e456814b7d714ba8db3892" - integrity sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg== +idb@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.0.tgz#33d7ed894ed36e23bcb542fb701ad579bfaad41f" + integrity sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw== ieee754@^1.1.13: version "1.2.1"