@dhis2/app-service-offline 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,12 @@ Object.defineProperty(exports, "useOnlineStatus", {
33
33
  return _onlineStatus.useOnlineStatus;
34
34
  }
35
35
  });
36
+ Object.defineProperty(exports, "clearSensitiveCaches", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _clearSensitiveCaches.clearSensitiveCaches;
40
+ }
41
+ });
36
42
 
37
43
  var _offlineProvider = require("./lib/offline-provider");
38
44
 
@@ -40,4 +46,6 @@ var _cacheableSection = require("./lib/cacheable-section");
40
46
 
41
47
  var _cacheableSectionState = require("./lib/cacheable-section-state");
42
48
 
43
- var _onlineStatus = require("./lib/online-status");
49
+ var _onlineStatus = require("./lib/online-status");
50
+
51
+ var _clearSensitiveCaches = require("./lib/clear-sensitive-caches");
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ var _FDBFactory = _interopRequireDefault(require("fake-indexeddb/lib/FDBFactory"));
4
+
5
+ var _idb = require("idb");
6
+
7
+ require("fake-indexeddb/auto");
8
+
9
+ var _clearSensitiveCaches = require("../clear-sensitive-caches");
10
+
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+
13
+ // Mocks for CacheStorage API
14
+ // Returns true if an existing cache is deleted
15
+ const makeCachesDeleteMock = keys => {
16
+ return jest.fn().mockImplementation(key => Promise.resolve(keys.includes(key)));
17
+ };
18
+
19
+ const keysMockDefault = jest.fn().mockImplementation(async () => []);
20
+ const deleteMockDefault = makeCachesDeleteMock([]);
21
+ const cachesDefault = {
22
+ keys: keysMockDefault,
23
+ delete: deleteMockDefault
24
+ };
25
+ window.caches = cachesDefault;
26
+ afterEach(() => {
27
+ window.caches = cachesDefault;
28
+ jest.clearAllMocks();
29
+ }); // silence debug logs for these tests
30
+
31
+ const originalDebug = console.debug;
32
+ beforeAll(() => {
33
+ jest.spyOn(console, 'debug').mockImplementation((...args) => {
34
+ const pattern = /Clearing sensitive caches/;
35
+
36
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
37
+ return;
38
+ }
39
+
40
+ return originalDebug.call(console, ...args);
41
+ });
42
+ });
43
+ afterAll(() => {
44
+ ;
45
+ console.debug.mockRestore();
46
+ });
47
+ it('does not fail if there are no caches or no sections-db', () => {
48
+ return expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBe(false);
49
+ });
50
+ it('clears potentially sensitive caches', async () => {
51
+ const testKeys = ['cache1', 'cache2', 'app-shell'];
52
+ const keysMock = jest.fn().mockImplementation(() => Promise.resolve(testKeys));
53
+ const deleteMock = makeCachesDeleteMock(testKeys);
54
+ window.caches = {
55
+ keys: keysMock,
56
+ delete: deleteMock
57
+ };
58
+ const cachesDeleted = await (0, _clearSensitiveCaches.clearSensitiveCaches)();
59
+ expect(cachesDeleted).toBe(true);
60
+ expect(deleteMock).toHaveBeenCalledTimes(3);
61
+ expect(deleteMock.mock.calls[0][0]).toBe('cache1');
62
+ expect(deleteMock.mock.calls[1][0]).toBe('cache2');
63
+ expect(deleteMock.mock.calls[2][0]).toBe('app-shell');
64
+ });
65
+ it('preserves keepable caches', async () => {
66
+ const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell', 'other-assets', 'workbox-precache-v2-https://hey.howareya.now/']);
67
+ window.caches = { ...cachesDefault,
68
+ keys: keysMock
69
+ };
70
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)();
71
+ expect(deleteMockDefault).toHaveBeenCalledTimes(3);
72
+ expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
73
+ expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
74
+ expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
75
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('other-assets');
76
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('workbox-precache-v2-https://hey.howareya.now/');
77
+ });
78
+ describe('clears sections-db', () => {
79
+ // Test DB
80
+ function openTestDB(dbName) {
81
+ // simplified version of app platform openDB logic
82
+ return (0, _idb.openDB)(dbName, 1, {
83
+ upgrade(db) {
84
+ db.createObjectStore(_clearSensitiveCaches.SECTIONS_STORE, {
85
+ keyPath: 'sectionId'
86
+ });
87
+ }
88
+
89
+ });
90
+ }
91
+
92
+ afterEach(() => {
93
+ // reset indexedDB state
94
+ window.indexedDB = new _FDBFactory.default();
95
+ });
96
+ it('clears sections-db if it exists', async () => {
97
+ // Open and populate test DB
98
+ const db = await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
99
+ await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
100
+ sectionId: 'id-1',
101
+ lastUpdated: new Date(),
102
+ requests: 3
103
+ });
104
+ await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
105
+ sectionId: 'id-2',
106
+ lastUpdated: new Date(),
107
+ requests: 3
108
+ });
109
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)(); // Sections-db should be cleared
110
+
111
+ const allSections = await db.getAll(_clearSensitiveCaches.SECTIONS_STORE);
112
+ expect(allSections).toHaveLength(0);
113
+ });
114
+ it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
115
+ const openMock = jest.fn();
116
+ window.indexedDB.open = openMock;
117
+ expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
118
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)();
119
+ expect(openMock).not.toHaveBeenCalled();
120
+ return expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
121
+ });
122
+ it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
123
+ // Open DB -- 'indexedDB.open' _would_ get called in this test
124
+ // if 'databases' property exists
125
+ await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
126
+ const openMock = jest.fn();
127
+ window.indexedDB.open = openMock; // Remove 'databases' from indexedDB prototype for this test
128
+ // (simulates Firefox environment)
129
+
130
+ const idbProto = Object.getPrototypeOf(window.indexedDB);
131
+ const databases = idbProto.databases;
132
+ delete idbProto.databases;
133
+ expect('databases' in window.indexedDB).toBe(false);
134
+ await expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBeDefined();
135
+ expect(openMock).not.toHaveBeenCalled(); // Restore indexedDB prototype for later tests
136
+
137
+ idbProto.databases = databases;
138
+ expect('databases' in window.indexedDB).toBe(true);
139
+ });
140
+ });
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.clearSensitiveCaches = clearSensitiveCaches;
7
+ exports.SECTIONS_STORE = exports.SECTIONS_DB = void 0;
8
+ // IndexedDB names; should be the same as in @dhis2/pwa
9
+ const SECTIONS_DB = 'sections-db';
10
+ exports.SECTIONS_DB = SECTIONS_DB;
11
+ const SECTIONS_STORE = 'sections-store'; // Non-sensitive caches that can be kept:
12
+
13
+ exports.SECTIONS_STORE = SECTIONS_STORE;
14
+ const KEEPABLE_CACHES = [/^workbox-precache/, // precached static assets
15
+ /^other-assets/ // static assets cached at runtime - shouldn't be sensitive
16
+ ];
17
+
18
+ /*
19
+ * Clears the 'sections-db' IndexedDB if it exists. Designed to avoid opening
20
+ * a new DB if it doesn't exist yet. Firefox can't check if 'sections-db'
21
+ * exists, in which circumstance the IndexedDB is unaffected. It's inelegant
22
+ * but acceptable because the IndexedDB has no sensitive data (only metadata
23
+ * of recorded sections), and the OfflineInterface handles discrepancies
24
+ * between CacheStorage and IndexedDB.
25
+ */
26
+ const clearDB = async dbName => {
27
+ if (!('databases' in indexedDB)) {
28
+ // FF does not have indexedDB.databases. For that, just clear caches,
29
+ // and offline interface will handle discrepancies in PWA apps.
30
+ return;
31
+ }
32
+
33
+ const dbs = await window.indexedDB.databases();
34
+
35
+ if (!dbs.some(({
36
+ name
37
+ }) => name === dbName)) {
38
+ // Sections-db is not created; nothing to do here
39
+ return;
40
+ }
41
+
42
+ return new Promise((resolve, reject) => {
43
+ // IndexedDB fun:
44
+ const openDBRequest = indexedDB.open(dbName);
45
+
46
+ openDBRequest.onsuccess = e => {
47
+ const db = e.target.result;
48
+ const tx = db.transaction(SECTIONS_STORE, 'readwrite'); // When the transaction completes is when the operation is done:
49
+
50
+ tx.oncomplete = () => resolve();
51
+
52
+ tx.onerror = e => reject(e.target.error);
53
+
54
+ const os = tx.objectStore(SECTIONS_STORE);
55
+ const clearReq = os.clear();
56
+
57
+ clearReq.onerror = e => reject(e.target.error);
58
+ };
59
+
60
+ openDBRequest.onerror = e => {
61
+ reject(e.target.error);
62
+ };
63
+ });
64
+ };
65
+ /**
66
+ * Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
67
+ * different user logs in to prevent someone from accessing a different user's
68
+ * caches. Should be able to be used in a non-PWA app.
69
+ */
70
+
71
+
72
+ async function clearSensitiveCaches(dbName = SECTIONS_DB) {
73
+ console.debug('Clearing sensitive caches');
74
+ const cacheKeys = await caches.keys();
75
+ return Promise.all([// (Resolves to 'false' because this can't detect if anything was deleted):
76
+ clearDB(dbName).then(() => false), // Remove caches if not in keepable list
77
+ ...cacheKeys.map(key => {
78
+ if (!KEEPABLE_CACHES.some(pattern => pattern.test(key))) {
79
+ return caches.delete(key);
80
+ }
81
+
82
+ return false;
83
+ })]).then(responses => {
84
+ // Return true if any caches have been cleared
85
+ // (caches.delete() returns true if a cache is deleted successfully)
86
+ // PWA apps can reload to restore their app shell cache
87
+ return responses.some(response => response);
88
+ });
89
+ }
package/build/es/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { OfflineProvider } from './lib/offline-provider';
2
2
  export { CacheableSection, useCacheableSection } from './lib/cacheable-section';
3
3
  export { useCachedSections } from './lib/cacheable-section-state';
4
- export { useOnlineStatus } from './lib/online-status';
4
+ export { useOnlineStatus } from './lib/online-status';
5
+ export { clearSensitiveCaches } from './lib/clear-sensitive-caches';
@@ -0,0 +1,132 @@
1
+ import FDBFactory from 'fake-indexeddb/lib/FDBFactory';
2
+ import { openDB } from 'idb';
3
+ import 'fake-indexeddb/auto';
4
+ import { clearSensitiveCaches, SECTIONS_DB, SECTIONS_STORE } from '../clear-sensitive-caches'; // Mocks for CacheStorage API
5
+ // Returns true if an existing cache is deleted
6
+
7
+ const makeCachesDeleteMock = keys => {
8
+ return jest.fn().mockImplementation(key => Promise.resolve(keys.includes(key)));
9
+ };
10
+
11
+ const keysMockDefault = jest.fn().mockImplementation(async () => []);
12
+ const deleteMockDefault = makeCachesDeleteMock([]);
13
+ const cachesDefault = {
14
+ keys: keysMockDefault,
15
+ delete: deleteMockDefault
16
+ };
17
+ window.caches = cachesDefault;
18
+ afterEach(() => {
19
+ window.caches = cachesDefault;
20
+ jest.clearAllMocks();
21
+ }); // silence debug logs for these tests
22
+
23
+ const originalDebug = console.debug;
24
+ beforeAll(() => {
25
+ jest.spyOn(console, 'debug').mockImplementation((...args) => {
26
+ const pattern = /Clearing sensitive caches/;
27
+
28
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
29
+ return;
30
+ }
31
+
32
+ return originalDebug.call(console, ...args);
33
+ });
34
+ });
35
+ afterAll(() => {
36
+ ;
37
+ console.debug.mockRestore();
38
+ });
39
+ it('does not fail if there are no caches or no sections-db', () => {
40
+ return expect(clearSensitiveCaches()).resolves.toBe(false);
41
+ });
42
+ it('clears potentially sensitive caches', async () => {
43
+ const testKeys = ['cache1', 'cache2', 'app-shell'];
44
+ const keysMock = jest.fn().mockImplementation(() => Promise.resolve(testKeys));
45
+ const deleteMock = makeCachesDeleteMock(testKeys);
46
+ window.caches = {
47
+ keys: keysMock,
48
+ delete: deleteMock
49
+ };
50
+ const cachesDeleted = await clearSensitiveCaches();
51
+ expect(cachesDeleted).toBe(true);
52
+ expect(deleteMock).toHaveBeenCalledTimes(3);
53
+ expect(deleteMock.mock.calls[0][0]).toBe('cache1');
54
+ expect(deleteMock.mock.calls[1][0]).toBe('cache2');
55
+ expect(deleteMock.mock.calls[2][0]).toBe('app-shell');
56
+ });
57
+ it('preserves keepable caches', async () => {
58
+ const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell', 'other-assets', 'workbox-precache-v2-https://hey.howareya.now/']);
59
+ window.caches = { ...cachesDefault,
60
+ keys: keysMock
61
+ };
62
+ await clearSensitiveCaches();
63
+ expect(deleteMockDefault).toHaveBeenCalledTimes(3);
64
+ expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
65
+ expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
66
+ expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
67
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('other-assets');
68
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('workbox-precache-v2-https://hey.howareya.now/');
69
+ });
70
+ describe('clears sections-db', () => {
71
+ // Test DB
72
+ function openTestDB(dbName) {
73
+ // simplified version of app platform openDB logic
74
+ return openDB(dbName, 1, {
75
+ upgrade(db) {
76
+ db.createObjectStore(SECTIONS_STORE, {
77
+ keyPath: 'sectionId'
78
+ });
79
+ }
80
+
81
+ });
82
+ }
83
+
84
+ afterEach(() => {
85
+ // reset indexedDB state
86
+ window.indexedDB = new FDBFactory();
87
+ });
88
+ it('clears sections-db if it exists', async () => {
89
+ // Open and populate test DB
90
+ const db = await openTestDB(SECTIONS_DB);
91
+ await db.put(SECTIONS_STORE, {
92
+ sectionId: 'id-1',
93
+ lastUpdated: new Date(),
94
+ requests: 3
95
+ });
96
+ await db.put(SECTIONS_STORE, {
97
+ sectionId: 'id-2',
98
+ lastUpdated: new Date(),
99
+ requests: 3
100
+ });
101
+ await clearSensitiveCaches(); // Sections-db should be cleared
102
+
103
+ const allSections = await db.getAll(SECTIONS_STORE);
104
+ expect(allSections).toHaveLength(0);
105
+ });
106
+ it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
107
+ const openMock = jest.fn();
108
+ window.indexedDB.open = openMock;
109
+ expect(await indexedDB.databases()).not.toContain(SECTIONS_DB);
110
+ await clearSensitiveCaches();
111
+ expect(openMock).not.toHaveBeenCalled();
112
+ return expect(await indexedDB.databases()).not.toContain(SECTIONS_DB);
113
+ });
114
+ it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
115
+ // Open DB -- 'indexedDB.open' _would_ get called in this test
116
+ // if 'databases' property exists
117
+ await openTestDB(SECTIONS_DB);
118
+ const openMock = jest.fn();
119
+ window.indexedDB.open = openMock; // Remove 'databases' from indexedDB prototype for this test
120
+ // (simulates Firefox environment)
121
+
122
+ const idbProto = Object.getPrototypeOf(window.indexedDB);
123
+ const databases = idbProto.databases;
124
+ delete idbProto.databases;
125
+ expect('databases' in window.indexedDB).toBe(false);
126
+ await expect(clearSensitiveCaches()).resolves.toBeDefined();
127
+ expect(openMock).not.toHaveBeenCalled(); // Restore indexedDB prototype for later tests
128
+
129
+ idbProto.databases = databases;
130
+ expect('databases' in window.indexedDB).toBe(true);
131
+ });
132
+ });
@@ -0,0 +1,80 @@
1
+ // IndexedDB names; should be the same as in @dhis2/pwa
2
+ export const SECTIONS_DB = 'sections-db';
3
+ export const SECTIONS_STORE = 'sections-store'; // Non-sensitive caches that can be kept:
4
+
5
+ const KEEPABLE_CACHES = [/^workbox-precache/, // precached static assets
6
+ /^other-assets/ // static assets cached at runtime - shouldn't be sensitive
7
+ ];
8
+
9
+ /*
10
+ * Clears the 'sections-db' IndexedDB if it exists. Designed to avoid opening
11
+ * a new DB if it doesn't exist yet. Firefox can't check if 'sections-db'
12
+ * exists, in which circumstance the IndexedDB is unaffected. It's inelegant
13
+ * but acceptable because the IndexedDB has no sensitive data (only metadata
14
+ * of recorded sections), and the OfflineInterface handles discrepancies
15
+ * between CacheStorage and IndexedDB.
16
+ */
17
+ const clearDB = async dbName => {
18
+ if (!('databases' in indexedDB)) {
19
+ // FF does not have indexedDB.databases. For that, just clear caches,
20
+ // and offline interface will handle discrepancies in PWA apps.
21
+ return;
22
+ }
23
+
24
+ const dbs = await window.indexedDB.databases();
25
+
26
+ if (!dbs.some(({
27
+ name
28
+ }) => name === dbName)) {
29
+ // Sections-db is not created; nothing to do here
30
+ return;
31
+ }
32
+
33
+ return new Promise((resolve, reject) => {
34
+ // IndexedDB fun:
35
+ const openDBRequest = indexedDB.open(dbName);
36
+
37
+ openDBRequest.onsuccess = e => {
38
+ const db = e.target.result;
39
+ const tx = db.transaction(SECTIONS_STORE, 'readwrite'); // When the transaction completes is when the operation is done:
40
+
41
+ tx.oncomplete = () => resolve();
42
+
43
+ tx.onerror = e => reject(e.target.error);
44
+
45
+ const os = tx.objectStore(SECTIONS_STORE);
46
+ const clearReq = os.clear();
47
+
48
+ clearReq.onerror = e => reject(e.target.error);
49
+ };
50
+
51
+ openDBRequest.onerror = e => {
52
+ reject(e.target.error);
53
+ };
54
+ });
55
+ };
56
+ /**
57
+ * Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
58
+ * different user logs in to prevent someone from accessing a different user's
59
+ * caches. Should be able to be used in a non-PWA app.
60
+ */
61
+
62
+
63
+ export async function clearSensitiveCaches(dbName = SECTIONS_DB) {
64
+ console.debug('Clearing sensitive caches');
65
+ const cacheKeys = await caches.keys();
66
+ return Promise.all([// (Resolves to 'false' because this can't detect if anything was deleted):
67
+ clearDB(dbName).then(() => false), // Remove caches if not in keepable list
68
+ ...cacheKeys.map(key => {
69
+ if (!KEEPABLE_CACHES.some(pattern => pattern.test(key))) {
70
+ return caches.delete(key);
71
+ }
72
+
73
+ return false;
74
+ })]).then(responses => {
75
+ // Return true if any caches have been cleared
76
+ // (caches.delete() returns true if a cache is deleted successfully)
77
+ // PWA apps can reload to restore their app shell cache
78
+ return responses.some(response => response);
79
+ });
80
+ }
@@ -2,3 +2,4 @@ export { OfflineProvider } from './lib/offline-provider';
2
2
  export { CacheableSection, useCacheableSection } from './lib/cacheable-section';
3
3
  export { useCachedSections } from './lib/cacheable-section-state';
4
4
  export { useOnlineStatus } from './lib/online-status';
5
+ export { clearSensitiveCaches } from './lib/clear-sensitive-caches';
@@ -0,0 +1,16 @@
1
+ export declare const SECTIONS_DB = "sections-db";
2
+ export declare const SECTIONS_STORE = "sections-store";
3
+ declare global {
4
+ interface IDBFactory {
5
+ databases(): Promise<[{
6
+ name: string;
7
+ version: number;
8
+ }]>;
9
+ }
10
+ }
11
+ /**
12
+ * Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
13
+ * different user logs in to prevent someone from accessing a different user's
14
+ * caches. Should be able to be used in a non-PWA app.
15
+ */
16
+ export declare function clearSensitiveCaches(dbName?: string): Promise<any>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dhis2/app-service-offline",
3
3
  "description": "A runtime service for online/offline detection and offline caching",
4
- "version": "3.1.0",
4
+ "version": "3.2.0",
5
5
  "main": "./build/cjs/index.js",
6
6
  "module": "./build/es/index.js",
7
7
  "types": "build/types/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "coverage": "yarn test --coverage"
34
34
  },
35
35
  "peerDependencies": {
36
- "@dhis2/app-service-alerts": "3.1.0",
36
+ "@dhis2/app-service-alerts": "3.2.0",
37
37
  "prop-types": "^15.7.2",
38
38
  "react": "^16.8.6",
39
39
  "react-dom": "^16.8.6"