passbolt-browser-extension 5.2.0 → 5.3.2

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/Gruntfile.js +1 -1
  3. package/RELEASE_NOTES.md +27 -71
  4. package/doc/browser-extension-class-diagram.md +9 -0
  5. package/package.json +5 -5
  6. package/src/all/_locales/sl/messages.json +2 -2
  7. package/src/all/background_page/controller/clipboard/cancelClipboardContentFlushController.js +51 -0
  8. package/src/all/background_page/controller/clipboard/cancelClipboardContentFlushController.test.js +46 -0
  9. package/src/all/background_page/controller/clipboard/copyTemporarilyToClipboardController.js +53 -0
  10. package/src/all/background_page/controller/clipboard/copyTemporarilyToClipboardController.test.js +47 -0
  11. package/src/all/background_page/controller/clipboard/{clipboardController.js → copyToClipboardController.js} +8 -8
  12. package/src/all/background_page/controller/clipboard/copyToClipboardController.test.js +47 -0
  13. package/src/all/background_page/controller/extension/getExtensionVersionController.test.js +1 -1
  14. package/src/all/background_page/controller/metadata/getOrFindMetadataKeysSettingsController.js +53 -0
  15. package/src/all/background_page/controller/metadata/getOrFindMetadataKeysSettingsController.test.js +106 -0
  16. package/src/all/background_page/controller/resource/updateResourceLocalStorageByFolderParentIdController.js +70 -0
  17. package/src/all/background_page/controller/resource/updateResourceLocalStorageByFolderParentIdController.test.js +78 -0
  18. package/src/all/background_page/controller/share/shareOneFolderController.test.js +2 -1
  19. package/src/all/background_page/controller/share/shareResourcesController.test.js +1 -1
  20. package/src/all/background_page/controller/user/deleteDryRunUserController.js +73 -0
  21. package/src/all/background_page/controller/user/deleteDryRunUserController.test.js +129 -0
  22. package/src/all/background_page/controller/user/deleteUserController.js +76 -0
  23. package/src/all/background_page/controller/user/deleteUserController.test.js +141 -0
  24. package/src/all/background_page/event/actionLogEvents.js +5 -12
  25. package/src/all/background_page/event/appEvents.js +33 -0
  26. package/src/all/background_page/event/findAllForActionLogController.js +58 -0
  27. package/src/all/background_page/event/findAllForActionLogController.test.js +43 -0
  28. package/src/all/background_page/event/quickAccessEvents.js +32 -0
  29. package/src/all/background_page/event/resourceEvents.js +11 -0
  30. package/src/all/background_page/event/userEvents.js +7 -20
  31. package/src/all/background_page/event/webIntegrationEvents.js +11 -0
  32. package/src/all/background_page/model/actionLog/{actionLogModel.js → findActionLogService.js} +25 -5
  33. package/src/all/background_page/model/actionLog/findActionLogService.test.js +61 -0
  34. package/src/all/background_page/model/comment/commentModel.js +5 -5
  35. package/src/all/background_page/model/entity/actionLog/actionLogsCollection.test.data.js +18 -0
  36. package/src/all/background_page/model/entity/actionLog/defaultActionLogEntity.test.data.js +34 -0
  37. package/src/all/background_page/model/entity/resource/resourcesCollection.js +25 -0
  38. package/src/all/background_page/model/entity/resource/resourcesCollection.test.js +34 -0
  39. package/src/all/background_page/model/import/resources/resourcesKdbxImportParser.test.js +0 -1
  40. package/src/all/background_page/model/user/userModel.js +0 -60
  41. package/src/all/background_page/pagemod/appPagemod.js +0 -2
  42. package/src/all/background_page/pagemod/appPagemod.test.js +2 -6
  43. package/src/all/background_page/service/alarm/globalAlarmService.js +2 -0
  44. package/src/all/background_page/service/api/actionLog/{actionLogService.js → actionLogApiService.js} +5 -4
  45. package/src/all/background_page/service/api/actionLog/actionLogApiService.test.js +55 -0
  46. package/src/all/background_page/service/api/comment/{commentService.js → commentApiService.js} +6 -7
  47. package/src/all/background_page/service/api/comment/commentApiService.test.js +122 -0
  48. package/src/all/background_page/service/api/resource/resourceService.js +1 -0
  49. package/src/all/background_page/service/auth/postLogoutService.js +2 -0
  50. package/src/all/background_page/service/auth/postLogoutService.test.js +4 -1
  51. package/src/all/background_page/service/browser/browserService.js +22 -0
  52. package/src/all/background_page/service/clipboard/clipboardProviderService.js +40 -0
  53. package/src/all/background_page/service/clipboard/clipboardProviderService.test.js +61 -0
  54. package/src/all/background_page/service/clipboard/copyToClipboardService.js +123 -0
  55. package/src/all/background_page/service/clipboard/copyToClipboardService.test.js +174 -0
  56. package/src/all/background_page/service/resource/findAndUpdateResourcesLocalStorageService.js +52 -1
  57. package/src/all/background_page/service/resource/findAndUpdateResourcesLocalStorageService.test.js +100 -0
  58. package/src/all/background_page/service/resource/findResourcesService.js +53 -19
  59. package/src/all/background_page/service/resource/findResourcesService.test.js +191 -0
  60. package/src/all/background_page/service/resourceType/updateResourceTypesService.test.js +1 -1
  61. package/src/all/background_page/service/share/shareResourceService.test.js +1 -1
  62. package/src/all/background_page/service/user/deleteUserService.js +97 -0
  63. package/src/all/background_page/service/user/deleteUserService.test.js +178 -0
  64. package/src/all/locales/de-DE/common.json +2 -2
  65. package/src/all/locales/es-ES/common.json +2 -2
  66. package/src/all/locales/fr-FR/common.json +5 -5
  67. package/src/all/locales/it-IT/common.json +2 -2
  68. package/src/all/locales/ja-JP/common.json +2 -2
  69. package/src/all/locales/ko-KR/common.json +2 -2
  70. package/src/all/locales/lt-LT/common.json +2 -2
  71. package/src/all/locales/nl-NL/common.json +2 -2
  72. package/src/all/locales/pl-PL/common.json +5 -5
  73. package/src/all/locales/pt-BR/common.json +2 -2
  74. package/src/all/locales/ro-RO/common.json +2 -2
  75. package/src/all/locales/ru-RU/common.json +2 -2
  76. package/src/all/locales/sl-SI/common.json +33 -33
  77. package/src/all/locales/sv-SE/common.json +2 -2
  78. package/src/all/locales/uk-UA/common.json +3 -3
  79. package/src/chrome/manifest.json +1 -1
  80. package/src/chrome/polyfill/clipboard/edgeBackgroundPageClipboardService.js +31 -0
  81. package/src/chrome/polyfill/clipboard/edgeBackgroundPageClipboardService.test.js +51 -0
  82. package/src/chrome-mv3/index.js +3 -3
  83. package/src/chrome-mv3/manifest.json +1 -1
  84. package/src/chrome-mv3/offscreens/{fetch.html → offscreen.html} +1 -1
  85. package/src/chrome-mv3/offscreens/{fetch.js → offscreen.js} +2 -2
  86. package/src/chrome-mv3/offscreens/service/clipboard/writeClipobardOffscreenService.js +54 -0
  87. package/src/chrome-mv3/offscreens/service/clipboard/writeClipobardOffscreenService.test.js +56 -0
  88. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.js +36 -44
  89. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.test.data.js +0 -1
  90. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.test.js +90 -120
  91. package/src/chrome-mv3/offscreens/service/offscreen/handleOffscreenRequestService.js +85 -0
  92. package/src/chrome-mv3/offscreens/service/offscreen/handleOffscreenRequestService.test.js +99 -0
  93. package/src/chrome-mv3/polyfill/clipboardOffscreenPolyfill.js +19 -0
  94. package/src/chrome-mv3/serviceWorker/service/clipboard/requestClipboardOffscreenService.js +51 -0
  95. package/src/chrome-mv3/serviceWorker/service/clipboard/requestClipboardOffscreenService.test.js +70 -0
  96. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.js +25 -0
  97. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.test.data.js +21 -0
  98. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.test.js +33 -0
  99. package/src/chrome-mv3/serviceWorker/service/network/requestFetchOffscreenService.js +25 -50
  100. package/src/chrome-mv3/serviceWorker/service/network/requestFetchOffscreenService.test.js +16 -39
  101. package/src/chrome-mv3/serviceWorker/service/network/responseFetchOffscreenService.js +14 -45
  102. package/src/chrome-mv3/serviceWorker/service/network/responseFetchOffscreenService.test.js +5 -37
  103. package/src/chrome-mv3/serviceWorker/service/offscreen/createOffscreenDocumentService.js +43 -0
  104. package/src/chrome-mv3/serviceWorker/service/offscreen/createOffscreenDocumentService.test.js +48 -0
  105. package/src/chrome-mv3/serviceWorker/service/offscreen/handleOffscreenResponseService.js +119 -0
  106. package/src/chrome-mv3/serviceWorker/service/offscreen/handleOffscreenResponseService.test.js +159 -0
  107. package/src/firefox/manifest.json +1 -1
  108. package/src/safari/manifest.json +1 -1
  109. package/test/jest.setup.js +4 -0
  110. package/test/mocks/mockNavigatorClipboard.js +40 -0
  111. package/test/mocks/mockWebExtensionPolyfill.js +2 -1
  112. package/{webpack-offscreens.fetch.config.js → webpack-offscreens.config.js} +1 -1
  113. package/webpack.service-worker.config.js +1 -0
  114. package/src/all/background_page/controller/clipboard/clipboardController.test.js +0 -68
  115. package/src/all/background_page/event/clipboardEvents.js +0 -28
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Passbolt ~ Open source password manager for teams
3
+ * Copyright (c) Passbolt SA (https://www.passbolt.com)
4
+ *
5
+ * Licensed under GNU Affero General Public License version 3 of the or any later version.
6
+ * For full copyright and license information, please see the LICENSE.txt
7
+ * Redistributions of files must retain the above copyright notice.
8
+ *
9
+ * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
10
+ * @license https://opensource.org/licenses/AGPL-3.0 AGPL License
11
+ * @link https://www.passbolt.com Passbolt(tm)
12
+ * @since 5.3.2
13
+ */
14
+ import "../../../../../test/mocks/mockNavigatorClipboard";
15
+ import ClipboardProviderService from "./clipboardProviderService";
16
+ import BrowserService from "../browser/browserService";
17
+ import EdgeBackgroundPageClipboardService from "../../../../chrome/polyfill/clipboard/edgeBackgroundPageClipboardService";
18
+
19
+ beforeEach(() => {
20
+ global.customNavigatorClipboard = undefined;
21
+ });
22
+
23
+ describe("ClipboardProviderService", () => {
24
+ describe("::getClipboard", () => {
25
+ it("should return the customNavigatorClipboard if it is set", async() => {
26
+ expect.assertions(1);
27
+
28
+ const customNavigatorClipboard = {writeText: jest.fn()};
29
+ global.customNavigatorClipboard = customNavigatorClipboard;
30
+
31
+ expect(ClipboardProviderService.getClipboard()).toStrictEqual(customNavigatorClipboard);
32
+ });
33
+
34
+ it("should return the EdgeBackgroundPageClipboardService if not on Firefox and is on MV2", async() => {
35
+ expect.assertions(1);
36
+
37
+ chrome.runtime.getManifest.mockImplementation(() => ({manifest_version: 2}));
38
+ jest.spyOn(BrowserService, "isFirefox").mockImplementation(() => false);
39
+
40
+ expect(ClipboardProviderService.getClipboard()).toStrictEqual(EdgeBackgroundPageClipboardService);
41
+ });
42
+
43
+ it("should return the navigator.clipboard in every other cases: Firefox", async() => {
44
+ expect.assertions(1);
45
+
46
+ chrome.runtime.getManifest.mockImplementation(() => ({manifest_version: 2}));
47
+ jest.spyOn(BrowserService, "isFirefox").mockImplementation(() => true);
48
+
49
+ expect(ClipboardProviderService.getClipboard()).toStrictEqual(navigator.clipboard);
50
+ });
51
+
52
+ it("should return the navigator.clipboard in every other cases: Firefox + mv3", async() => {
53
+ expect.assertions(1);
54
+
55
+ chrome.runtime.getManifest.mockImplementation(() => ({manifest_version: 3}));
56
+ jest.spyOn(BrowserService, "isFirefox").mockImplementation(() => true);
57
+
58
+ expect(ClipboardProviderService.getClipboard()).toStrictEqual(navigator.clipboard);
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Passbolt ~ Open source password manager for teams
3
+ * Copyright (c) Passbolt SA (https://www.passbolt.com)
4
+ *
5
+ * Licensed under GNU Affero General Public License version 3 of the or any later version.
6
+ * For full copyright and license information, please see the LICENSE.txt
7
+ * Redistributions of files must retain the above copyright notice.
8
+ *
9
+ * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
10
+ * @license https://opensource.org/licenses/AGPL-3.0 AGPL License
11
+ * @link https://www.passbolt.com Passbolt(tm)
12
+ * @since 5.3.2
13
+ */
14
+ import {assertString} from "../../utils/assertions";
15
+ import BrowserService from "../browser/browserService";
16
+ import ClipboardProviderService from "./clipboardProviderService";
17
+
18
+ const CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND = 30;
19
+ const CLIPBOARD_TEMPORARY_CONTENT_FLUSH_ALARM = "ClipboardTemporaryContentFlush";
20
+ /**
21
+ * The service aims to use the clipboard capability on Edge MV2 where:
22
+ * - navigator.clipboard.writeText cannot be used in Edge MV2 background page due to a focus issue.
23
+ * - offscreen clipboard cannot be used due to offscreen API available only with MV3.
24
+ */
25
+ export default class CopyToClipboardService {
26
+ /**
27
+ * @constructor
28
+ */
29
+ constructor() {
30
+ this.clipboard = ClipboardProviderService.getClipboard();
31
+ }
32
+
33
+ /**
34
+ * Copies the given data into the clipboard and sets a timer to remove after 30sec.
35
+ * @param {string} data
36
+ * @return {Promise<void>}
37
+ */
38
+ async copyTemporarily(data) {
39
+ assertString(data);
40
+
41
+ await this.clearAlarm();
42
+ await this.clipboard.writeText(data);
43
+
44
+ await browser.alarms.create(CopyToClipboardService.ALARM_NAME, {
45
+ when: Date.now() + CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND * 1000
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Copies the given data into the clipboard and unsets any flush timer.
51
+ * @param {string} data
52
+ * @return {Promise<void>}
53
+ */
54
+ async copy(data) {
55
+ assertString(data);
56
+
57
+ await this.clearAlarm();
58
+ await this.clipboard.writeText(data);
59
+ }
60
+
61
+ /**
62
+ * Flushes the clipboard if the content is the expected one.
63
+ * @returns {Promise<void>}
64
+ */
65
+ async flushTemporaryContent() {
66
+ await this.clearAlarm();
67
+ await this.clipboard.writeText(this._getContentToFlushClipboard());
68
+ }
69
+
70
+ /**
71
+ * Flushes the clipboard content if there is any temporary content in it.
72
+ * @returns {Promise<void>}
73
+ */
74
+ async flushTemporaryContentIfAny() {
75
+ const hasTemporaryContent = Boolean(await browser.alarms.get(CopyToClipboardService.ALARM_NAME));
76
+ if (hasTemporaryContent) {
77
+ this.flushTemporaryContent();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Removes the flush alarm if any.
83
+ * @returns {Promise<void>}
84
+ */
85
+ async clearAlarm() {
86
+ await browser.alarms.clear(CopyToClipboardService.ALARM_NAME);
87
+ }
88
+
89
+ /**
90
+ * Returns a string that the browser can use to clean the clipboard.
91
+ * The "empty" string depends on the browser.
92
+ * Chromimum: "" is not accepted but "\x00" does the trick
93
+ * Firefox: "" is working fine but "\x00" is a bit buggy (the character could be pasted)
94
+ * @returns {string}
95
+ * @private
96
+ */
97
+ _getContentToFlushClipboard() {
98
+ return BrowserService.isFirefox()
99
+ ? ""
100
+ : "\x00";
101
+ }
102
+
103
+ /**
104
+ * Flush the current stored passphrase when the PassphraseStorageFlush alarm triggers.
105
+ * This is a top-level alarm callback
106
+ * @param {Alarm} alarm
107
+ * @returns {Promise<void>}
108
+ */
109
+ static async handleClipboardTemporaryContentFlushEvent(alarm) {
110
+ if (alarm.name === CopyToClipboardService.ALARM_NAME) {
111
+ const clipboardService = new CopyToClipboardService();
112
+ await clipboardService.flushTemporaryContent();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Returns the PASSPHRASE_FLUSH_ALARM name
118
+ * @returns {string}
119
+ */
120
+ static get ALARM_NAME() {
121
+ return CLIPBOARD_TEMPORARY_CONTENT_FLUSH_ALARM;
122
+ }
123
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Passbolt ~ Open source password manager for teams
3
+ * Copyright (c) Passbolt SA (https://www.passbolt.com)
4
+ *
5
+ * Licensed under GNU Affero General Public License version 3 of the or any later version.
6
+ * For full copyright and license information, please see the LICENSE.txt
7
+ * Redistributions of files must retain the above copyright notice.
8
+ *
9
+ * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
10
+ * @license https://opensource.org/licenses/AGPL-3.0 AGPL License
11
+ * @link https://www.passbolt.com Passbolt(tm)
12
+ * @since 5.3.2
13
+ */
14
+ import AccountEntity from "../../model/entity/account/accountEntity";
15
+ import {defaultAccountDto} from "../../model/entity/account/accountEntity.test.data";
16
+ import CopyToClipboardService from "./copyToClipboardService";
17
+ import "../../../../../test/mocks/mockNavigatorClipboard";
18
+ import "../../../../../test/mocks/mockAlarms";
19
+
20
+ beforeEach(async() => {
21
+ jest.clearAllMocks();
22
+ jest.useFakeTimers();
23
+ await browser.alarms.clearAll();
24
+ });
25
+
26
+ describe("CopyToClipboardService", () => {
27
+ describe("::copyTemporarily", () => {
28
+ it("should copy the given data into the clipboard and set an alarm", async() => {
29
+ expect.assertions(5);
30
+
31
+ const account = new AccountEntity(defaultAccountDto());
32
+ const service = new CopyToClipboardService(account);
33
+
34
+ //ensures the test is not blocked by an unmocked promise
35
+ jest.spyOn(browser.alarms, "create").mockImplementation(() => {});
36
+ jest.spyOn(browser.alarms, "clear").mockImplementation(() => {});
37
+
38
+ const data = "data";
39
+ await service.copyTemporarily(data);
40
+
41
+ expect(await navigator.clipboard.readText()).toStrictEqual(data);
42
+ expect(browser.alarms.create).toHaveBeenCalledTimes(1);
43
+ expect(browser.alarms.create).toHaveBeenCalledWith(CopyToClipboardService.ALARM_NAME, {when: Date.now() + 30_000});
44
+ expect(browser.alarms.clear).toHaveBeenCalledTimes(1);
45
+ expect(browser.alarms.clear).toHaveBeenCalledWith(CopyToClipboardService.ALARM_NAME);
46
+ });
47
+
48
+ it("should throw an error if the data is not a string", async() => {
49
+ expect.assertions(1);
50
+
51
+ const account = new AccountEntity(defaultAccountDto());
52
+ const service = new CopyToClipboardService(account);
53
+
54
+ await expect(() => service.copyTemporarily(42)).rejects.toThrowError();
55
+ });
56
+ });
57
+
58
+ describe("::copy", () => {
59
+ it("should copy the given data into the clipboard and unset any flush alarm", async() => {
60
+ expect.assertions(3);
61
+
62
+ const account = new AccountEntity(defaultAccountDto());
63
+ const service = new CopyToClipboardService(account);
64
+
65
+ //ensures the test is not blocked by an unmocked promise
66
+ jest.spyOn(browser.alarms, "clear").mockImplementation(() => {});
67
+
68
+ const data = "data";
69
+ await service.copy(data);
70
+
71
+ expect(await navigator.clipboard.readText()).toStrictEqual(data);
72
+ expect(browser.alarms.clear).toHaveBeenCalledTimes(1);
73
+ expect(browser.alarms.clear).toHaveBeenCalledWith(CopyToClipboardService.ALARM_NAME);
74
+ });
75
+
76
+ it("should throw an error if the data is not a string", async() => {
77
+ expect.assertions(1);
78
+
79
+ const account = new AccountEntity(defaultAccountDto());
80
+ const service = new CopyToClipboardService(account);
81
+
82
+ await expect(() => service.copy(42)).rejects.toThrowError();
83
+ });
84
+ });
85
+
86
+ describe("::flushTemporaryContent", () => {
87
+ it("should flush the data in the clipboard", async() => {
88
+ expect.assertions(1);
89
+
90
+ const account = new AccountEntity(defaultAccountDto());
91
+ const service = new CopyToClipboardService(account);
92
+
93
+ await service.copyTemporarily("data");
94
+ await service.flushTemporaryContent();
95
+
96
+ expect(await navigator.clipboard.readText()).toStrictEqual("\x00");
97
+ });
98
+ });
99
+
100
+ describe("::flushTemporaryContentIfAny", () => {
101
+ it("should flush the data in the clipboard if an alarm has been set", async() => {
102
+ expect.assertions(3);
103
+
104
+ const account = new AccountEntity(defaultAccountDto());
105
+ const service = new CopyToClipboardService(account);
106
+
107
+ jest.spyOn(browser.alarms, "get").mockImplementation(async() => ({test: 42}));
108
+ jest.spyOn(navigator.clipboard, "writeText").mockImplementation(async() => {});
109
+
110
+ await service.copyTemporarily("data");
111
+ await service.flushTemporaryContentIfAny();
112
+
113
+ expect(await navigator.clipboard.writeText).toHaveBeenCalledTimes(2);
114
+ expect(await navigator.clipboard.writeText).toHaveBeenCalledWith("data");
115
+ expect(await navigator.clipboard.writeText).toHaveBeenCalledWith("\x00");
116
+ });
117
+
118
+ it("should not flush the data in the clipboard if not alarm has been set", async() => {
119
+ expect.assertions(2);
120
+
121
+ const account = new AccountEntity(defaultAccountDto());
122
+ const service = new CopyToClipboardService(account);
123
+ const clipboardContent = "data";
124
+
125
+ jest.spyOn(browser.alarms, "get").mockImplementation(() => null);
126
+ jest.spyOn(navigator.clipboard, "writeText").mockImplementation(async() => {});
127
+
128
+ await service.copy(clipboardContent);
129
+ await service.flushTemporaryContentIfAny();
130
+
131
+ expect(await navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
132
+ expect(await navigator.clipboard.writeText).toHaveBeenCalledWith(clipboardContent);
133
+ });
134
+ });
135
+
136
+ describe("::clearAlarm", () => {
137
+ it("should remove the alarm if any", async() => {
138
+ expect.assertions(1);
139
+
140
+ jest.spyOn(browser.alarms, "clear").mockImplementation(() => {});
141
+
142
+ const account = new AccountEntity(defaultAccountDto());
143
+ const service = new CopyToClipboardService(account);
144
+
145
+ await service.clearAlarm();
146
+
147
+ expect(browser.alarms.clear).toHaveBeenCalledTimes(1);
148
+ });
149
+ });
150
+
151
+ describe("::handleClipboardTemporaryContentFlushEvent", () => {
152
+ it("should call to flush the clipboard if the alarm triggers", async() => {
153
+ expect.assertions(1);
154
+
155
+ jest.spyOn(CopyToClipboardService.prototype, "flushTemporaryContent").mockImplementation(() => {});
156
+
157
+ const alarm = {name: "ClipboardTemporaryContentFlush"};
158
+ await CopyToClipboardService.handleClipboardTemporaryContentFlushEvent(alarm);
159
+
160
+ expect(CopyToClipboardService.prototype.flushTemporaryContent).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ it("should do nothing if the alarm is not the right one", async() => {
164
+ expect.assertions(1);
165
+
166
+ jest.spyOn(CopyToClipboardService.prototype, "flushTemporaryContent").mockImplementation(() => {});
167
+
168
+ const alarm = {name: "other-alarm"};
169
+ await CopyToClipboardService.handleClipboardTemporaryContentFlushEvent(alarm);
170
+
171
+ expect(CopyToClipboardService.prototype.flushTemporaryContent).not.toHaveBeenCalled();
172
+ });
173
+ });
174
+ });
@@ -12,7 +12,7 @@
12
12
  * @since 4.6.0
13
13
  */
14
14
  import ResourceLocalStorage from "../local_storage/resourceLocalStorage";
15
- import {assertNumber} from "../../utils/assertions";
15
+ import {assertNumber, assertUuid} from "../../utils/assertions";
16
16
  import FindResourcesService from "./findResourcesService";
17
17
  import ResourcesCollection from "../../model/entity/resource/resourcesCollection";
18
18
  import ResourceTypeModel from "../../model/resourceType/resourceTypeModel";
@@ -113,6 +113,57 @@ class FindAndUpdateResourcesLocalStorage {
113
113
  await ResourceLocalStorage.addOrReplaceResourcesCollection(resourcesCollection);
114
114
  return resourcesCollection;
115
115
  }
116
+
117
+ /**
118
+ * Find and update the local storage with the resources filtered by parent folder id retrieved from the API.
119
+ * @param {string} parentFolderId The parent folder id to filter the resources with.
120
+ * @param {string|null} [passphrase = null] The passphrase to use to decrypt the metadata. Marked as optional as it
121
+ * might be available in the passphrase session storage.
122
+ * @return {Promise<ResourcesCollection>} The resource shared with the group
123
+ * @throw {TypeError} If the parentFolderId is not valid UUID
124
+ */
125
+ async findAndUpdateAllByParentFolderId(parentFolderId, passphrase = null) {
126
+ assertUuid(parentFolderId);
127
+ const resourceTypes = await this.resourceTypeModel.getOrFindAll();
128
+
129
+ const apiResourcesCollection = await this.findResourcesServices.findAllByParentFolderIdForLocalStorage(parentFolderId);
130
+ apiResourcesCollection.filterByResourceTypes(resourceTypes);
131
+
132
+ const apiResourcesCollectionMapIds = apiResourcesCollection.items.reduce((result, resource) => {
133
+ result[resource.id] = resource;
134
+ return result;
135
+ }, {});
136
+
137
+ const localStorageResourcesCollection = new ResourcesCollection(await ResourceLocalStorage.get(), {validate: false});
138
+ const knownResourcesCollectionInFolderIds = localStorageResourcesCollection.items.reduce((result, resource) => {
139
+ if (resource.folderParentId === parentFolderId) {
140
+ result.push(resource.id);
141
+ }
142
+ return result;
143
+ }, Array(apiResourcesCollection.length));
144
+
145
+ apiResourcesCollection.setDecryptedMetadataFromCollection(localStorageResourcesCollection);
146
+ localStorageResourcesCollection.updateWithCollection(apiResourcesCollection);
147
+
148
+ const movedOrRemovedResourcesIds = knownResourcesCollectionInFolderIds.filter(id => !apiResourcesCollectionMapIds[id]);
149
+ if (movedOrRemovedResourcesIds.length > 0) {
150
+ const updatedResources = await this.findResourcesServices.findAllByIdsForLocalStorage(movedOrRemovedResourcesIds);
151
+
152
+ updatedResources.setDecryptedMetadataFromCollection(localStorageResourcesCollection);
153
+ localStorageResourcesCollection.updateWithCollection(updatedResources);
154
+
155
+ // These resources have been deleted (or permissions revoked) and need to be removed from the local storage.
156
+ const remainingResourcesIds = movedOrRemovedResourcesIds
157
+ .filter(id => !updatedResources.getFirstById(id));
158
+
159
+ localStorageResourcesCollection.removeMany(remainingResourcesIds);
160
+ }
161
+
162
+ await this.decryptMetadataService.decryptAllFromForeignModels(localStorageResourcesCollection, passphrase, {ignoreDecryptionError: true, updateSessionKeys: true});
163
+ localStorageResourcesCollection.filterOutMetadataEncrypted();
164
+
165
+ await ResourceLocalStorage.set(localStorageResourcesCollection);
166
+ }
116
167
  }
117
168
 
118
169
  export default FindAndUpdateResourcesLocalStorage;
@@ -371,4 +371,104 @@ describe("UpdateResourcesLocalStorage", () => {
371
371
  expect(expectLocalStorageResult[1]).toEqual(ResourceEntity.transformDtoFromV4toV5(resourceDto2));
372
372
  });
373
373
  });
374
+
375
+ describe("::findAndUpdateAllByParentFolderId", () => {
376
+ let service;
377
+
378
+ beforeEach(() => {
379
+ service = new FindAndUpdateResourcesLocalStorage(account, apiClientOptions);
380
+ jest.spyOn(ResourceTypeService.prototype, "findAll").mockImplementation(() => resourceTypesCollectionDto());
381
+ });
382
+
383
+ it("should extract the id from the resource collection", async() => {
384
+ expect.assertions(2);
385
+
386
+ const parentFolderId = uuidv4();
387
+
388
+ const resourcesDto = multipleResourceDtos();
389
+ jest.spyOn(ResourceService.prototype, "findAll").mockImplementation(() => resourcesDto);
390
+ jest.spyOn(service.findResourcesServices, "findAllByParentFolderIdForLocalStorage");
391
+
392
+ await service.findAndUpdateAllByParentFolderId(parentFolderId);
393
+
394
+ expect(service.findResourcesServices.findAllByParentFolderIdForLocalStorage).toHaveBeenCalledTimes(1);
395
+ expect(service.findResourcesServices.findAllByParentFolderIdForLocalStorage).toHaveBeenCalledWith(parentFolderId);
396
+ });
397
+
398
+ it("should assert its parameter", async() => {
399
+ expect.assertions(1);
400
+ await expect(() => service.findAndUpdateAllByParentFolderId("test")).rejects.toThrow();
401
+ });
402
+
403
+ it("should update the local storage resource collection by removing deleted resources, moving resources and updating resources data", async() => {
404
+ expect.assertions(8);
405
+
406
+ const parentFolderId = uuidv4();
407
+ const otherParentFolderId = uuidv4();
408
+
409
+ /*
410
+ * Resources collection in local storage:
411
+ * Resource0.folderParentId = null;
412
+ * Resource1.folderParentId = parentFolderId; // on API it should be modified only
413
+ * Resource2.folderParentId = parentFolderId; // on API it should be moved
414
+ * Resource3.folderParentId = parentFolderId; // on API it should be removed
415
+ */
416
+
417
+ const resourcesDto = multipleResourceDtos();
418
+ resourcesDto[1].folder_parent_id = parentFolderId;
419
+ resourcesDto[2].folder_parent_id = parentFolderId;
420
+ resourcesDto[3].folder_parent_id = parentFolderId;
421
+
422
+ delete resourcesDto[0].name;
423
+ delete resourcesDto[1].name;
424
+ delete resourcesDto[2].name;
425
+ delete resourcesDto[3].name;
426
+
427
+ const localStorageResourceCollection = new ResourcesCollection(resourcesDto);
428
+ await ResourceLocalStorage.set(localStorageResourceCollection);
429
+
430
+ // the resources in the folder on the API does not have resource2 anymore but resources1 remains and is changed.
431
+ const apiResourcesDtoInFolder = [
432
+ {...resourcesDto[1]},
433
+ ];
434
+ apiResourcesDtoInFolder[0].metadata.name = "Resource1 - UPDATED";
435
+ apiResourcesDtoInFolder[0].modified = (new Date()).toISOString();
436
+
437
+ // resource2 on the API will be return and updated, resource3 will never be sent back as it is deleted
438
+ const allIdsApiResourcesDto = [
439
+ {...resourcesDto[2]},
440
+ ];
441
+ allIdsApiResourcesDto[0].folder_parent_id = otherParentFolderId;
442
+
443
+ async function mockedFindAllApi(_, filter) {
444
+ const isParentFolderSearchRequest = Boolean(filter["has-parent"]);
445
+
446
+ return isParentFolderSearchRequest
447
+ ? apiResourcesDtoInFolder
448
+ : allIdsApiResourcesDto;
449
+ }
450
+
451
+ jest.spyOn(ResourceService.prototype, "findAll").mockImplementation(mockedFindAllApi);
452
+
453
+ await service.findAndUpdateAllByParentFolderId(parentFolderId);
454
+
455
+ const updatedResourceLocalStorage = new ResourcesCollection(await ResourceLocalStorage.get());
456
+
457
+ expect(updatedResourceLocalStorage).toHaveLength(3); //1 resource should be removed compared to the original data
458
+ const resource0 = updatedResourceLocalStorage.getFirstById(resourcesDto[0].id);
459
+ expect(resource0.toDto(ResourceEntity.ALL_CONTAIN_OPTIONS)).toStrictEqual(resourcesDto[0]); //this resource should remain unchanged
460
+
461
+ const updatedResource1 = updatedResourceLocalStorage.getFirstById(resourcesDto[1].id); // this resource should have been updated but not moved
462
+ expect(updatedResource1.metadata.name).toStrictEqual("Resource1 - UPDATED");
463
+ expect(updatedResource1.modified).toStrictEqual(apiResourcesDtoInFolder[0].modified);
464
+ expect(updatedResource1.folderParentId).toStrictEqual(parentFolderId);
465
+
466
+ const updatedResource2 = updatedResourceLocalStorage.getFirstById(resourcesDto[2].id); // this resource should not have changed per say but only moved
467
+ expect(updatedResource2.modified).toStrictEqual(resourcesDto[2].modified);
468
+ expect(updatedResource2.folderParentId).toStrictEqual(otherParentFolderId);
469
+
470
+ const updatedResource3 = updatedResourceLocalStorage.getFirstById(resourcesDto[3].id); // this resource should have been removed
471
+ expect(updatedResource3).toBeUndefined();
472
+ });
473
+ });
374
474
  });
@@ -46,18 +46,9 @@ export default class FindResourcesService {
46
46
  * @returns {Promise<ResourcesCollection>}
47
47
  * @private
48
48
  */
49
- async findAll(contains, filters, ignoreInvalidEntity) {
50
- //Assert contains
51
- const supportedOptions = ResourceService.getSupportedContainOptions();
52
- const supportedFilter = ResourceService.getSupportedFiltersOptions();
53
-
54
- if (contains && !Object.keys(contains).every(option => supportedOptions.includes(option))) {
55
- throw new Error("Unsupported contains parameter used, please check supported contains");
56
- }
57
-
58
- if (filters && !Object.keys(filters).every(filter => supportedFilter.includes(filter))) {
59
- throw new Error("Unsupported filter parameter used, please check supported filters");
60
- }
49
+ async findAll(contains, filters, ignoreInvalidEntity = false) {
50
+ this.assertContains(contains);
51
+ this.assertFilters(filters);
61
52
 
62
53
  const resourcesDto = await this.resourceService.findAll(contains, filters);
63
54
  return new ResourcesCollection(resourcesDto, {clone: false, ignoreInvalidEntity: ignoreInvalidEntity});
@@ -69,7 +60,7 @@ export default class FindResourcesService {
69
60
  * @param {Array<string>} resourcesIds
70
61
  * @returns {Promise<ResourcesCollection>}
71
62
  */
72
- async findAllByIds(resourcesIds, contains = {}) {
63
+ async findAllByIds(resourcesIds, contains = {}, ignoreInvalidEntity = false) {
73
64
  assertArrayUUID(resourcesIds);
74
65
 
75
66
  // We split the requests in chunks in order to avoid any too long url error.
@@ -78,7 +69,7 @@ export default class FindResourcesService {
78
69
  const filter = {
79
70
  "has-id": resourceIds
80
71
  };
81
- return async() => await this.findAll(contains, filter);
72
+ return async() => await this.findAll(contains, filter, ignoreInvalidEntity);
82
73
  });
83
74
 
84
75
  // @todo Later (tm). The Collection should provide this capability, ensuring that validation build rules are executed and performance is guaranteed.
@@ -196,12 +187,8 @@ export default class FindResourcesService {
196
187
  */
197
188
  async findOneById(resourceId, contains = {}) {
198
189
  assertUuid(resourceId);
199
- //Assert contains
200
- const supportedOptions = ResourceService.getSupportedContainOptions();
190
+ this.assertContains(contains);
201
191
 
202
- if (contains && !Object.keys(contains).every(option => supportedOptions.includes(option))) {
203
- throw new Error("Unsupported contains parameter used, please check supported contains");
204
- }
205
192
  const resourcesDto = await this.resourceService.get(resourceId, contains);
206
193
 
207
194
  return new ResourceEntity(resourcesDto);
@@ -224,4 +211,51 @@ export default class FindResourcesService {
224
211
  const resource = this.findOneById(resourceId, contains);
225
212
  return resource;
226
213
  }
214
+
215
+ /**
216
+ * Find all the resources matching the given ids for the local storage.
217
+ * All entities that cannot be validated will be ignored and excluded from the resulting collection.
218
+ * @param {Array<uuid>} resourceIds
219
+ * @returns {Promise<ResourcesCollection>}
220
+ */
221
+ async findAllByIdsForLocalStorage(resourceIds) {
222
+ assertArrayUUID(resourceIds);
223
+ return await this.findAllByIds(resourceIds, ResourceLocalStorage.DEFAULT_CONTAIN, true);
224
+ }
225
+
226
+ /**
227
+ * Find all the resources having the given parentFolderId as direct ancestor for the local storage
228
+ * All entities that cannot be validated will be ignored and excluded from the resulting collection.
229
+ * @param {uuid} parentFolderId
230
+ * @returns {Promise<ResourcesCollection>}
231
+ */
232
+ async findAllByParentFolderIdForLocalStorage(parentFolderId) {
233
+ assertUuid(parentFolderId);
234
+ const filters = {"has-parent": parentFolderId};
235
+ return await this.findAll(ResourceLocalStorage.DEFAULT_CONTAIN, filters, true);
236
+ }
237
+
238
+ /**
239
+ * Assert the contains to ensure they match the supported ones.
240
+ * @param {object} contains
241
+ * @private
242
+ */
243
+ assertContains(contains) {
244
+ const supportedOptions = ResourceService.getSupportedContainOptions();
245
+ if (contains && !Object.keys(contains).every(option => supportedOptions.includes(option))) {
246
+ throw new Error("Unsupported contains parameter used, please check supported contains");
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Assert the filters to ensure they match the supported ones.
252
+ * @param {object} filters
253
+ * @private
254
+ */
255
+ assertFilters(filters) {
256
+ const supportedFilter = ResourceService.getSupportedFiltersOptions();
257
+ if (filters && !Object.keys(filters).every(filter => supportedFilter.includes(filter))) {
258
+ throw new Error("Unsupported filter parameter used, please check supported filters");
259
+ }
260
+ }
227
261
  }