diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79ef6460b5..b091820ab3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,4 @@ +--- name: Build on: @@ -10,6 +11,7 @@ on: jobs: cloc: + name: CLOC runs-on: ubuntu-latest steps: - name: Checkout repo @@ -24,6 +26,7 @@ jobs: setup: + name: Setup runs-on: ubuntu-latest outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} @@ -38,10 +41,11 @@ jobs: repo_url=https://github.com/$GITHUB_REPOSITORY.git adj_build_num=${GITHUB_SHA:0:7} - echo "::set-output name=repo_url::$repo_url" - echo "::set-output name=adj_build_number::$adj_build_num" + echo "::set-output name=repo_url::$repo_url" + echo "::set-output name=adj_build_number::$adj_build_num" locales-test: + name: Locales Test runs-on: ubuntu-latest needs: setup steps: @@ -63,21 +67,21 @@ jobs: found_error=true fi done - + if $found_error; then echo echo "Please fix 'extName' for the locales listed above." exit 1 else echo "Test passed!" - fi + fi build: + name: Build runs-on: windows-latest - needs: [ setup, locales-test ] + needs: [setup, locales-test] env: - REPO_URL: ${{ needs.setup.outputs.repo_url }} - BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} + _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} steps: - name: Checkout repo uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -96,41 +100,41 @@ jobs: node --version npm --version - - name: npm setup & test + - name: NPM setup & test run: | npm install npm run dist npm run test - - name: gulp + - name: Gulp run: gulp ci - - name: Upload opera artifact + - name: Upload Opera artifact uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 with: - name: dist-opera-${{ env.BUILD_NUMBER }}.zip - path: dist/dist-opera-${{ env.BUILD_NUMBER }}.zip + name: dist-opera-${{ env._BUILD_NUMBER }}.zip + path: dist/dist-opera-${{ env._BUILD_NUMBER }}.zip - - name: Upload chrome artifact + - name: Upload Chrome artifact uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 with: - name: dist-chrome-${{ env.BUILD_NUMBER }}.zip - path: dist/dist-chrome-${{ env.BUILD_NUMBER }}.zip + name: dist-chrome-${{ env._BUILD_NUMBER }}.zip + path: dist/dist-chrome-${{ env._BUILD_NUMBER }}.zip - - name: Upload firefox artifact + - name: Upload Firefox artifact uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 with: - name: dist-firefox-${{ env.BUILD_NUMBER }}.zip - path: dist/dist-firefox-${{ env.BUILD_NUMBER }}.zip + name: dist-firefox-${{ env._BUILD_NUMBER }}.zip + path: dist/dist-firefox-${{ env._BUILD_NUMBER }}.zip - - name: Upload edge artifact + - name: Upload Edge artifact uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 with: - name: dist-edge-${{ env.BUILD_NUMBER }}.zip - path: dist/dist-edge-${{ env.BUILD_NUMBER }}.zip + name: dist-edge-${{ env._BUILD_NUMBER }}.zip + path: dist/dist-edge-${{ env._BUILD_NUMBER }}.zip - name: Upload coverage artifact uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 with: - name: coverage-${{ env.BUILD_NUMBER }}.zip - path: coverage/coverage-${{ env.BUILD_NUMBER }}.zip + name: coverage-${{ env._BUILD_NUMBER }}.zip + path: coverage/coverage-${{ env._BUILD_NUMBER }}.zip diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml index 1adcb6144b..1f99c27e73 100644 --- a/.github/workflows/crowdin-sync.yml +++ b/.github/workflows/crowdin-sync.yml @@ -1,10 +1,11 @@ +--- name: Crowdin Sync on: workflow_dispatch: inputs: {} - #schedule: - # - cron: '0 0 * * *' +# schedule: +# - cron: '0 0 * * *' jobs: crowdin-sync: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b98307bf0..bcce70efb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,4 @@ +--- name: Release on: @@ -10,6 +11,7 @@ on: jobs: setup: + name: Setup runs-on: ubuntu-latest outputs: tag_version: ${{ steps.create_tags.outputs.tag_version }} @@ -25,7 +27,7 @@ jobs: echo "===================================" exit 1 fi - + - name: Checkout repo uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -37,8 +39,8 @@ jobs: $build_num = [int]$env:GITHUB_RUN_NUMBER $adj_build_num = $build_num + 3000 - echo "::set-output name=repo_url::$repo_url" - echo "::set-output name=adj_build_number::$adj_build_num" + echo "::set-output name=repo_url::$repo_url" + echo "::set-output name=adj_build_number::$adj_build_num" - name: Create Release Vars id: create_tags @@ -74,6 +76,7 @@ jobs: locales-test: + name: Locales Test runs-on: ubuntu-latest needs: setup steps: @@ -105,11 +108,12 @@ jobs: fi build: + name: Build runs-on: windows-latest - needs: [ setup, locales-test ] + needs: [setup, locales-test] env: - REPO_URL: ${{ needs.setup.outputs.repo_url }} - BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} + _REPO_URL: ${{ needs.setup.outputs.repo_url }} + _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} steps: - name: Checkout repo uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -128,84 +132,84 @@ jobs: node --version npm --version - - name: npm setup & test + - name: NPM setup & test run: | npm install npm run dist npm run test - - name: gulp + - name: Gulp run: gulp ci - name: Build sources for reviewers shell: cmd run: | mkdir dist\Source - call git clone %REPO_URL% dist\Source + call git clone %_REPO_URL% dist\Source cd dist\Source call git checkout %GITHUB_SHA% call git submodule update --init --recursive cd ../ del /S/Q "Source\.git\objects\pack\*" - call 7z a browser-source-%BUILD_NUMBER%.zip "Source\*" + call 7z a browser-source-%_BUILD_NUMBER%.zip "Source\*" - - name: upload opera release asset + - name: Upload Opera release asset uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: dist-opera-${{ env.BUILD_NUMBER }}.zip - asset_path: dist/dist-opera-${{ env.BUILD_NUMBER }}.zip + asset_name: dist-opera-${{ env._BUILD_NUMBER }}.zip + asset_path: dist/dist-opera-${{ env._BUILD_NUMBER }}.zip asset_content_type: application - - name: upload chrome release asset + - name: Upload Chrome release asset uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: dist-chrome-${{ env.BUILD_NUMBER }}.zip - asset_path: dist/dist-chrome-${{ env.BUILD_NUMBER }}.zip + asset_name: dist-chrome-${{ env._BUILD_NUMBER }}.zip + asset_path: dist/dist-chrome-${{ env._BUILD_NUMBER }}.zip asset_content_type: application/zip - - name: upload firefox release asset + - name: Upload Firefox release asset uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: dist-firefox-${{ env.BUILD_NUMBER }}.zip - asset_path: dist/dist-firefox-${{ env.BUILD_NUMBER }}.zip + asset_name: dist-firefox-${{ env._BUILD_NUMBER }}.zip + asset_path: dist/dist-firefox-${{ env._BUILD_NUMBER }}.zip asset_content_type: application/zip - - name: upload edge release asset + - name: Upload Edge release asset uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: dist-edge-${{ env.BUILD_NUMBER }}.zip - asset_path: dist/dist-edge-${{ env.BUILD_NUMBER }}.zip + asset_name: dist-edge-${{ env._BUILD_NUMBER }}.zip + asset_path: dist/dist-edge-${{ env._BUILD_NUMBER }}.zip asset_content_type: application/zip - - name: upload browser source zip release asset + - name: Upload browser source zip release asset uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: browser-source-${{ env.BUILD_NUMBER }}.zip - asset_path: dist/browser-source-${{ env.BUILD_NUMBER }}.zip + asset_name: browser-source-${{ env._BUILD_NUMBER }}.zip + asset_path: dist/browser-source-${{ env._BUILD_NUMBER }}.zip asset_content_type: application/zip - - name: upload coverage release asset + - name: Upload coverage release asset if: false uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.setup.outputs.release_upload_url }} - asset_name: coverage-${{ env.BUILD_NUMBER }}.zip - asset_path: coverage/coverage-${{ env.BUILD_NUMBER }}.zip + asset_name: coverage-${{ env._BUILD_NUMBER }}.zip + asset_path: coverage/coverage-${{ env._BUILD_NUMBER }}.zip asset_content_type: application/zip diff --git a/jslib b/jslib index 1f0127966e..30419a625f 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 1f0127966e85aa29f9e50144de9b2a03b00de5d4 +Subproject commit 30419a625fbdeefee1b0d0c24ee1d26064b17ba2 diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 643d21d913..7dfa637610 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -88,6 +88,9 @@ "generatePasswordCopied": { "message": "Generate Password (copied)" }, + "copyElementIdentifier": { + "message": "Copy Custom Field Name" + }, "noMatchingLogins": { "message": "No matching logins." }, @@ -1761,5 +1764,8 @@ }, "updateMasterPasswordWarning": { "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + }, + "selectFolder": { + "message": "Select folder..." } } diff --git a/src/background/contextMenus.background.ts b/src/background/contextMenus.background.ts index 19ee4421c2..f690d28320 100644 --- a/src/background/contextMenus.background.ts +++ b/src/background/contextMenus.background.ts @@ -29,6 +29,8 @@ export default class ContextMenusBackground { this.contextMenus.onClicked.addListener(async (info: any, tab: any) => { if (info.menuItemId === 'generate-password') { await this.generatePasswordToClipboard(); + } else if (info.menuItemId === 'copy-identifier') { + await this.getClickedElement(); } else if (info.parentMenuItemId === 'autofill' || info.parentMenuItemId === 'copy-username' || info.parentMenuItemId === 'copy-password' || @@ -45,6 +47,15 @@ export default class ContextMenusBackground { this.passwordGenerationService.addHistory(password); } + private async getClickedElement() { + const tab = await BrowserApi.getTabFromCurrentWindow(); + if (tab == null) { + return; + } + + BrowserApi.tabSendMessageData(tab, 'getClickedElement'); + } + private async cipherAction(info: any) { const id = info.menuItemId.split('_')[1]; if (id === 'noop') { diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 2e7fa19fac..f987591ca7 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -244,7 +244,7 @@ export default class MainBackground { this.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.cipherService, this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, this.notificationsService, this.systemService, this.vaultTimeoutService, - this.environmentService, this.policyService, this.userService, this.messagingService); + this.environmentService, this.policyService, this.userService, this.messagingService, this.folderService); this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService, this.platformUtilsService); @@ -512,6 +512,14 @@ export default class MainBackground { title: this.i18nService.t('generatePasswordCopied'), }); + await this.contextMenusCreate({ + type: 'normal', + id: 'copy-identifier', + parentId: 'root', + contexts: ['all'], + title: this.i18nService.t('copyElementIdentifier'), + }); + this.buildingContextMenu = false; } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index e51e5c8161..583cc29f77 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -6,6 +6,7 @@ import { LoginView } from 'jslib-common/models/view/loginView'; import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; +import { FolderService } from 'jslib-common/abstractions/folder.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; @@ -39,7 +40,8 @@ export default class RuntimeBackground { private notificationsService: NotificationsService, private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService, private environmentService: EnvironmentService, private policyService: PolicyService, - private userService: UserService, private messagingService: MessagingService) { + private userService: UserService, private messagingService: MessagingService, + private folderService: FolderService) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -107,7 +109,7 @@ export default class RuntimeBackground { this.removeTabFromNotificationQueue(sender.tab); break; case 'bgAddSave': - await this.saveAddLogin(sender.tab); + await this.saveAddLogin(sender.tab, msg.folder); break; case 'bgChangeSave': await this.saveChangePassword(sender.tab); @@ -195,6 +197,8 @@ export default class RuntimeBackground { type: 'info', }); break; + case 'getClickedElementResponse': + this.platformUtilsService.copyToClipboard(msg.identifier, { window: window }); default: break; } @@ -216,7 +220,7 @@ export default class RuntimeBackground { this.pageDetailsToAutoFill = []; } - private async saveAddLogin(tab: any) { + private async saveAddLogin(tab: any, folderId: string) { if (await this.vaultTimeoutService.isLocked()) { return; } @@ -247,6 +251,13 @@ export default class RuntimeBackground { model.type = CipherType.Login; model.login = loginModel; + if (!Utils.isNullOrWhitespace(folderId)) { + const folders = await this.folderService.getAllDecrypted(); + if (folders.some(x => x.id === folderId)) { + model.folderId = folderId; + } + } + const cipher = await this.cipherService.encrypt(model); await this.cipherService.saveWithServer(cipher); } @@ -450,24 +461,14 @@ export default class RuntimeBackground { notificationChangeSave: this.i18nService.t('notificationChangeSave'), notificationChangeDesc: this.i18nService.t('notificationChangeDesc'), }; + } else if (responseCommand === 'notificationBarGetFoldersList') { + responseData.folders = await this.folderService.getAllDecrypted(); } await BrowserApi.tabSendMessageData(tab, responseCommand, responseData); } private async allowPersonalOwnership(): Promise { - const personalOwnershipPolicies = await this.policyService.getAll(PolicyType.PersonalOwnership); - if (personalOwnershipPolicies != null) { - for (const policy of personalOwnershipPolicies) { - if (policy.enabled) { - const org = await this.userService.getOrganization(policy.organizationId); - if (org != null && org.enabled && org.usePolicies && !org.canManagePolicies - && org.status === OrganizationUserStatusType.Confirmed) { - return false; - } - } - } - } - return true; + return !await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership); } } diff --git a/src/content/contextMenuHandler.ts b/src/content/contextMenuHandler.ts new file mode 100644 index 0000000000..6aae8ca168 --- /dev/null +++ b/src/content/contextMenuHandler.ts @@ -0,0 +1,45 @@ +const inputTags = ['input', 'textarea', 'select']; +const attributes = ['id', 'name', 'label-aria', 'placeholder']; +let clickedEl: HTMLElement = null; + +// Find the best attribute to be used as the Name for an element in a custom field. +function getClickedElementIdentifier() { + if (clickedEl == null) { + return 'Unable to identify clicked element.' + } + + if (!inputTags.includes(clickedEl.nodeName.toLowerCase())) { + return 'Invalid element type.'; + } + + for (const attr of attributes) { + const attributeValue = clickedEl.getAttribute(attr); + const selector = '[' + attr + '="' + attributeValue + '"]'; + if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) { + return attributeValue; + } + } + return 'No unique identifier found.'; +} + +function isNullOrEmpty(s: string) { + return s == null || s === ''; +} + +// We only have access to the element that's been clicked when the context menu is first opened. +// Remember it for use later. +document.addEventListener('contextmenu', event => { + clickedEl = event.target as HTMLElement; +}); + +// Runs when the 'Copy Custom Field Name' context menu item is actually clicked. +chrome.runtime.onMessage.addListener(event => { + if (event.command === 'getClickedElement') { + const identifier = getClickedElementIdentifier(); + chrome.runtime.sendMessage({ + command: 'getClickedElementResponse', + sender: 'contextMenuHandler', + identifier: identifier, + }); + } +}); diff --git a/src/manifest.json b/src/manifest.json index 74146f72dc..cb79e00627 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -20,7 +20,8 @@ "js": [ "content/autofill.js", "content/autofiller.js", - "content/notificationBar.js" + "content/notificationBar.js", + "content/contextMenuHandler.js" ], "matches": [ "http://*/*", diff --git a/src/notification/bar.html b/src/notification/bar.html index 065ca944bb..0403ad1b82 100644 --- a/src/notification/bar.html +++ b/src/notification/bar.html @@ -27,8 +27,11 @@ - + + + + diff --git a/src/notification/bar.js b/src/notification/bar.js index a5e55ca664..7bcd2b2eef 100644 --- a/src/notification/bar.js +++ b/src/notification/bar.js @@ -17,7 +17,6 @@ document.addEventListener('DOMContentLoaded', () => { // delay 50ms so that we get proper body dimensions setTimeout(load, 50); - function load() { var closeButton = document.getElementById('close-button'), @@ -34,10 +33,12 @@ document.addEventListener('DOMContentLoaded', () => { if (bodyRect.width < 768) { document.querySelector('#template-add .add-save').textContent = i18n.yes; document.querySelector('#template-add .never-save').textContent = i18n.never; + document.querySelector('#template-add .select-folder').style.display = 'none'; document.querySelector('#template-change .change-save').textContent = i18n.yes; } else { document.querySelector('#template-add .add-save').textContent = i18n.notificationAddSave; document.querySelector('#template-add .never-save').textContent = i18n.notificationNeverSave; + document.querySelector('#template-add .select-folder').style.display = 'initial'; document.querySelector('#template-change .change-save').textContent = i18n.notificationChangeSave; } @@ -53,7 +54,8 @@ document.addEventListener('DOMContentLoaded', () => { addButton.addEventListener('click', (e) => { e.preventDefault(); sendPlatformMessage({ - command: 'bgAddSave' + command: 'bgAddSave', + folder: document.getElementById("select-folder").value, }); }); @@ -63,6 +65,17 @@ document.addEventListener('DOMContentLoaded', () => { command: 'bgNeverSave' }); }); + + const responseFoldersCommand = 'notificationBarGetFoldersList'; + chrome.runtime.onMessage.addListener((msg) => { + if (msg.command === responseFoldersCommand && msg.data) { + fillSelectorWithFolders(msg.data.folders); + } + }); + sendPlatformMessage({ + command: 'bgGetDataForTab', + responseCommand: responseFoldersCommand + }); } else if (getQueryVariable('change')) { setContent(document.getElementById('template-change')); var changeButton = document.querySelector('#template-change-clone .change-save'); @@ -120,4 +133,13 @@ document.addEventListener('DOMContentLoaded', () => { function sendPlatformMessage(msg) { chrome.runtime.sendMessage(msg); } + + function fillSelectorWithFolders(folders) { + const select = document.querySelector('#template-add-clone .select-folder'); + select.appendChild(new Option(chrome.i18n.getMessage('selectFolder'), null, true)); + folders.forEach((folder) => { + //Select "No Folder" (id=null) folder by default + select.appendChild(new Option(folder.name, folder.id || '', false)); + }); + } }); diff --git a/src/notification/bar.scss b/src/notification/bar.scss index 1f673d5255..2705ed324b 100644 --- a/src/notification/bar.scss +++ b/src/notification/bar.scss @@ -82,7 +82,7 @@ button.link { } body[class*='lang-en'] .add-buttons { - width: 175px; + width: 50px; } @media (min-width: 768px) { @@ -96,3 +96,4 @@ body[class*='lang-en'] .add-buttons { display: none; } } + diff --git a/src/popup/accounts/environment.component.html b/src/popup/accounts/environment.component.html index 5654fdc367..dbacca585a 100644 --- a/src/popup/accounts/environment.component.html +++ b/src/popup/accounts/environment.component.html @@ -1,4 +1,4 @@ -