Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs related to strict privacy settings in browsers #1166

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"brutusin-json-forms": "https://github.com/brutusin/json-forms",
"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",
Expand Down
34 changes: 24 additions & 10 deletions src/DataType/SavedIdData.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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<IdDataElement>(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<IdDataElement>(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>('::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() {
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions src/Utility/Privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const PRIVACY_ACTIONS: Record<string, PrivacyAction> = {
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',
Expand Down
40 changes: 29 additions & 11 deletions src/Utility/PrivacyAsyncStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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'
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Utility/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function rethrow(
}, 0);
}

class GenericException extends Error {
export class GenericException extends Error {
code = -1;
description?: string;
context?: Record<string, unknown>;
Expand Down
1 change: 1 addition & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@
"privacy-controls": {
"title": "Datenschutzeinstellungen",
"explanation": "<p>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 <strong>nicht</strong> zu).<br>Hier hast Du die Möglichkeit, selbst zu entscheiden, welche Funktionen Du aktivieren möchtest.</p><p>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.</p>",
"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?",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@
"privacy-controls": {
"title": "Privacy controls",
"explanation": "<p>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 <strong>do not</strong> require that, though).<br>Here, you have the option to decide yourself which features you want to enable.</p><p>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.</p>",
"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?",
Expand Down
9 changes: 9 additions & 0 deletions src/privacy-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -91,6 +92,14 @@ const PrivacyControls = () => {
<ClearMyRequestsModal />
<MarkupText id="explanation" />

{!navigator.cookieEnabled ? (
<div className="box box-info">
<Text id="explanation-cookies-disabled" />
</div>
) : (
<></>
)}

<table>
{Object.keys(PRIVACY_ACTIONS).map((action) => (
<PrivacyControl
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6925,10 +6925,10 @@ iconv-lite@^0.6.3:
dependencies:
safer-buffer ">= 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"
Expand Down