passbolt-browser-extension 5.3.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 (84) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/Gruntfile.js +1 -1
  3. package/RELEASE_NOTES.md +29 -48
  4. package/package.json +5 -5
  5. package/src/all/background_page/controller/clipboard/cancelClipboardContentFlushController.js +51 -0
  6. package/src/all/background_page/controller/clipboard/cancelClipboardContentFlushController.test.js +46 -0
  7. package/src/all/background_page/controller/clipboard/copyTemporarilyToClipboardController.js +53 -0
  8. package/src/all/background_page/controller/clipboard/copyTemporarilyToClipboardController.test.js +47 -0
  9. package/src/all/background_page/controller/clipboard/{clipboardController.js → copyToClipboardController.js} +8 -8
  10. package/src/all/background_page/controller/clipboard/copyToClipboardController.test.js +47 -0
  11. package/src/all/background_page/controller/extension/getExtensionVersionController.test.js +1 -1
  12. package/src/all/background_page/controller/metadata/getOrFindMetadataKeysSettingsController.js +53 -0
  13. package/src/all/background_page/controller/metadata/getOrFindMetadataKeysSettingsController.test.js +106 -0
  14. package/src/all/background_page/controller/user/deleteDryRunUserController.js +73 -0
  15. package/src/all/background_page/controller/user/deleteDryRunUserController.test.js +129 -0
  16. package/src/all/background_page/controller/user/deleteUserController.js +76 -0
  17. package/src/all/background_page/controller/user/deleteUserController.test.js +141 -0
  18. package/src/all/background_page/event/actionLogEvents.js +5 -12
  19. package/src/all/background_page/event/appEvents.js +33 -0
  20. package/src/all/background_page/event/findAllForActionLogController.js +58 -0
  21. package/src/all/background_page/event/findAllForActionLogController.test.js +43 -0
  22. package/src/all/background_page/event/quickAccessEvents.js +32 -0
  23. package/src/all/background_page/event/userEvents.js +7 -20
  24. package/src/all/background_page/event/webIntegrationEvents.js +11 -0
  25. package/src/all/background_page/model/actionLog/{actionLogModel.js → findActionLogService.js} +25 -5
  26. package/src/all/background_page/model/actionLog/findActionLogService.test.js +61 -0
  27. package/src/all/background_page/model/comment/commentModel.js +5 -5
  28. package/src/all/background_page/model/entity/actionLog/actionLogsCollection.test.data.js +18 -0
  29. package/src/all/background_page/model/entity/actionLog/defaultActionLogEntity.test.data.js +34 -0
  30. package/src/all/background_page/model/import/resources/resourcesKdbxImportParser.test.js +0 -1
  31. package/src/all/background_page/model/user/userModel.js +0 -60
  32. package/src/all/background_page/pagemod/appPagemod.js +0 -2
  33. package/src/all/background_page/pagemod/appPagemod.test.js +2 -6
  34. package/src/all/background_page/service/alarm/globalAlarmService.js +2 -0
  35. package/src/all/background_page/service/api/actionLog/{actionLogService.js → actionLogApiService.js} +5 -4
  36. package/src/all/background_page/service/api/actionLog/actionLogApiService.test.js +55 -0
  37. package/src/all/background_page/service/api/comment/{commentService.js → commentApiService.js} +6 -7
  38. package/src/all/background_page/service/api/comment/commentApiService.test.js +122 -0
  39. package/src/all/background_page/service/auth/postLogoutService.js +2 -0
  40. package/src/all/background_page/service/auth/postLogoutService.test.js +4 -1
  41. package/src/all/background_page/service/browser/browserService.js +22 -0
  42. package/src/all/background_page/service/clipboard/clipboardProviderService.js +40 -0
  43. package/src/all/background_page/service/clipboard/clipboardProviderService.test.js +61 -0
  44. package/src/all/background_page/service/clipboard/copyToClipboardService.js +123 -0
  45. package/src/all/background_page/service/clipboard/copyToClipboardService.test.js +174 -0
  46. package/src/all/background_page/service/user/deleteUserService.js +97 -0
  47. package/src/all/background_page/service/user/deleteUserService.test.js +178 -0
  48. package/src/chrome/manifest.json +1 -1
  49. package/src/chrome/polyfill/clipboard/edgeBackgroundPageClipboardService.js +31 -0
  50. package/src/chrome/polyfill/clipboard/edgeBackgroundPageClipboardService.test.js +51 -0
  51. package/src/chrome-mv3/index.js +3 -3
  52. package/src/chrome-mv3/manifest.json +1 -1
  53. package/src/chrome-mv3/offscreens/{fetch.html → offscreen.html} +1 -1
  54. package/src/chrome-mv3/offscreens/{fetch.js → offscreen.js} +2 -2
  55. package/src/chrome-mv3/offscreens/service/clipboard/writeClipobardOffscreenService.js +54 -0
  56. package/src/chrome-mv3/offscreens/service/clipboard/writeClipobardOffscreenService.test.js +56 -0
  57. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.js +36 -44
  58. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.test.data.js +0 -1
  59. package/src/chrome-mv3/offscreens/service/network/fetchOffscreenService.test.js +90 -120
  60. package/src/chrome-mv3/offscreens/service/offscreen/handleOffscreenRequestService.js +85 -0
  61. package/src/chrome-mv3/offscreens/service/offscreen/handleOffscreenRequestService.test.js +99 -0
  62. package/src/chrome-mv3/polyfill/clipboardOffscreenPolyfill.js +19 -0
  63. package/src/chrome-mv3/serviceWorker/service/clipboard/requestClipboardOffscreenService.js +51 -0
  64. package/src/chrome-mv3/serviceWorker/service/clipboard/requestClipboardOffscreenService.test.js +70 -0
  65. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.js +25 -0
  66. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.test.data.js +21 -0
  67. package/src/chrome-mv3/serviceWorker/service/clipboard/responseClipboardOffscreenService.test.js +33 -0
  68. package/src/chrome-mv3/serviceWorker/service/network/requestFetchOffscreenService.js +25 -50
  69. package/src/chrome-mv3/serviceWorker/service/network/requestFetchOffscreenService.test.js +16 -39
  70. package/src/chrome-mv3/serviceWorker/service/network/responseFetchOffscreenService.js +14 -45
  71. package/src/chrome-mv3/serviceWorker/service/network/responseFetchOffscreenService.test.js +5 -37
  72. package/src/chrome-mv3/serviceWorker/service/offscreen/createOffscreenDocumentService.js +43 -0
  73. package/src/chrome-mv3/serviceWorker/service/offscreen/createOffscreenDocumentService.test.js +48 -0
  74. package/src/chrome-mv3/serviceWorker/service/offscreen/handleOffscreenResponseService.js +119 -0
  75. package/src/chrome-mv3/serviceWorker/service/offscreen/handleOffscreenResponseService.test.js +159 -0
  76. package/src/firefox/manifest.json +1 -1
  77. package/src/safari/manifest.json +1 -1
  78. package/test/jest.setup.js +4 -0
  79. package/test/mocks/mockNavigatorClipboard.js +40 -0
  80. package/test/mocks/mockWebExtensionPolyfill.js +2 -1
  81. package/{webpack-offscreens.fetch.config.js → webpack-offscreens.config.js} +1 -1
  82. package/webpack.service-worker.config.js +1 -0
  83. package/src/all/background_page/controller/clipboard/clipboardController.test.js +0 -68
  84. package/src/all/background_page/event/clipboardEvents.js +0 -28
@@ -12,12 +12,13 @@
12
12
  * @since 3.0.0
13
13
  */
14
14
  import CommentEntity from "../../../model/entity/comment/commentEntity";
15
+ import {assertNonEmptyString} from "../../../utils/assertions";
15
16
  import AbstractService from "../abstract/abstractService";
16
17
 
17
18
 
18
19
  const COMMENT_SERVICE_RESOURCE_NAME = 'comments';
19
20
 
20
- class CommentService extends AbstractService {
21
+ class CommentApiService extends AbstractService {
21
22
  /**
22
23
  * Constructor
23
24
  *
@@ -25,7 +26,7 @@ class CommentService extends AbstractService {
25
26
  * @public
26
27
  */
27
28
  constructor(apiClientOptions) {
28
- super(apiClientOptions, CommentService.RESOURCE_NAME);
29
+ super(apiClientOptions, CommentApiService.RESOURCE_NAME);
29
30
  }
30
31
 
31
32
  /**
@@ -61,7 +62,7 @@ class CommentService extends AbstractService {
61
62
  this.assertValidId(foreignId);
62
63
  this.assertValidForeignModel(foreignModel);
63
64
 
64
- contains = contains ? this.formatContainOptions(contains, CommentService.getSupportedContainOptions()) : null;
65
+ contains = contains ? this.formatContainOptions(contains, CommentApiService.getSupportedContainOptions()) : null;
65
66
  const urlOptions = {...contains};
66
67
 
67
68
  const url = this.apiClient.buildUrl(`${this.apiClient.baseUrl}/${foreignModel.toLowerCase()}/${foreignId}`, urlOptions || {});
@@ -122,13 +123,11 @@ class CommentService extends AbstractService {
122
123
  * @public
123
124
  */
124
125
  assertValidForeignModel(foreignModel) {
125
- if (!foreignModel || typeof foreignModel !== 'string') {
126
- throw new TypeError(`Comment foreign model should be a valid string.`);
127
- }
126
+ assertNonEmptyString(foreignModel, `Comment foreign model should be a valid string.`);
128
127
  if (!CommentEntity.ALLOWED_FOREIGN_MODELS.includes(foreignModel)) {
129
128
  throw new TypeError(`Comment foreign model ${foreignModel} in not in the list of supported models.`);
130
129
  }
131
130
  }
132
131
  }
133
132
 
134
- export default CommentService;
133
+ export default CommentApiService;
@@ -0,0 +1,122 @@
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.4.0
13
+ */
14
+
15
+ import {enableFetchMocks} from "jest-fetch-mock";
16
+ import {v4 as uuidv4} from "uuid";
17
+ import {mockApiResponse} from '../../../../../../test/mocks/mockApiResponse';
18
+ import CommentApiService from "./commentApiService";
19
+ import AccountEntity from "../../../model/entity/account/accountEntity";
20
+ import {defaultAccountDto} from "../../../model/entity/account/accountEntity.test.data";
21
+ import BuildApiClientOptionsService from "../../account/buildApiClientOptionsService";
22
+ import {defaultCommentCollectionDto} from "passbolt-styleguide/src/shared/models/entity/comment/commentEntityCollection.test.data";
23
+ import {defaultCommentDto} from "passbolt-styleguide/src/shared/models/entity/comment/commentEntity.test.data";
24
+ import PassboltServiceUnavailableError from "passbolt-styleguide/src/shared/lib/Error/PassboltServiceUnavailableError";
25
+ import CommentEntity from "../../../model/entity/comment/commentEntity";
26
+ import {mockApiResponseError} from "../../../../../../test/mocks/mockApiResponse";
27
+
28
+ describe.only("ActionLogService", () => {
29
+ let apiClientOptions, account;
30
+ beforeEach(async() => {
31
+ enableFetchMocks();
32
+ fetch.resetMocks();
33
+ account = new AccountEntity(defaultAccountDto());
34
+ apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
35
+ });
36
+
37
+ describe('::findAll', () => {
38
+ it("retrieves the comments from API", async() => {
39
+ expect.assertions(2);
40
+
41
+ const commentsCollection = [defaultCommentCollectionDto()];
42
+ fetch.doMockOnceIf(/comments\/resource/, () => mockApiResponse(commentsCollection));
43
+
44
+ const service = new CommentApiService(apiClientOptions, account);
45
+ const resultDto = await service.findAll('Resource', uuidv4(), {creator: true});
46
+
47
+ expect(resultDto).toBeInstanceOf(Array);
48
+ expect(resultDto).toHaveLength(commentsCollection.length);
49
+ });
50
+
51
+ it("throws API error if the API encountered an issue", async() => {
52
+ expect.assertions(1);
53
+ fetch.doMockOnceIf(/comments\/resource/, () => mockApiResponseError(500, "Something wrong happened!"));
54
+
55
+ const service = new CommentApiService(apiClientOptions);
56
+
57
+ await expect(() => service.findAll('Resource')).rejects.toThrow(TypeError);
58
+ });
59
+
60
+ it("throws service unavailable error if an error occurred but not from the API (by instance cloudflare)", async() => {
61
+ expect.assertions(1);
62
+ fetch.doMockOnceIf(/comments\/resource/, () => { throw new Error("Service unavailable"); });
63
+
64
+ const service = new CommentApiService(apiClientOptions);
65
+
66
+ await expect(() => service.findAll('Resource', uuidv4(), {creator: true})).rejects.toThrow(PassboltServiceUnavailableError);
67
+ });
68
+ });
69
+
70
+ describe('::create', () => {
71
+ it("Create a comment", async() => {
72
+ expect.assertions(2);
73
+
74
+ const commentDto = defaultCommentDto({}, {withCreator: false, withModifier: false});
75
+
76
+ const payload = new CommentEntity(commentDto);
77
+ let reqPayload;
78
+ fetch.doMockOnceIf(/comments\/resource/, async req => {
79
+ expect(req.method).toEqual("POST");
80
+ reqPayload = await req.json();
81
+ return mockApiResponse(defaultCommentDto({...reqPayload, id: commentDto.id}));
82
+ });
83
+
84
+ const service = new CommentApiService(apiClientOptions);
85
+ const resultDto = await service.create(payload._props);
86
+
87
+ expect(resultDto).toEqual(expect.objectContaining(commentDto));
88
+ });
89
+
90
+ it("throws an error if input is invalid ", async() => {
91
+ expect.assertions(1);
92
+
93
+ const service = new CommentApiService(apiClientOptions);
94
+
95
+ await expect(() => service.create(42, {withCreator: false, withModifier: false})).rejects.toThrow(TypeError);
96
+ });
97
+ });
98
+
99
+
100
+ describe('::delete', () => {
101
+ it("Delete a comment", async() => {
102
+ expect.assertions(1);
103
+
104
+ const deleteCommentId = uuidv4();
105
+ fetch.doMockOnceIf(new RegExp(`/comments\/${deleteCommentId}\.json`), async req => {
106
+ expect(req.method).toEqual("DELETE");
107
+ return mockApiResponse({});
108
+ });
109
+
110
+ const service = new CommentApiService(apiClientOptions);
111
+ await service.delete(deleteCommentId);
112
+ });
113
+
114
+ it("throws an invalid parameter error if the id parameter is not valid", async() => {
115
+ expect.assertions(1);
116
+
117
+ const service = new CommentApiService(apiClientOptions);
118
+
119
+ await expect(() => service.delete(42)).rejects.toThrow(Error);
120
+ });
121
+ });
122
+ });
@@ -19,6 +19,7 @@ import resourceInProgressCacheService from "../cache/resourceInProgressCache.ser
19
19
  import OnExtensionUpdateAvailableService from "../extension/onExtensionUpdateAvailableService";
20
20
  import InformCallToActionPagemod from "../../pagemod/informCallToActionPagemod";
21
21
  import WorkerService from "../worker/workerService";
22
+ import CopyToClipboardService from "../clipboard/copyToClipboardService";
22
23
  class PostLogoutService {
23
24
  /**
24
25
  * Execute all processes after a logout
@@ -27,6 +28,7 @@ class PostLogoutService {
27
28
  static async exec() {
28
29
  await PostLogoutService.sendLogoutEventForWorkers();
29
30
  await LocalStorageService.flush();
31
+ await (new CopyToClipboardService()).flushTemporaryContentIfAny();
30
32
  await StartLoopAuthSessionCheckService.clearAlarm();
31
33
  toolbarService.handleUserLoggedOut();
32
34
  resourceInProgressCacheService.reset();
@@ -26,6 +26,7 @@ import StartLoopAuthSessionCheckService from "./startLoopAuthSessionCheckService
26
26
  import resourceInProgressCacheService from "../cache/resourceInProgressCache.service";
27
27
  import OnExtensionUpdateAvailableService from "../extension/onExtensionUpdateAvailableService";
28
28
  import toolbarService from "../toolbar/toolbarService";
29
+ import CopyToClipboardService from "../clipboard/copyToClipboardService";
29
30
 
30
31
  describe("PostLogoutService", () => {
31
32
  beforeEach(() => {
@@ -84,11 +85,12 @@ describe("PostLogoutService", () => {
84
85
  });
85
86
 
86
87
  it("Should call all services that needs to run processes on logout", async() => {
87
- expect.assertions(5);
88
+ expect.assertions(6);
88
89
  jest.spyOn(PortManager, "isPortExist").mockImplementation(() => false);
89
90
  jest.spyOn(LocalStorageService, "flush");
90
91
  jest.spyOn(toolbarService, "handleUserLoggedOut");
91
92
  jest.spyOn(StartLoopAuthSessionCheckService, "clearAlarm");
93
+ jest.spyOn(CopyToClipboardService.prototype, "flushTemporaryContentIfAny");
92
94
  jest.spyOn(resourceInProgressCacheService, "reset");
93
95
  jest.spyOn(OnExtensionUpdateAvailableService, "handleUserLoggedOut");
94
96
 
@@ -99,6 +101,7 @@ describe("PostLogoutService", () => {
99
101
  expect(StartLoopAuthSessionCheckService.clearAlarm).toHaveBeenCalledTimes(1);
100
102
  expect(resourceInProgressCacheService.reset).toHaveBeenCalledTimes(1);
101
103
  expect(OnExtensionUpdateAvailableService.handleUserLoggedOut).toHaveBeenCalledTimes(1);
104
+ expect(CopyToClipboardService.prototype.flushTemporaryContentIfAny).toHaveBeenCalledTimes(1);
102
105
  });
103
106
  });
104
107
  });
@@ -0,0 +1,22 @@
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
+
15
+ /**
16
+ * The service aims to help get browser information.
17
+ */
18
+ export default class BrowserService {
19
+ static isFirefox() {
20
+ return browser.runtime.getURL("/").startsWith("moz-extension://");
21
+ }
22
+ }
@@ -0,0 +1,40 @@
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 EdgeBackgroundPageClipboardService from "../../../../chrome/polyfill/clipboard/edgeBackgroundPageClipboardService";
15
+ import BrowserService from "../browser/browserService";
16
+
17
+ /**
18
+ * The service retrieves the appropriate clipboard provider for the current environment: Edge, Chrome, or Firefox.
19
+ */
20
+ export default class ClipboardProviderService {
21
+ /**
22
+ * Returns a clipboard API that works for the currently used browser.
23
+ * @returns {Clipboard}
24
+ */
25
+ static getClipboard() {
26
+ //is Chrome with MV3?
27
+ if (typeof customNavigatorClipboard !== "undefined") {
28
+ // eslint-disable-next-line no-undef
29
+ return customNavigatorClipboard;
30
+ }
31
+
32
+ const isMV2 = chrome.runtime.getManifest().manifest_version === 2;
33
+ if (!BrowserService.isFirefox() && isMV2) {
34
+ //it's not firefox and it's MV2 => it's most probably Edge then
35
+ return EdgeBackgroundPageClipboardService;
36
+ }
37
+ //by default we provide the default clipboard
38
+ return navigator.clipboard;
39
+ }
40
+ }
@@ -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
+ });