mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
Merge remote-tracking branch 'origin/master' into feature/flexible-collections
This commit is contained in:
commit
07b4204772
2
.github/workflows/brew-bump-cli.yml
vendored
2
.github/workflows/brew-bump-cli.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "brew-bump-workflow-pat"
|
||||
|
2
.github/workflows/brew-bump-desktop.yml
vendored
2
.github/workflows/brew-bump-desktop.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "brew-bump-workflow-pat"
|
||||
|
10
.github/workflows/build-browser.yml
vendored
10
.github/workflows/build-browser.yml
vendored
@ -149,7 +149,11 @@ jobs:
|
||||
FILES=$(find . -maxdepth 1 -type f)
|
||||
for FILE in $FILES; do cp "$FILE" browser-source/; done
|
||||
|
||||
# Copy apps/browser to Browser source directory
|
||||
# Copy patches to the Browser source directory
|
||||
mkdir -p browser-source/patches
|
||||
cp -r patches/* browser-source/patches
|
||||
|
||||
# Copy apps/browser to the Browser source directory
|
||||
mkdir -p browser-source/apps/browser
|
||||
cp -r apps/browser/* browser-source/apps/browser
|
||||
|
||||
@ -357,7 +361,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
@ -419,7 +423,7 @@ jobs:
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: failure()
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
2
.github/workflows/build-cli.yml
vendored
2
.github/workflows/build-cli.yml
vendored
@ -404,7 +404,7 @@ jobs:
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: failure()
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
8
.github/workflows/build-desktop.yml
vendored
8
.github/workflows/build-desktop.yml
vendored
@ -277,7 +277,7 @@ jobs:
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Install AST
|
||||
uses: bitwarden/gh-actions/install-ast@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/install-ast@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
|
||||
- name: Set up environmentF
|
||||
run: choco install checksum --no-progress
|
||||
@ -302,7 +302,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "code-signing-vault-url,
|
||||
@ -1190,7 +1190,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
@ -1269,7 +1269,7 @@ jobs:
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: failure()
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
6
.github/workflows/build-web.yml
vendored
6
.github/workflows/build-web.yml
vendored
@ -188,7 +188,7 @@ jobs:
|
||||
|
||||
- name: Retrieve github PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
@ -264,7 +264,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
@ -325,7 +325,7 @@ jobs:
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: failure()
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
4
.github/workflows/crowdin-pull.yml
vendored
4
.github/workflows/crowdin-pull.yml
vendored
@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: bitwarden/gh-actions/crowdin@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/crowdin@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
4
.github/workflows/deploy-eu-prod-web.yml
vendored
4
.github/workflows/deploy-eu-prod-web.yml
vendored
@ -24,13 +24,13 @@ jobs:
|
||||
|
||||
- name: Retrieve Storage Account connection string
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: webvault-westeurope-prod
|
||||
secrets: "sa-bitwarden-web-vault-dev-key-temp"
|
||||
|
||||
- name: Download latest cloud asset
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: apps/web
|
||||
|
4
.github/workflows/deploy-eu-qa-web.yml
vendored
4
.github/workflows/deploy-eu-qa-web.yml
vendored
@ -24,13 +24,13 @@ jobs:
|
||||
|
||||
- name: Retrieve Storage Account connection string
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: webvaulteu-westeurope-qa
|
||||
secrets: "sa-bitwarden-web-vault-dev-key-temp"
|
||||
|
||||
- name: Download latest cloud asset
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: apps/web
|
||||
|
2
.github/workflows/deploy-non-prod-web.yml
vendored
2
.github/workflows/deploy-non-prod-web.yml
vendored
@ -67,7 +67,7 @@ jobs:
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Download latest cloud asset
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: apps/web
|
||||
|
6
.github/workflows/release-browser.yml
vendored
6
.github/workflows/release-browser.yml
vendored
@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@58a2fdfbd3f1fc7e6727bc5dc51d159f4df07072
|
||||
uses: bitwarden/gh-actions/release-version-check@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Download latest Release build artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-browser.yml
|
||||
workflow_conclusion: success
|
||||
@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download latest master build artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-browser.yml
|
||||
workflow_conclusion: success
|
||||
|
24
.github/workflows/release-cli.yml
vendored
24
.github/workflows/release-cli.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/release-version-check@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Download all Release artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli
|
||||
@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli
|
||||
@ -150,7 +150,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "snapcraft-store-token"
|
||||
@ -162,7 +162,7 @@ jobs:
|
||||
|
||||
- name: Download artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli
|
||||
@ -172,7 +172,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli
|
||||
@ -206,7 +206,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "cli-choco-api-key"
|
||||
@ -222,7 +222,7 @@ jobs:
|
||||
|
||||
- name: Download artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli/dist
|
||||
@ -232,7 +232,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli/dist
|
||||
@ -265,14 +265,14 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "npm-api-key"
|
||||
|
||||
- name: Download artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli/build
|
||||
@ -282,7 +282,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-cli.yml
|
||||
path: apps/cli/build
|
||||
|
8
.github/workflows/release-desktop-beta.yml
vendored
8
.github/workflows/release-desktop-beta.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/release-version-check@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
release-type: 'Initial Release'
|
||||
project-type: ts
|
||||
@ -231,7 +231,7 @@ jobs:
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Install AST
|
||||
uses: bitwarden/gh-actions/install-ast@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/install-ast@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
|
||||
- name: Set up environment
|
||||
run: choco install checksum --no-progress
|
||||
@ -249,7 +249,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "code-signing-vault-url,
|
||||
@ -932,7 +932,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "aws-electron-access-id,
|
||||
|
58
.github/workflows/release-desktop.yml
vendored
58
.github/workflows/release-desktop.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Branch check
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then
|
||||
echo "==================================="
|
||||
@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/release-version-check@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
release-type: ${{ inputs.release_type }}
|
||||
project-type: ts
|
||||
@ -93,7 +93,7 @@ jobs:
|
||||
esac
|
||||
|
||||
- name: Create GitHub deployment
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
id: deployment
|
||||
with:
|
||||
@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "aws-electron-access-id,
|
||||
@ -122,8 +122,8 @@ jobs:
|
||||
cf-prod-account"
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -131,8 +131,8 @@ jobs:
|
||||
path: apps/desktop/artifacts
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -146,7 +146,7 @@ jobs:
|
||||
run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive
|
||||
|
||||
- name: Set staged rollout percentage
|
||||
if: ${{ inputs.electron_publish == 'true' }}
|
||||
if: ${{ github.event.inputs.electron_publish == 'true' }}
|
||||
env:
|
||||
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}
|
||||
ROLLOUT_PCT: ${{ inputs.rollout_percentage }}
|
||||
@ -156,7 +156,7 @@ jobs:
|
||||
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml
|
||||
|
||||
- name: Publish artifacts to S3
|
||||
if: ${{ inputs.release_type != 'Dry Run' && inputs.electron_publish == 'true' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }}
|
||||
@ -170,7 +170,7 @@ jobs:
|
||||
--quiet
|
||||
|
||||
- name: Publish artifacts to R2
|
||||
if: ${{ inputs.release_type != 'Dry Run' && inputs.electron_publish == 'true' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }}
|
||||
@ -185,14 +185,14 @@ jobs:
|
||||
--endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com
|
||||
|
||||
- name: Get checksum files
|
||||
uses: bitwarden/gh-actions/get-checksum@82cfceb235b308c2eb63923824e61d8350d280db
|
||||
uses: bitwarden/gh-actions/get-checksum@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
packages_dir: "apps/desktop/artifacts"
|
||||
file_path: "apps/desktop/artifacts/sha256-checksums.txt"
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@a2e71bdd4e7dab70ca26a852f29600c98b33153e # v1.12.0
|
||||
if: ${{ steps.release-channel.outputs.channel == 'latest' && inputs.release_type != 'Dry Run' && inputs.github_release == 'true' }}
|
||||
if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' && github.event.inputs.github_release == 'true' }}
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.version.outputs.version }}
|
||||
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}
|
||||
@ -230,7 +230,7 @@ jobs:
|
||||
draft: true
|
||||
|
||||
- name: Update deployment status to Success
|
||||
if: ${{ inputs.release_type != 'Dry Run' && success() }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
@ -238,7 +238,7 @@ jobs:
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ inputs.release_type != 'Dry Run' && failure() }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
@ -249,7 +249,7 @@ jobs:
|
||||
name: Deploy Snap
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
if: ${{ inputs.snap_publish == 'true' }}
|
||||
if: ${{ github.event.inputs.snap_publish == 'true' }}
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
steps:
|
||||
@ -263,7 +263,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "snapcraft-store-token"
|
||||
@ -278,8 +278,8 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Download Snap artifact
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -288,8 +288,8 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Dry Run - Download Snap artifact
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -298,7 +298,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Deploy to Snap Store
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }}
|
||||
run: |
|
||||
@ -310,7 +310,7 @@ jobs:
|
||||
name: Deploy Choco
|
||||
runs-on: windows-2019
|
||||
needs: setup
|
||||
if: ${{ inputs.choco_publish == 'true' }}
|
||||
if: ${{ github.event.inputs.choco_publish == 'true' }}
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
steps:
|
||||
@ -329,7 +329,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "cli-choco-api-key"
|
||||
@ -346,8 +346,8 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Download choco artifact
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -356,8 +356,8 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Dry Run - Download choco artifact
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
workflow_conclusion: success
|
||||
@ -366,7 +366,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Push to Chocolatey
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
shell: pwsh
|
||||
run: choco push --source=https://push.chocolatey.org/
|
||||
working-directory: apps/desktop/dist
|
||||
|
12
.github/workflows/release-web.yml
vendored
12
.github/workflows/release-web.yml
vendored
@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/release-version-check@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
- name: Retrieve bot secrets
|
||||
id: retrieve-bot-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: bitwarden-ci
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
- name: Download latest cloud asset
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: assets
|
||||
@ -154,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download latest cloud asset
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: assets
|
||||
@ -227,7 +227,7 @@ jobs:
|
||||
|
||||
- name: Download latest build artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: apps/web/artifacts
|
||||
@ -238,7 +238,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download latest build artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/download-artifacts@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
workflow: build-web.yml
|
||||
path: apps/web/artifacts
|
||||
|
2
.github/workflows/staged-rollout-desktop.yml
vendored
2
.github/workflows/staged-rollout-desktop.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "aws-electron-access-id,
|
||||
|
6
.github/workflows/version-bump.yml
vendored
6
.github/workflows/version-bump.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
@ -125,14 +125,14 @@ jobs:
|
||||
|
||||
- name: Bump Browser Version - Manifest
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/version-bump@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "apps/browser/src/manifest.json"
|
||||
|
||||
- name: Bump Browser Version - Manifest v3
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/version-bump@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "apps/browser/src/manifest.v3.json"
|
||||
|
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@62d1bf7c3e31c458cc7236b1e69a475d235cd78f
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2023.9.0",
|
||||
"version": "2023.9.1",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "الميزة غير متوفرة"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "لا يمكنك استخدام هذه المِيزة حتى تحديث مفتاح التشفير الخاص بك."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "العضوية المميزة"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Özəllik əlçatmazdır"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Şifrələmə açarınızı güncəlləyənə qədər bu özəlliyi istifadə edə bilməzsiniz."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Şifrələmə açarının daşınması tələb olunur. Şifrələmə açarınızı güncəlləmək üçün zəhmət olmasa veb anbar üzərindən giriş edin."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium üzvlük"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Функцыя недаступна"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Вы не зможаце выкарыстоўваць гэту функцыю, пакуль не абнавіце свой ключ шыфравання."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Прэміяльны статус"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Функцията е недостъпна"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Трябва да обновите шифриращия си ключ, за да използвате тази възможност."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Необходима е промяна на шифриращия ключ. Впишете се в трезора си по уеб, за да обновите своя шифриращ ключ."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Платен абонамент"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "বৈশিষ্ট্য অনুপলব্ধ"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "আপনি আপনার এনক্রিপশন কী হালনাগাদ না করা পর্যন্ত এই বৈশিষ্ট্যটি ব্যবহার করতে পারবেন না।"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "প্রিমিয়াম সদস্য"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Característica no disponible"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "No podeu utilitzar aquesta característica fins que actualitzeu la vostra clau de xifratge."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Subscripció Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funkce je nedostupná"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Dokud neaktualizujete svůj šifrovací klíč, nemůžete tuto funkci použít."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Vyžaduje se migrace šifrovacího klíče. Pro aktualizaci šifrovacího klíče se přihlaste přes webový trezor."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Prémiové členství"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Aelodaeth uwch"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funktion ikke tilgængelig"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Du kan ikke bruge denne funktion, før du opdaterer din krypteringsnøgle."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Krypteringsnøglemigrering nødvendig. Log ind gennem web-boksen for at opdatere krypteringsnøglen."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-medlemskab"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funktion nicht verfügbar"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Du kannst diese Funktion nicht nutzen, solange du deinen Verschlüsselungsschlüssel nicht aktualisiert hast."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-Mitgliedschaft"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Μη διαθέσιμο χαρακτηριστικό"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Δεν μπορείτε να χρησιμοποιήσετε αυτήν τη λειτουργία μέχρι να ενημερώσετε το κλειδί κρυπτογράφησης."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Συνδρομή Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
@ -2406,5 +2406,17 @@
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
"description": "Toggling an expand/collapse state."
|
||||
},
|
||||
"passwordRepromptDisabledAutofillOnPageLoad": {
|
||||
"message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.",
|
||||
"description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load."
|
||||
},
|
||||
"autofillOnPageLoadSetToDefault": {
|
||||
"message": "Auto-fill on page load set to use default setting.",
|
||||
"description": "Toast message for informing the user that auto-fill on page load has been set to the default setting."
|
||||
},
|
||||
"turnOffMasterPasswordPromptToEditField": {
|
||||
"message": "Turn off master password re-prompt to edit this field",
|
||||
"description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item."
|
||||
}
|
||||
}
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Característica no disponible"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "No puedes usar esta característica hasta que actualices tu clave de cifrado."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Se requiere migración de la clave de cifrado. Por favor, inicie sesión a través de la caja fuerte para actualizar su clave de cifrado."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Membresía Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funktsioon pole saadaval"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Seda funktsiooni ei saa enne krüpteerimise võtme uuendamist kasutada."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium versioon"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Ezaugarria ez dago erabilgarri"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Ezin duzu ezaugarri hau erabili zifratze-gakoa eguneratu arte."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium bazkidea"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "ویژگی موجود نیست"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "تا زمانی که کد رمزنگاری را بهروز نکنید نمیتوانید از این قابلیت استفاده کنید."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "عضویت پرمیوم"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Ominaisuus ei ole käytettävissä"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Et voi käyttää tätä toimintoa ennen kuin päivität salausavaimesi."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Salausavaimen siirto vaaditaan. Päivitä salausavaimesi kirjautumalla verkkoholviin."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-jäsenyys"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Hindi magagamit ang tampok"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Hindi mo maari gamitin ang tampok na ito hanggang hindi mo iupdate ang iyong encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Pagiging miyembro ng premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Fonctionnalité non disponible"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Vous ne pouvez pas utiliser cette fonctionnalité avant de mettre à jour votre clé de chiffrement."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Migration de la clé de chiffrement nécessaire. Veuillez vous connecter sur le coffre web pour mettre à jour votre clé de chiffrement."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Adhésion Premium"
|
||||
@ -1606,10 +1606,10 @@
|
||||
"message": "Le déverrouillage biométrique dans le navigateur n’est pas pris en charge sur cet appareil"
|
||||
},
|
||||
"biometricsFailedTitle": {
|
||||
"message": "Biometrics failed"
|
||||
"message": "Le déverrouillage biométique a échoué\n"
|
||||
},
|
||||
"biometricsFailedDesc": {
|
||||
"message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support."
|
||||
"message": "Impossible d'utiliser le déverrouillage biométrique, utilisez votre mot de passe principal ou déconnectez-vous. Si le problème persiste, veuillez contacter le support Bitwarden."
|
||||
},
|
||||
"nativeMessaginPermissionErrorTitle": {
|
||||
"message": "Permission non accordée"
|
||||
@ -1992,7 +1992,7 @@
|
||||
"message": "Export du coffre personnel"
|
||||
},
|
||||
"exportingIndividualVaultDescription": {
|
||||
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
|
||||
"message": "Seuls les éléments individuels du coffre associés à $EMAIL$ seront exportés. Les éléments du coffre de l'organisation ne seront pas inclus. Seules les informations sur les éléments du coffre seront exportées et n'incluront pas les pièces jointes associées.",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -2153,7 +2153,7 @@
|
||||
"message": "Une notification a été envoyée à votre appareil."
|
||||
},
|
||||
"loginInitiated": {
|
||||
"message": "Login initiated"
|
||||
"message": "Connexion initiée"
|
||||
},
|
||||
"exposedMasterPassword": {
|
||||
"message": "Mot de passe principal exposé"
|
||||
@ -2240,28 +2240,28 @@
|
||||
"message": "S'ouvre dans une nouvelle fenêtre"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Device approval required. Select an approval option below:"
|
||||
"message": "L'approbation de l'appareil est requise. Sélectionnez une option d'approbation ci-dessous :"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Remember this device"
|
||||
"message": "Se souvenir de cet appareil"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Uncheck if using a public device"
|
||||
"message": "Décocher si vous utilisez un appareil public"
|
||||
},
|
||||
"approveFromYourOtherDevice": {
|
||||
"message": "Approve from your other device"
|
||||
"message": "Approuver sur votre autre appareil"
|
||||
},
|
||||
"requestAdminApproval": {
|
||||
"message": "Request admin approval"
|
||||
"message": "Demander l'approbation de l'administrateur"
|
||||
},
|
||||
"approveWithMasterPassword": {
|
||||
"message": "Approve with master password"
|
||||
"message": "Approuver avec le mot de passe principal"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization SSO identifier is required."
|
||||
"message": "Identifiant SSO de l'organisation requis."
|
||||
},
|
||||
"eu": {
|
||||
"message": "EU",
|
||||
"message": "UE",
|
||||
"description": "European Union"
|
||||
},
|
||||
"usDomain": {
|
||||
@ -2280,28 +2280,28 @@
|
||||
"message": "Affichage"
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Account successfully created!"
|
||||
"message": "Compte créé avec succès !"
|
||||
},
|
||||
"adminApprovalRequested": {
|
||||
"message": "Admin approval requested"
|
||||
"message": "Approbation de l'administrateur demandée"
|
||||
},
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Demande transmise à votre administrateur."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
"message": "Vous serez notifié une fois approuvé."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
"message": "Problème pour vous connecter ?"
|
||||
},
|
||||
"loginApproved": {
|
||||
"message": "Login approved"
|
||||
"message": "Connexion approuvée"
|
||||
},
|
||||
"userEmailMissing": {
|
||||
"message": "User email missing"
|
||||
"message": "Courriel de l'utilisateur manquant"
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Device trusted"
|
||||
"message": "Appareil de confiance"
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "Saisie requise."
|
||||
@ -2310,7 +2310,7 @@
|
||||
"message": "requis"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search"
|
||||
"message": "Rechercher"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "La saisie doit comporter au moins $COUNT$ caractères.",
|
||||
@ -2340,7 +2340,7 @@
|
||||
}
|
||||
},
|
||||
"inputMinValue": {
|
||||
"message": "Input value must be at least $MIN$.",
|
||||
"message": "La valeur d'entrée doit être au moins de $MIN$.",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"content": "$1",
|
||||
@ -2349,7 +2349,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxValue": {
|
||||
"message": "Input value must not exceed $MAX$.",
|
||||
"message": "La valeur d'entrée ne doit pas excéder $MAX$.",
|
||||
"placeholders": {
|
||||
"max": {
|
||||
"content": "$1",
|
||||
@ -2368,7 +2368,7 @@
|
||||
"message": "La saisie n'est pas une adresse e-mail."
|
||||
},
|
||||
"fieldsNeedAttention": {
|
||||
"message": "$COUNT$ field(s) above need your attention.",
|
||||
"message": "$COUNT$ champ(s) ci-dessus nécessitent votre attention.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -2377,22 +2377,22 @@
|
||||
}
|
||||
},
|
||||
"selectPlaceholder": {
|
||||
"message": "-- Select --"
|
||||
"message": "-- Sélectionner --"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to filter --"
|
||||
"message": "-- Saisir pour filtrer --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Retrieving options..."
|
||||
"message": "Récupération des options..."
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "No items found"
|
||||
"message": "Aucun élément trouvé"
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
"message": "Effacer tout"
|
||||
},
|
||||
"plusNMore": {
|
||||
"message": "+ $QUANTITY$ more",
|
||||
"message": "+ $QUANTITY$ de plus",
|
||||
"placeholders": {
|
||||
"quantity": {
|
||||
"content": "$1",
|
||||
@ -2401,7 +2401,7 @@
|
||||
}
|
||||
},
|
||||
"submenu": {
|
||||
"message": "Submenu"
|
||||
"message": "Sous-menu"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "יכולת זו לא זמינה"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "לא ניתן להשתמש ביכולת זו עד שתעדכן את מפתח ההצפנה שלך."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "חשבון פרימיום"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature Unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "जब तक आप अपनी एन्क्रिप्शन कुंजी को अपडेट नहीं करते, तब तक आप इस सुविधा का उपयोग नहीं कर सकते हैं।"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium Membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Značajka nije dostupna"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Ne možeš koristiti ovu značajku prije nego ažuriraš ključ za šifriranje."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium članstvo"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Ez a funkció nem érhető el."
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Ez a funkció nem használható, amíg nem frissíted a titkosítási kulcsod."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Titkosítási kulcs migráció szükséges. Jelentkezzünk be a webes széfen keresztül a titkosítási kulcs frissítéséhez."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Prémium tagság"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Fitur Tidak Tersedia"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Anda tidak dapat menggunakan fitur ini sampai Anda memperbarui kunci enkripsi Anda."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Keanggotaan Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funzionalità non disponibile"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Non puoi usare questa funzionalità finché non aggiorni la tua chiave di criptografia."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Migrazione della chiave di criptografia obbligatoria. Accedi tramite la cassaforte web per aggiornare la tua chiave di criptografia."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Abbonamento Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "サービスが利用できません"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "暗号キーを更新するまでこの機能は使用できません。"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "暗号化キーの移行が必要です。暗号化キーを更新するには、ウェブ保管庫からログインしてください。"
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "プレミアム会員"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "ವೈಶಿಷ್ಟ್ಯ ಲಭ್ಯವಿಲ್ಲ"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "ನಿಮ್ಮ ಎನ್ಕ್ರಿಪ್ಶನ್ ಕೀಲಿಯನ್ನು ನವೀಕರಿಸುವವರೆಗೆ ನೀವು ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲಾಗುವುದಿಲ್ಲ."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "ಪ್ರೀಮಿಯಂ ಸದಸ್ಯತ್ವ"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "기능 사용할 수 없음"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "이 기능을 사용하려면 암호화 키를 업데이트해야 합니다."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "프리미엄 멤버십"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funkcija neprieinama"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Negali naudotis šia funkcija, kol neatnaujinsi šifravimo raktą."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium narystė"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Iespēja nav pieejama"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Jūs nevarat izmantot šo funkciju līdz jūs atjaunojat savu šifrēšanas atslēgu."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Nepieciešama šifrēšanas atslēgas nomaiņa. Lūgums pieteikties tīmekļa glabātavā, lai atjauninātu savu šifrēšanas atslēgu."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium dalība"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "സവിശേഷത ലഭ്യമല്ല"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "നിങ്ങളുടെ എൻക്രിപ്ഷൻ കീ അപ്ഡേറ്റ് ചെയ്യുന്നതുവരെ നിങ്ങൾക്ക് ഈ സവിശേഷത ഉപയോഗിക്കാൻ കഴിയില്ല."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "പ്രീമിയം അംഗത്വം"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Egenskapen er utilgjengelig"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Du kan ikke bruke denne funksjonen før du oppdaterer krypteringsnøkkelen din."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-medlemskap"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Functionaliteit niet beschikbaar"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Je kunt deze functie pas gebruiken als je je encryptiesleutel bijwerkt."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-abonnement"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funkcja jest niedostępna"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Nie możesz używać tej funkcji, dopóki nie zaktualizujesz klucza szyfrowania."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Wymagana jest migracja klucza szyfrowania. Zaloguj się przez sejf internetowy, aby zaktualizować klucz szyfrowania."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Konto Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funcionalidade Indisponível"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Você não pode usar este recurso, até você atualizar sua chave de criptografia."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Assinatura Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funcionalidade indisponível"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Não pode utilizar esta funcionalidade até atualizar a sua chave de encriptação."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "É necessária a migração da chave de encriptação. Inicie sessão através do cofre Web para atualizar a sua chave de encriptação."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Subscrição Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funcție indisponibilă"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Nu puteți utiliza această caracteristică înainte de a actualiza cheia de criptare."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Abonament Premium"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Функция недоступна"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Вы не можете использовать эту функцию, пока не обновите свой ключ шифрования."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Требуется миграция ключа шифрования. Чтобы обновить ключ шифрования, войдите через веб-хранилище."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Премиум"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "විශේෂාංගය ලබාගත නොහැක"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "ඔබ ඔබේ සංකේතාංකන යතුර යාවත්කාලීන කරන තුරු ඔබට මෙම අංගය භාවිතා කළ නොහැක."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "වාරික සාමාජිකත්වය"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funkcia nie je k dispozícii"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Túto funkciu nemožno použiť, pokým neaktualizujete svoj šifrovací kľúč."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Vyžaduje sa migrácia šifrovacieho kľúča. Na aktualizáciu šifrovacieho kľúča sa prihláste cez webový trezor."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Prémiové členstvo"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funkcija ni na voljo."
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "To funkcijo lahko uporabite šele, ko posodobite svoj šifrirni ključ."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium članstvo"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Функција је недоступна"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Не можете да користите ову способност док не промените Ваш кључ за шифровање."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Премијум чланство"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Funktion ej tillgänglig"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Du kan inte använda denna funktion förrän du uppdaterar din krypteringsnyckel."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Migrering av krypteringsnyckel krävs. Logga in på webbvalvet för att uppdatera din krypteringsnyckel."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium-medlemskap"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature Unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "คุณไม่สามารถใช้คุณลักษณะนี้ได้จนกว่าคุณจะปรับปรุงคีย์การเข้ารหัสลับของคุณ"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium Membership"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Özellik kullanılamıyor"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Şifreleme anahtarınızı güncellemeden bu özelliği kullanamazsınız."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Şifreleme anahtarınızın güncellenmesi gerekiyor. Şifreleme anahtarınızı güncellemek için lütfen web kasasına giriş yapın."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium üyelik"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Функція недоступна"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Ви не можете використовувати цю функцію доки не оновите свій ключ шифрування."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Потрібно перенести ключ шифрування. Увійдіть у вебсховище та оновіть свій ключ шифрування."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Преміум статус"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Tính năng không có sẵn"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "Bạn không thể sử dụng tính năng này cho đến khi bạn cập nhật khoá mã hóa."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Thành viên Cao Cấp"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "功能不可用"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "在您更新加密密钥前,您不能使用此功能。"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "需要迁移加密密钥。请登录网页版密码库来更新您的加密密钥。"
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "高级会员"
|
||||
|
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "功能不可用"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "更新加密金鑰前不能使用此功能。"
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "進階會員"
|
||||
|
@ -2,6 +2,7 @@
|
||||
* Represents an HTML form whose elements can be autofilled
|
||||
*/
|
||||
export default class AutofillForm {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* The unique identifier assigned to this field during collection of the page details
|
||||
*/
|
||||
|
@ -1,8 +1,32 @@
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
import AutofillForm from "../../models/autofill-form";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
import { ElementWithOpId, FormFieldElement } from "../../types";
|
||||
|
||||
type AutofillFormElements = Map<ElementWithOpId<HTMLFormElement>, AutofillForm>;
|
||||
|
||||
type AutofillFieldElements = Map<ElementWithOpId<FormFieldElement>, AutofillField>;
|
||||
|
||||
type UpdateAutofillDataAttributeParams = {
|
||||
element: ElementWithOpId<HTMLFormElement | FormFieldElement>;
|
||||
attributeName: string;
|
||||
dataTarget?: AutofillForm | AutofillField;
|
||||
dataTargetKey?: string;
|
||||
};
|
||||
|
||||
interface CollectAutofillContentService {
|
||||
getPageDetails(): Promise<AutofillPageDetails>;
|
||||
getAutofillFieldElementByOpid(opid: string): HTMLElement | null;
|
||||
queryAllTreeWalkerNodes(
|
||||
rootNode: Node,
|
||||
filterCallback: CallableFunction,
|
||||
isObservingShadowRoot?: boolean
|
||||
): Node[];
|
||||
}
|
||||
|
||||
export { CollectAutofillContentService };
|
||||
export {
|
||||
AutofillFormElements,
|
||||
AutofillFieldElements,
|
||||
UpdateAutofillDataAttributeParams,
|
||||
CollectAutofillContentService,
|
||||
};
|
||||
|
@ -75,7 +75,7 @@ describe("AutofillService", () => {
|
||||
const autofillV1Script = "autofill.js";
|
||||
const autofillV2Script = "autofill-init.js";
|
||||
const defaultAutofillScripts = ["autofiller.js", "notificationBar.js", "contextMenuHandler.js"];
|
||||
const defaultExecuteScriptOptions = { allFrames: true, runAt: "document_start" };
|
||||
const defaultExecuteScriptOptions = { runAt: "document_start" };
|
||||
let tabMock: chrome.tabs.Tab;
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
|
||||
@ -91,11 +91,13 @@ describe("AutofillService", () => {
|
||||
[autofillV1Script, ...defaultAutofillScripts].forEach((scriptName) => {
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${scriptName}`,
|
||||
frameId: sender.frameId,
|
||||
...defaultExecuteScriptOptions,
|
||||
});
|
||||
});
|
||||
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillV2Script}`,
|
||||
frameId: sender.frameId,
|
||||
...defaultExecuteScriptOptions,
|
||||
});
|
||||
});
|
||||
|
@ -19,9 +19,9 @@ import AutofillScript from "../models/autofill-script";
|
||||
import {
|
||||
AutoFillOptions,
|
||||
AutofillService as AutofillServiceInterface,
|
||||
PageDetail,
|
||||
FormData,
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
} from "./abstractions/autofill.service";
|
||||
import {
|
||||
AutoFillConstants,
|
||||
@ -62,7 +62,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
for (const injectedScript of injectedScripts) {
|
||||
await BrowserApi.executeScriptInTab(sender.tab.id, {
|
||||
file: `content/${injectedScript}`,
|
||||
allFrames: true,
|
||||
frameId: sender.frameId,
|
||||
runAt: "document_start",
|
||||
});
|
||||
}
|
||||
@ -260,7 +260,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher == null) {
|
||||
if (cipher == null || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillForm from "../models/autofill-form";
|
||||
import {
|
||||
ElementWithOpId,
|
||||
FillableFormFieldElement,
|
||||
@ -32,7 +36,128 @@ describe("CollectAutofillContentService", () => {
|
||||
});
|
||||
|
||||
describe("getPageDetails", () => {
|
||||
it("returns an object containing information about the curren page as well as autofill data for the forms and fields of the page", async () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(collectAutofillContentService as any, "setupMutationObserver")
|
||||
.mockImplementationOnce(() => {
|
||||
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>();
|
||||
});
|
||||
});
|
||||
|
||||
it("sets up the mutation observer the first time getPageDetails is called", async () => {
|
||||
await collectAutofillContentService.getPageDetails();
|
||||
await collectAutofillContentService.getPageDetails();
|
||||
|
||||
expect(collectAutofillContentService["setupMutationObserver"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns an object with empty forms and fields if no fields were found on a previous iteration", async () => {
|
||||
collectAutofillContentService["domRecentlyMutated"] = false;
|
||||
collectAutofillContentService["noFieldsFound"] = true;
|
||||
jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
|
||||
jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
|
||||
|
||||
await collectAutofillContentService.getPageDetails();
|
||||
|
||||
expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalledWith({}, []);
|
||||
expect(
|
||||
collectAutofillContentService["queryAutofillFormAndFieldElements"]
|
||||
).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an object with cached form and field data values", async () => {
|
||||
const formId = "validFormId";
|
||||
const formAction = "https://example.com/";
|
||||
const formMethod = "post";
|
||||
const formName = "validFormName";
|
||||
const usernameFieldId = "usernameField";
|
||||
const usernameFieldName = "username";
|
||||
const usernameFieldLabel = "User Name";
|
||||
const passwordFieldId = "passwordField";
|
||||
const passwordFieldName = "password";
|
||||
const passwordFieldLabel = "Password";
|
||||
document.body.innerHTML = `
|
||||
<form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
|
||||
<label for="${usernameFieldId}">${usernameFieldLabel}</label>
|
||||
<input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
|
||||
<label for="${passwordFieldId}">${passwordFieldLabel}</label>
|
||||
<input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
|
||||
</form>
|
||||
`;
|
||||
const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
|
||||
const autofillForm: AutofillForm = {
|
||||
opid: "__form__0",
|
||||
htmlAction: formAction,
|
||||
htmlName: formName,
|
||||
htmlID: formId,
|
||||
htmlMethod: formMethod,
|
||||
};
|
||||
const fieldElement = document.getElementById(
|
||||
usernameFieldId
|
||||
) as ElementWithOpId<FormFieldElement>;
|
||||
const autofillField: AutofillField = {
|
||||
opid: "__0",
|
||||
elementNumber: 0,
|
||||
maxLength: 999,
|
||||
viewable: true,
|
||||
htmlID: usernameFieldId,
|
||||
htmlName: usernameFieldName,
|
||||
htmlClass: null,
|
||||
tabindex: null,
|
||||
title: "",
|
||||
tagName: "input",
|
||||
"label-tag": usernameFieldLabel,
|
||||
"label-data": null,
|
||||
"label-aria": null,
|
||||
"label-top": null,
|
||||
"label-right": passwordFieldLabel,
|
||||
"label-left": usernameFieldLabel,
|
||||
placeholder: "",
|
||||
rel: null,
|
||||
type: "text",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
form: "__form__0",
|
||||
"aria-hidden": false,
|
||||
"aria-disabled": false,
|
||||
"aria-haspopup": false,
|
||||
"data-stripe": null,
|
||||
};
|
||||
collectAutofillContentService["domRecentlyMutated"] = false;
|
||||
collectAutofillContentService["autofillFormElements"] = new Map([
|
||||
[formElement, autofillForm],
|
||||
]);
|
||||
collectAutofillContentService["autofillFieldElements"] = new Map([
|
||||
[fieldElement, autofillField],
|
||||
]);
|
||||
jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
|
||||
jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFormsData");
|
||||
jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFieldsData");
|
||||
jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
|
||||
|
||||
await collectAutofillContentService.getPageDetails();
|
||||
|
||||
expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["getFormattedAutofillFormsData"]).toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["getFormattedAutofillFieldsData"]).toHaveBeenCalled();
|
||||
expect(
|
||||
collectAutofillContentService["queryAutofillFormAndFieldElements"]
|
||||
).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an object containing information about the current page as well as autofill data for the forms and fields of the page", async () => {
|
||||
const documentTitle = "Test Page";
|
||||
const formId = "validFormId";
|
||||
const formAction = "https://example.com/";
|
||||
@ -145,6 +270,19 @@ describe("CollectAutofillContentService", () => {
|
||||
collectedTimestamp: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the noFieldsFond property to true if the page has no forms or fields", async function () {
|
||||
document.body.innerHTML = "";
|
||||
collectAutofillContentService["noFieldsFound"] = false;
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
|
||||
|
||||
await collectAutofillContentService.getPageDetails();
|
||||
|
||||
expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["noFieldsFound"]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAutofillFieldElementByOpid", () => {
|
||||
@ -213,6 +351,44 @@ describe("CollectAutofillContentService", () => {
|
||||
});
|
||||
|
||||
describe("buildAutofillFormsData", () => {
|
||||
it("will not attempt to gather data from a cached form element", () => {
|
||||
const documentTitle = "Test Page";
|
||||
const formId = "validFormId";
|
||||
const formAction = "https://example.com/";
|
||||
const formMethod = "post";
|
||||
const formName = "validFormName";
|
||||
document.title = documentTitle;
|
||||
document.body.innerHTML = `
|
||||
<form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
|
||||
<label for="usernameFieldId">usernameFieldLabel</label>
|
||||
<input type="text" id="usernameFieldId" name="usernameFieldName" />
|
||||
<label for="passwordFieldId">passwordFieldLabel</label>
|
||||
<input type="password" id="passwordFieldId" name="passwordFieldName" />
|
||||
</form>
|
||||
|
||||
`;
|
||||
const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
|
||||
const existingAutofillForm: AutofillForm = {
|
||||
opid: "__form__0",
|
||||
htmlAction: formAction,
|
||||
htmlName: formName,
|
||||
htmlID: formId,
|
||||
htmlMethod: formMethod,
|
||||
};
|
||||
collectAutofillContentService["autofillFormElements"] = new Map([
|
||||
[formElement, existingAutofillForm],
|
||||
]);
|
||||
const formElements = Array.from(document.querySelectorAll("form"));
|
||||
jest.spyOn(collectAutofillContentService as any, "getFormActionAttribute");
|
||||
|
||||
const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"](
|
||||
formElements as Node[]
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["getFormActionAttribute"]).not.toHaveBeenCalled();
|
||||
expect(autofillFormsData).toStrictEqual({ __form__0: existingAutofillForm });
|
||||
});
|
||||
|
||||
it("returns an object of AutofillForm objects with the form id as a key", () => {
|
||||
const documentTitle = "Test Page";
|
||||
const formId1 = "validFormId";
|
||||
@ -237,7 +413,9 @@ describe("CollectAutofillContentService", () => {
|
||||
</form>
|
||||
`;
|
||||
|
||||
const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"]();
|
||||
const { formElements } = collectAutofillContentService["queryAutofillFormAndFieldElements"]();
|
||||
const autofillFormsData =
|
||||
collectAutofillContentService["buildAutofillFormsData"](formElements);
|
||||
|
||||
expect(autofillFormsData).toStrictEqual({
|
||||
__form__0: {
|
||||
@ -266,10 +444,17 @@ describe("CollectAutofillContentService", () => {
|
||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"]();
|
||||
const { formFieldElements } =
|
||||
collectAutofillContentService["queryAutofillFormAndFieldElements"]();
|
||||
const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"](
|
||||
formFieldElements as FormFieldElement[]
|
||||
);
|
||||
const autofillFieldsData = await Promise.resolve(autofillFieldsPromise);
|
||||
|
||||
expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(50);
|
||||
expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(
|
||||
100,
|
||||
formFieldElements
|
||||
);
|
||||
expect(collectAutofillContentService["buildAutofillFieldItem"]).toHaveBeenCalledTimes(2);
|
||||
expect(autofillFieldsPromise).toBeInstanceOf(Promise);
|
||||
expect(autofillFieldsData).toStrictEqual([
|
||||
@ -372,9 +557,6 @@ describe("CollectAutofillContentService", () => {
|
||||
const formElements: FormFieldElement[] =
|
||||
collectAutofillContentService["getAutofillFieldElements"]();
|
||||
|
||||
expect(document.querySelectorAll).toHaveBeenCalledWith(
|
||||
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]'
|
||||
);
|
||||
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
|
||||
expect(formElements).toEqual([
|
||||
usernameInput,
|
||||
@ -538,6 +720,105 @@ describe("CollectAutofillContentService", () => {
|
||||
});
|
||||
|
||||
describe("buildAutofillFieldItem", () => {
|
||||
it("returns an existing autofill field item if it exists", async () => {
|
||||
const index = 0;
|
||||
const usernameField = {
|
||||
labelText: "Username",
|
||||
id: "username-id",
|
||||
classes: "username input classes",
|
||||
name: "username",
|
||||
type: "text",
|
||||
maxLength: 42,
|
||||
tabIndex: 0,
|
||||
title: "Username Input Title",
|
||||
autocomplete: "username-autocomplete",
|
||||
dataLabel: "username-data-label",
|
||||
ariaLabel: "username-aria-label",
|
||||
placeholder: "username-placeholder",
|
||||
rel: "username-rel",
|
||||
value: "username-value",
|
||||
dataStripe: "data-stripe",
|
||||
};
|
||||
document.body.innerHTML = `
|
||||
<form>
|
||||
<label for="${usernameField.id}">${usernameField.labelText}</label>
|
||||
<input
|
||||
id="${usernameField.id}"
|
||||
class="${usernameField.classes}"
|
||||
name="${usernameField.name}"
|
||||
type="${usernameField.type}"
|
||||
maxlength="${usernameField.maxLength}"
|
||||
tabindex="${usernameField.tabIndex}"
|
||||
title="${usernameField.title}"
|
||||
autocomplete="${usernameField.autocomplete}"
|
||||
data-label="${usernameField.dataLabel}"
|
||||
aria-label="${usernameField.ariaLabel}"
|
||||
placeholder="${usernameField.placeholder}"
|
||||
rel="${usernameField.rel}"
|
||||
value="${usernameField.value}"
|
||||
data-stripe="${usernameField.dataStripe}"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
document.body.innerHTML = `
|
||||
<form>
|
||||
<label for="${usernameField.id}">${usernameField.labelText}</label>
|
||||
<input
|
||||
id="${usernameField.id}"
|
||||
class="${usernameField.classes}"
|
||||
name="${usernameField.name}"
|
||||
type="${usernameField.type}"
|
||||
maxlength="${usernameField.maxLength}"
|
||||
tabindex="${usernameField.tabIndex}"
|
||||
title="${usernameField.title}"
|
||||
autocomplete="${usernameField.autocomplete}"
|
||||
data-label="${usernameField.dataLabel}"
|
||||
aria-label="${usernameField.ariaLabel}"
|
||||
placeholder="${usernameField.placeholder}"
|
||||
rel="${usernameField.rel}"
|
||||
value="${usernameField.value}"
|
||||
data-stripe="${usernameField.dataStripe}"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
const existingFieldData: AutofillField = {
|
||||
elementNumber: index,
|
||||
htmlClass: usernameField.classes,
|
||||
htmlID: usernameField.id,
|
||||
htmlName: usernameField.name,
|
||||
maxLength: usernameField.maxLength,
|
||||
opid: `__${index}`,
|
||||
tabindex: String(usernameField.tabIndex),
|
||||
tagName: "input",
|
||||
title: usernameField.title,
|
||||
viewable: true,
|
||||
};
|
||||
const usernameInput = document.getElementById(
|
||||
usernameField.id
|
||||
) as ElementWithOpId<FillableFormFieldElement>;
|
||||
usernameInput.opid = "__0";
|
||||
collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData);
|
||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
||||
jest
|
||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
||||
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
||||
|
||||
const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
|
||||
usernameInput,
|
||||
0
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled();
|
||||
expect(
|
||||
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable
|
||||
).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
|
||||
expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
|
||||
expect(autofillFieldItem).toEqual(existingFieldData);
|
||||
});
|
||||
|
||||
it("returns the AutofillField base data values without the field labels or input values if the passed element is a span element", async () => {
|
||||
const index = 0;
|
||||
const spanElementId = "span-element";
|
||||
@ -958,6 +1239,20 @@ describe("CollectAutofillContentService", () => {
|
||||
|
||||
expect(labels).toEqual(document.querySelectorAll("label[for='username']"));
|
||||
});
|
||||
|
||||
it("removes any new lines generated for the query selector", () => {
|
||||
document.body.innerHTML = `
|
||||
<label for="username-
|
||||
id">Username</label>
|
||||
<input type="text" id="username-
|
||||
id">
|
||||
`;
|
||||
const element = document.querySelector("input") as FillableFormFieldElement;
|
||||
|
||||
const labels = collectAutofillContentService["queryElementLabels"](element);
|
||||
|
||||
expect(labels).toEqual(document.querySelectorAll("label[for='username-id']"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLabelElementsTag", () => {
|
||||
@ -1585,4 +1880,466 @@ describe("CollectAutofillContentService", () => {
|
||||
expect(selectWithoutOptionsOptions).toEqual({ options: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getShadowRoot", () => {
|
||||
it("returns null if the passed node is not an HTMLElement instance", () => {
|
||||
const textNode = document.createTextNode("Hello, world!");
|
||||
const shadowRoot = collectAutofillContentService["getShadowRoot"](textNode);
|
||||
|
||||
expect(shadowRoot).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns a value provided by Chrome's openOrClosedShadowRoot API", () => {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
globalThis.chrome.dom = {
|
||||
openOrClosedShadowRoot: jest.fn(),
|
||||
};
|
||||
const element = document.createElement("div");
|
||||
collectAutofillContentService["getShadowRoot"](element);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
expect(chrome.dom.openOrClosedShadowRoot).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTreeWalkerNodesQueryResults", () => {
|
||||
it("will recursively call itself if a shadowDOM element is found and will observe the element for mutations", () => {
|
||||
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>({
|
||||
observe: jest.fn(),
|
||||
});
|
||||
jest.spyOn(collectAutofillContentService as any, "buildTreeWalkerNodesQueryResults");
|
||||
const shadowRoot = document.createElement("div");
|
||||
jest
|
||||
.spyOn(collectAutofillContentService as any, "getShadowRoot")
|
||||
.mockReturnValueOnce(shadowRoot);
|
||||
const callbackFilter = jest.fn();
|
||||
|
||||
collectAutofillContentService["buildTreeWalkerNodesQueryResults"](
|
||||
document.body,
|
||||
[],
|
||||
callbackFilter,
|
||||
true
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["buildTreeWalkerNodesQueryResults"]).toBeCalledTimes(2);
|
||||
expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled();
|
||||
});
|
||||
|
||||
it("will not observe the shadowDOM element if required to skip", () => {
|
||||
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>({
|
||||
observe: jest.fn(),
|
||||
});
|
||||
const shadowRoot = document.createElement("div");
|
||||
jest
|
||||
.spyOn(collectAutofillContentService as any, "getShadowRoot")
|
||||
.mockReturnValueOnce(shadowRoot);
|
||||
const callbackFilter = jest.fn();
|
||||
|
||||
collectAutofillContentService["buildTreeWalkerNodesQueryResults"](
|
||||
document.body,
|
||||
[],
|
||||
callbackFilter,
|
||||
false
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["mutationObserver"].observe).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupMutationObserver", () => {
|
||||
it("sets up a mutation observer and observes the document element", () => {
|
||||
jest.spyOn(MutationObserver.prototype, "observe");
|
||||
|
||||
collectAutofillContentService["setupMutationObserver"]();
|
||||
|
||||
expect(collectAutofillContentService["mutationObserver"]).toBeInstanceOf(MutationObserver);
|
||||
expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleMutationObserverMutation", () => {
|
||||
it("will set the domRecentlyMutated value to true and the noFieldsFound value to false if a form or field node has been added ", () => {
|
||||
const form = document.createElement("form");
|
||||
document.body.appendChild(form);
|
||||
const addedNodes = document.querySelectorAll("form");
|
||||
const removedNodes = document.querySelectorAll("li");
|
||||
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "childList",
|
||||
addedNodes: addedNodes,
|
||||
attributeName: null,
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: removedNodes,
|
||||
target: document.body,
|
||||
};
|
||||
collectAutofillContentService["domRecentlyMutated"] = false;
|
||||
collectAutofillContentService["noFieldsFound"] = true;
|
||||
collectAutofillContentService["currentLocationHref"] = window.location.href;
|
||||
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
|
||||
|
||||
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
|
||||
|
||||
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
|
||||
expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
|
||||
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith(
|
||||
removedNodes,
|
||||
true
|
||||
);
|
||||
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith(
|
||||
addedNodes
|
||||
);
|
||||
});
|
||||
|
||||
it("will handle updating the autofill element if any attribute mutations are encountered", () => {
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "attributes",
|
||||
addedNodes: null,
|
||||
attributeName: "value",
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: null,
|
||||
target: document.body,
|
||||
};
|
||||
collectAutofillContentService["domRecentlyMutated"] = false;
|
||||
collectAutofillContentService["noFieldsFound"] = true;
|
||||
collectAutofillContentService["currentLocationHref"] = window.location.href;
|
||||
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
|
||||
jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation");
|
||||
|
||||
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
|
||||
|
||||
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(false);
|
||||
expect(collectAutofillContentService["noFieldsFound"]).toEqual(true);
|
||||
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
|
||||
expect(collectAutofillContentService["handleAutofillElementAttributeMutation"]).toBeCalled();
|
||||
});
|
||||
|
||||
it("will handle window location mutations", () => {
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "attributes",
|
||||
addedNodes: null,
|
||||
attributeName: "value",
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: null,
|
||||
target: document.body,
|
||||
};
|
||||
collectAutofillContentService["currentLocationHref"] = "https://someotherurl.com";
|
||||
jest.spyOn(collectAutofillContentService as any, "handleWindowLocationMutation");
|
||||
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
|
||||
jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation");
|
||||
|
||||
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
|
||||
|
||||
expect(collectAutofillContentService["handleWindowLocationMutation"]).toBeCalled();
|
||||
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
|
||||
expect(
|
||||
collectAutofillContentService["handleAutofillElementAttributeMutation"]
|
||||
).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteCachedAutofillElement", () => {
|
||||
it("removes the autofill form element from the map of elements", () => {
|
||||
const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
|
||||
const autofillForm: AutofillForm = {
|
||||
opid: "1234",
|
||||
htmlName: "formEl",
|
||||
htmlID: "formEl-id",
|
||||
htmlAction: "https://example.com",
|
||||
htmlMethod: "POST",
|
||||
};
|
||||
collectAutofillContentService["autofillFormElements"] = new Map([
|
||||
[formElement, autofillForm],
|
||||
]);
|
||||
|
||||
collectAutofillContentService["deleteCachedAutofillElement"](formElement);
|
||||
|
||||
expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0);
|
||||
});
|
||||
|
||||
it("removes the autofill field element form the map of elements", () => {
|
||||
const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
|
||||
const autofillField: AutofillField = {
|
||||
elementNumber: 0,
|
||||
htmlClass: "",
|
||||
tabindex: "",
|
||||
title: "",
|
||||
viewable: false,
|
||||
opid: "1234",
|
||||
htmlName: "username",
|
||||
htmlID: "username-id",
|
||||
htmlType: "text",
|
||||
htmlAutocomplete: "username",
|
||||
htmlAutofocus: false,
|
||||
htmlDisabled: false,
|
||||
htmlMaxLength: 999,
|
||||
htmlReadonly: false,
|
||||
htmlRequired: false,
|
||||
htmlValue: "jsmith",
|
||||
};
|
||||
collectAutofillContentService["autofillFieldElements"] = new Map([
|
||||
[fieldElement, autofillField],
|
||||
]);
|
||||
|
||||
collectAutofillContentService["deleteCachedAutofillElement"](fieldElement);
|
||||
|
||||
expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleWindowLocationMutation", () => {
|
||||
it("will set the current location to the global location href, set the dom recently mutated flag and the no fields found flag, clear out the autofill form and field maps, and update the autofill elements after mutation", () => {
|
||||
collectAutofillContentService["currentLocationHref"] = "https://example.com/login";
|
||||
collectAutofillContentService["domRecentlyMutated"] = false;
|
||||
collectAutofillContentService["noFieldsFound"] = true;
|
||||
jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation");
|
||||
|
||||
collectAutofillContentService["handleWindowLocationMutation"]();
|
||||
|
||||
expect(collectAutofillContentService["currentLocationHref"]).toEqual(window.location.href);
|
||||
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
|
||||
expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
|
||||
expect(collectAutofillContentService["updateAutofillElementsAfterMutation"]).toBeCalled();
|
||||
expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0);
|
||||
expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAutofillElementAttributeMutation", () => {
|
||||
it("returns early if the target node is not an HTMLElement instance", () => {
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "attributes",
|
||||
addedNodes: null,
|
||||
attributeName: "value",
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: null,
|
||||
target: document.createTextNode("Hello, world!"),
|
||||
};
|
||||
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
|
||||
|
||||
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
|
||||
|
||||
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("will update the autofill form element data if the target node can be found in the autofillFormElements map", () => {
|
||||
const targetNode = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
|
||||
targetNode.setAttribute("name", "username");
|
||||
targetNode.setAttribute("value", "jsmith");
|
||||
const autofillForm: AutofillForm = {
|
||||
opid: "1234",
|
||||
htmlName: "formEl",
|
||||
htmlID: "formEl-id",
|
||||
htmlAction: "https://example.com",
|
||||
htmlMethod: "POST",
|
||||
};
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "attributes",
|
||||
addedNodes: null,
|
||||
attributeName: "id",
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: null,
|
||||
target: targetNode,
|
||||
};
|
||||
collectAutofillContentService["autofillFormElements"] = new Map([[targetNode, autofillForm]]);
|
||||
jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData");
|
||||
|
||||
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
|
||||
|
||||
expect(collectAutofillContentService["updateAutofillFormElementData"]).toBeCalledWith(
|
||||
mutationRecord.attributeName,
|
||||
mutationRecord.target,
|
||||
autofillForm
|
||||
);
|
||||
});
|
||||
|
||||
it("will update the autofill field element data if the target node can be found in the autofillFieldElements map", () => {
|
||||
const targetNode = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
|
||||
targetNode.setAttribute("name", "username");
|
||||
targetNode.setAttribute("value", "jsmith");
|
||||
const autofillField: AutofillField = {
|
||||
elementNumber: 0,
|
||||
htmlClass: "",
|
||||
tabindex: "",
|
||||
title: "",
|
||||
viewable: false,
|
||||
opid: "1234",
|
||||
htmlName: "username",
|
||||
htmlID: "username-id",
|
||||
htmlType: "text",
|
||||
htmlAutocomplete: "username",
|
||||
htmlAutofocus: false,
|
||||
htmlDisabled: false,
|
||||
htmlMaxLength: 999,
|
||||
htmlReadonly: false,
|
||||
htmlRequired: false,
|
||||
htmlValue: "jsmith",
|
||||
};
|
||||
const mutationRecord: MutationRecord = {
|
||||
type: "attributes",
|
||||
addedNodes: null,
|
||||
attributeName: "id",
|
||||
attributeNamespace: null,
|
||||
nextSibling: null,
|
||||
oldValue: null,
|
||||
previousSibling: null,
|
||||
removedNodes: null,
|
||||
target: targetNode,
|
||||
};
|
||||
collectAutofillContentService["autofillFieldElements"] = new Map([
|
||||
[targetNode, autofillField],
|
||||
]);
|
||||
jest.spyOn(collectAutofillContentService as any, "updateAutofillFieldElementData");
|
||||
|
||||
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
|
||||
|
||||
expect(collectAutofillContentService["updateAutofillFieldElementData"]).toBeCalledWith(
|
||||
mutationRecord.attributeName,
|
||||
mutationRecord.target,
|
||||
autofillField
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAutofillFormElementData", () => {
|
||||
const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
|
||||
const autofillForm: AutofillForm = {
|
||||
opid: "1234",
|
||||
htmlName: "formEl",
|
||||
htmlID: "formEl-id",
|
||||
htmlAction: "https://example.com",
|
||||
htmlMethod: "POST",
|
||||
};
|
||||
const updatedAttributes = ["action", "name", "id", "method"];
|
||||
|
||||
updatedAttributes.forEach((attribute) => {
|
||||
it(`will update the ${attribute} value for the form element`, () => {
|
||||
jest.spyOn(collectAutofillContentService["autofillFormElements"], "set");
|
||||
|
||||
collectAutofillContentService["updateAutofillFormElementData"](
|
||||
attribute,
|
||||
formElement,
|
||||
autofillForm
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["autofillFormElements"].set).toBeCalledWith(
|
||||
formElement,
|
||||
autofillForm
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("will not update an attribute value if it is not present in the updateActions object", () => {
|
||||
jest.spyOn(collectAutofillContentService["autofillFormElements"], "set");
|
||||
|
||||
collectAutofillContentService["updateAutofillFormElementData"](
|
||||
"aria-label",
|
||||
formElement,
|
||||
autofillForm
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["autofillFormElements"].set).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAutofillFieldElementData", () => {
|
||||
const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
|
||||
const autofillField: AutofillField = {
|
||||
htmlClass: "value",
|
||||
htmlID: "",
|
||||
htmlName: "",
|
||||
opid: "",
|
||||
tabindex: "",
|
||||
title: "",
|
||||
viewable: false,
|
||||
elementNumber: 0,
|
||||
};
|
||||
const updatedAttributes = [
|
||||
"maxlength",
|
||||
"name",
|
||||
"id",
|
||||
"type",
|
||||
"autocomplete",
|
||||
"class",
|
||||
"tabindex",
|
||||
"title",
|
||||
"value",
|
||||
"rel",
|
||||
"tagname",
|
||||
"checked",
|
||||
"disabled",
|
||||
"readonly",
|
||||
"data-label",
|
||||
"aria-label",
|
||||
"aria-hidden",
|
||||
"aria-disabled",
|
||||
"aria-haspopup",
|
||||
"data-stripe",
|
||||
];
|
||||
|
||||
updatedAttributes.forEach((attribute) => {
|
||||
it(`will update the ${attribute} value for the field element`, async () => {
|
||||
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");
|
||||
|
||||
await collectAutofillContentService["updateAutofillFieldElementData"](
|
||||
attribute,
|
||||
fieldElement,
|
||||
autofillField
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["autofillFieldElements"].set).toBeCalledWith(
|
||||
fieldElement,
|
||||
autofillField
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("will check the dom element's visibility if the `style` or `class` attribute has updated ", async () => {
|
||||
jest.spyOn(
|
||||
collectAutofillContentService["domElementVisibilityService"],
|
||||
"isFormFieldViewable"
|
||||
);
|
||||
const attributes = ["class", "style"];
|
||||
|
||||
for (const attribute of attributes) {
|
||||
await collectAutofillContentService["updateAutofillFieldElementData"](
|
||||
attribute,
|
||||
fieldElement,
|
||||
autofillField
|
||||
);
|
||||
|
||||
expect(
|
||||
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable
|
||||
).toBeCalledWith(fieldElement);
|
||||
}
|
||||
});
|
||||
|
||||
it("will not update an attribute value if it is not present in the updateActions object", async () => {
|
||||
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");
|
||||
|
||||
await collectAutofillContentService["updateAutofillFieldElementData"](
|
||||
"random-attribute",
|
||||
fieldElement,
|
||||
autofillField
|
||||
);
|
||||
|
||||
expect(collectAutofillContentService["autofillFieldElements"].set).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,34 +8,70 @@ import {
|
||||
FormElementWithAttribute,
|
||||
} from "../types";
|
||||
|
||||
import { CollectAutofillContentService as CollectAutofillContentServiceInterface } from "./abstractions/collect-autofill-content.service";
|
||||
import {
|
||||
UpdateAutofillDataAttributeParams,
|
||||
AutofillFieldElements,
|
||||
AutofillFormElements,
|
||||
CollectAutofillContentService as CollectAutofillContentServiceInterface,
|
||||
} from "./abstractions/collect-autofill-content.service";
|
||||
import DomElementVisibilityService from "./dom-element-visibility.service";
|
||||
|
||||
class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
|
||||
private readonly domElementVisibilityService: DomElementVisibilityService;
|
||||
private noFieldsFound = false;
|
||||
private domRecentlyMutated = true;
|
||||
private autofillFormElements: AutofillFormElements = new Map();
|
||||
private autofillFieldElements: AutofillFieldElements = new Map();
|
||||
private currentLocationHref = "";
|
||||
private mutationObserver: MutationObserver;
|
||||
private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout;
|
||||
private readonly updateAfterMutationTimeoutDelay = 1000;
|
||||
|
||||
constructor(domElementVisibilityService: DomElementVisibilityService) {
|
||||
this.domElementVisibilityService = domElementVisibilityService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the data for all the forms and fields
|
||||
* that are found within the page DOM.
|
||||
* Builds the data for all forms and fields found within the page DOM.
|
||||
* Sets up a mutation observer to verify DOM changes and returns early
|
||||
* with cached data if no changes are detected.
|
||||
* @returns {Promise<AutofillPageDetails>}
|
||||
* @public
|
||||
*/
|
||||
async getPageDetails(): Promise<AutofillPageDetails> {
|
||||
const autofillFormsData: Record<string, AutofillForm> = this.buildAutofillFormsData();
|
||||
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData();
|
||||
if (!this.mutationObserver) {
|
||||
this.setupMutationObserver();
|
||||
}
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
url: (document.defaultView || window).location.href,
|
||||
documentUrl: document.location.href,
|
||||
forms: autofillFormsData,
|
||||
fields: autofillFieldsData,
|
||||
collectedTimestamp: Date.now(),
|
||||
};
|
||||
if (!this.domRecentlyMutated && this.noFieldsFound) {
|
||||
return this.getFormattedPageDetails({}, []);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.domRecentlyMutated &&
|
||||
this.autofillFormElements.size &&
|
||||
this.autofillFieldElements.size
|
||||
) {
|
||||
return this.getFormattedPageDetails(
|
||||
this.getFormattedAutofillFormsData(),
|
||||
this.getFormattedAutofillFieldsData()
|
||||
);
|
||||
}
|
||||
|
||||
const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements();
|
||||
const autofillFormsData: Record<string, AutofillForm> =
|
||||
this.buildAutofillFormsData(formElements);
|
||||
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData(
|
||||
formFieldElements as FormFieldElement[]
|
||||
);
|
||||
this.sortAutofillFieldElementsMap();
|
||||
|
||||
if (!Object.values(autofillFormsData).length || !autofillFieldsData.length) {
|
||||
this.noFieldsFound = true;
|
||||
}
|
||||
|
||||
this.domRecentlyMutated = false;
|
||||
return this.getFormattedPageDetails(autofillFormsData, autofillFieldsData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,15 +82,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
* @returns {FormFieldElement | null}
|
||||
*/
|
||||
getAutofillFieldElementByOpid(opid: string): FormFieldElement | null {
|
||||
const fieldElements = this.getAutofillFieldElements();
|
||||
const fieldElementsWithOpid = fieldElements.filter(
|
||||
const cachedFormFieldElements = Array.from(this.autofillFieldElements.keys());
|
||||
const formFieldElements = cachedFormFieldElements?.length
|
||||
? cachedFormFieldElements
|
||||
: this.getAutofillFieldElements();
|
||||
const fieldElementsWithOpid = formFieldElements.filter(
|
||||
(fieldElement) => (fieldElement as ElementWithOpId<FormFieldElement>).opid === opid
|
||||
) as ElementWithOpId<FormFieldElement>[];
|
||||
|
||||
if (!fieldElementsWithOpid.length) {
|
||||
const elementIndex = parseInt(opid.split("__")[1], 10);
|
||||
|
||||
return fieldElements[elementIndex] || null;
|
||||
return formFieldElements[elementIndex] || null;
|
||||
}
|
||||
|
||||
if (fieldElementsWithOpid.length > 1) {
|
||||
@ -65,30 +104,120 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
return fieldElementsWithOpid[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the DOM for all the nodes that match the given filter callback
|
||||
* and returns a collection of nodes.
|
||||
* @param {Node} rootNode
|
||||
* @param {Function} filterCallback
|
||||
* @param {boolean} isObservingShadowRoot
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
queryAllTreeWalkerNodes(
|
||||
rootNode: Node,
|
||||
filterCallback: CallableFunction,
|
||||
isObservingShadowRoot = true
|
||||
): Node[] {
|
||||
const treeWalkerQueryResults: Node[] = [];
|
||||
|
||||
this.buildTreeWalkerNodesQueryResults(
|
||||
rootNode,
|
||||
treeWalkerQueryResults,
|
||||
filterCallback,
|
||||
isObservingShadowRoot
|
||||
);
|
||||
|
||||
return treeWalkerQueryResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the AutofillFieldElements map by the elementNumber property.
|
||||
* @private
|
||||
*/
|
||||
private sortAutofillFieldElementsMap() {
|
||||
if (!this.autofillFieldElements.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autofillFieldElements = new Map(
|
||||
[...this.autofillFieldElements].sort((a, b) => a[1].elementNumber - b[1].elementNumber)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and returns the AutofillPageDetails object
|
||||
* @param {Record<string, AutofillForm>} autofillFormsData
|
||||
* @param {AutofillField[]} autofillFieldsData
|
||||
* @returns {AutofillPageDetails}
|
||||
* @private
|
||||
*/
|
||||
private getFormattedPageDetails(
|
||||
autofillFormsData: Record<string, AutofillForm>,
|
||||
autofillFieldsData: AutofillField[]
|
||||
): AutofillPageDetails {
|
||||
return {
|
||||
title: document.title,
|
||||
url: (document.defaultView || window).location.href,
|
||||
documentUrl: document.location.href,
|
||||
forms: autofillFormsData,
|
||||
fields: autofillFieldsData,
|
||||
collectedTimestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the DOM for all the forms elements and
|
||||
* returns a collection of AutofillForm objects.
|
||||
* @returns {Record<string, AutofillForm>}
|
||||
* @private
|
||||
*/
|
||||
private buildAutofillFormsData(): Record<string, AutofillForm> {
|
||||
const autofillForms: Record<string, AutofillForm> = {};
|
||||
const documentFormElements = document.querySelectorAll("form");
|
||||
|
||||
documentFormElements.forEach((formElement: HTMLFormElement, index: number) => {
|
||||
private buildAutofillFormsData(formElements: Node[]): Record<string, AutofillForm> {
|
||||
for (let index = 0; index < formElements.length; index++) {
|
||||
const formElement = formElements[index] as ElementWithOpId<HTMLFormElement>;
|
||||
formElement.opid = `__form__${index}`;
|
||||
|
||||
autofillForms[formElement.opid] = {
|
||||
const existingAutofillForm = this.autofillFormElements.get(formElement);
|
||||
if (existingAutofillForm) {
|
||||
existingAutofillForm.opid = formElement.opid;
|
||||
this.autofillFormElements.set(formElement, existingAutofillForm);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.autofillFormElements.set(formElement, {
|
||||
opid: formElement.opid,
|
||||
htmlAction: new URL(
|
||||
this.getPropertyOrAttribute(formElement, "action"),
|
||||
window.location.href
|
||||
).href,
|
||||
htmlAction: this.getFormActionAttribute(formElement),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, "name"),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, "id"),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this.getFormattedAutofillFormsData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the action attribute of the form element. If the action attribute
|
||||
* is a relative path, it will be converted to an absolute path.
|
||||
* @param {ElementWithOpId<HTMLFormElement>} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string {
|
||||
return new URL(this.getPropertyOrAttribute(element, "action"), window.location.href).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all known form elements and returns an AutofillForm object
|
||||
* containing a key value pair of the form element's opid and the form data.
|
||||
* @returns {Record<string, AutofillForm>}
|
||||
* @private
|
||||
*/
|
||||
private getFormattedAutofillFormsData(): Record<string, AutofillForm> {
|
||||
const autofillForms: Record<string, AutofillForm> = {};
|
||||
const autofillFormElements = Array.from(this.autofillFormElements);
|
||||
for (let index = 0; index < autofillFormElements.length; index++) {
|
||||
const [formElement, autofillForm] = autofillFormElements[index];
|
||||
autofillForms[formElement.opid] = autofillForm;
|
||||
}
|
||||
|
||||
return autofillForms;
|
||||
}
|
||||
@ -99,8 +228,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
* @returns {Promise<AutofillField[]>}
|
||||
* @private
|
||||
*/
|
||||
private async buildAutofillFieldsData(): Promise<AutofillField[]> {
|
||||
const autofillFieldElements = this.getAutofillFieldElements(50);
|
||||
private async buildAutofillFieldsData(
|
||||
formFieldElements: FormFieldElement[]
|
||||
): Promise<AutofillField[]> {
|
||||
const autofillFieldElements = this.getAutofillFieldElements(100, formFieldElements);
|
||||
const autofillFieldDataPromises = autofillFieldElements.map(this.buildAutofillFieldItem);
|
||||
|
||||
return Promise.all(autofillFieldDataPromises);
|
||||
@ -111,18 +242,19 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
* and returns a list limited to the given `fieldsLimit` number that
|
||||
* is ordered by priority.
|
||||
* @param {number} fieldsLimit - The maximum number of fields to return
|
||||
* @param {FormFieldElement[]} previouslyFoundFormFieldElements - The list of all the field elements
|
||||
* @returns {FormFieldElement[]}
|
||||
* @private
|
||||
*/
|
||||
private getAutofillFieldElements(fieldsLimit?: number): FormFieldElement[] {
|
||||
const formFieldElements: FormFieldElement[] = [
|
||||
...(document.querySelectorAll(
|
||||
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), ' +
|
||||
"textarea:not([data-bwignore]), " +
|
||||
"select:not([data-bwignore]), " +
|
||||
"span[data-bwautofill]"
|
||||
) as NodeListOf<FormFieldElement>),
|
||||
];
|
||||
private getAutofillFieldElements(
|
||||
fieldsLimit?: number,
|
||||
previouslyFoundFormFieldElements?: FormFieldElement[]
|
||||
): FormFieldElement[] {
|
||||
const formFieldElements =
|
||||
previouslyFoundFormFieldElements ||
|
||||
(this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) =>
|
||||
this.isNodeFormFieldElement(node)
|
||||
) as FormFieldElement[]);
|
||||
|
||||
if (!fieldsLimit || formFieldElements.length <= fieldsLimit) {
|
||||
return formFieldElements;
|
||||
@ -168,6 +300,15 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
): Promise<AutofillField> => {
|
||||
element.opid = `__${index}`;
|
||||
|
||||
const existingAutofillField = this.autofillFieldElements.get(element);
|
||||
if (existingAutofillField) {
|
||||
existingAutofillField.opid = element.opid;
|
||||
existingAutofillField.elementNumber = index;
|
||||
this.autofillFieldElements.set(element, existingAutofillField);
|
||||
|
||||
return existingAutofillField;
|
||||
}
|
||||
|
||||
const autofillFieldBase = {
|
||||
opid: element.opid,
|
||||
elementNumber: index,
|
||||
@ -178,19 +319,16 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
||||
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
||||
title: this.getPropertyOrAttribute(element, "title"),
|
||||
tagName: this.getPropertyOrAttribute(element, "tagName")?.toLowerCase(),
|
||||
tagName: this.getAttributeLowerCase(element, "tagName"),
|
||||
};
|
||||
|
||||
if (element instanceof HTMLSpanElement) {
|
||||
this.autofillFieldElements.set(element, autofillFieldBase);
|
||||
return autofillFieldBase;
|
||||
}
|
||||
|
||||
let autofillFieldLabels = {};
|
||||
const autoCompleteType =
|
||||
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocomplete");
|
||||
const elementType = this.getPropertyOrAttribute(element, "type")?.toLowerCase();
|
||||
const elementType = this.getAttributeLowerCase(element, "type");
|
||||
if (elementType !== "hidden") {
|
||||
autofillFieldLabels = {
|
||||
"label-tag": this.createAutofillFieldLabelTag(element),
|
||||
@ -203,26 +341,87 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const autofillField = {
|
||||
...autofillFieldBase,
|
||||
...autofillFieldLabels,
|
||||
rel: this.getPropertyOrAttribute(element, "rel"),
|
||||
type: elementType,
|
||||
value: this.getElementValue(element),
|
||||
checked: Boolean(this.getPropertyOrAttribute(element, "checked")),
|
||||
autoCompleteType: autoCompleteType !== "off" ? autoCompleteType : null,
|
||||
disabled: Boolean(this.getPropertyOrAttribute(element, "disabled")),
|
||||
readonly: Boolean(this.getPropertyOrAttribute(element, "readOnly")),
|
||||
checked: this.getAttributeBoolean(element, "checked"),
|
||||
autoCompleteType: this.getAutoCompleteAttribute(element),
|
||||
disabled: this.getAttributeBoolean(element, "disabled"),
|
||||
readonly: this.getAttributeBoolean(element, "readonly"),
|
||||
selectInfo:
|
||||
element instanceof HTMLSelectElement ? this.getSelectElementOptions(element) : null,
|
||||
form: element.form ? this.getPropertyOrAttribute(element.form, "opid") : null,
|
||||
"aria-hidden": this.getPropertyOrAttribute(element, "aria-hidden") === "true",
|
||||
"aria-disabled": this.getPropertyOrAttribute(element, "aria-disabled") === "true",
|
||||
"aria-haspopup": this.getPropertyOrAttribute(element, "aria-haspopup") === "true",
|
||||
"aria-hidden": this.getAttributeBoolean(element, "aria-hidden", true),
|
||||
"aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
|
||||
};
|
||||
|
||||
this.autofillFieldElements.set(element, autofillField);
|
||||
return autofillField;
|
||||
};
|
||||
|
||||
/**
|
||||
* Identifies the autocomplete attribute associated with an element and returns
|
||||
* the value of the attribute if it is not set to "off".
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getAutoCompleteAttribute(element: ElementWithOpId<FormFieldElement>): string {
|
||||
const autoCompleteType =
|
||||
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocomplete");
|
||||
return autoCompleteType !== "off" ? autoCompleteType : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean representing the attribute value of an element.
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @param {string} attributeName
|
||||
* @param {boolean} checkString
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private getAttributeBoolean(
|
||||
element: ElementWithOpId<FormFieldElement>,
|
||||
attributeName: string,
|
||||
checkString = false
|
||||
): boolean {
|
||||
if (checkString) {
|
||||
return this.getPropertyOrAttribute(element, attributeName) === "true";
|
||||
}
|
||||
|
||||
return Boolean(this.getPropertyOrAttribute(element, attributeName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attribute of an element as a lowercase value.
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @param {string} attributeName
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getAttributeLowerCase(
|
||||
element: ElementWithOpId<FormFieldElement>,
|
||||
attributeName: string
|
||||
): string {
|
||||
return this.getPropertyOrAttribute(element, attributeName)?.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an element's property or attribute.
|
||||
* @returns {AutofillField[]}
|
||||
* @private
|
||||
*/
|
||||
private getFormattedAutofillFieldsData(): AutofillField[] {
|
||||
return Array.from(this.autofillFieldElements.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a label tag used to autofill the element pulled from a label
|
||||
* associated with the element's id, name, parent element or from an
|
||||
@ -235,13 +434,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
*/
|
||||
private createAutofillFieldLabelTag(element: FillableFormFieldElement): string {
|
||||
const labelElementsSet: Set<HTMLElement> = new Set(element.labels);
|
||||
|
||||
if (labelElementsSet.size) {
|
||||
return this.createLabelElementsTag(labelElementsSet);
|
||||
}
|
||||
|
||||
const labelElements: NodeListOf<HTMLLabelElement> | null = this.queryElementLabels(element);
|
||||
labelElements?.forEach((labelElement) => labelElementsSet.add(labelElement));
|
||||
for (let labelIndex = 0; labelIndex < labelElements?.length; labelIndex++) {
|
||||
labelElementsSet.add(labelElements[labelIndex]);
|
||||
}
|
||||
|
||||
let currentElement: HTMLElement | null = element;
|
||||
while (currentElement && currentElement !== document.documentElement) {
|
||||
@ -286,7 +486,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
return null;
|
||||
}
|
||||
|
||||
return document.querySelectorAll(labelQuerySelectors);
|
||||
return (element.getRootNode() as Document | ShadowRoot).querySelectorAll(
|
||||
labelQuerySelectors.replace(/\n/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,7 +499,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
* @private
|
||||
*/
|
||||
private createLabelElementsTag = (labelElementsSet: Set<HTMLElement>): string => {
|
||||
return [...labelElementsSet]
|
||||
return Array.from(labelElementsSet)
|
||||
.map((labelElement) => {
|
||||
const textContent: string | null = labelElement
|
||||
? labelElement.textContent || labelElement.innerText
|
||||
@ -561,7 +763,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
* @private
|
||||
*/
|
||||
private getSelectElementOptions(element: HTMLSelectElement): { options: (string | null)[][] } {
|
||||
const options = [...element.options].map((option) => {
|
||||
const options = Array.from(element.options).map((option) => {
|
||||
const optionText = option.text
|
||||
? String(option.text)
|
||||
.toLowerCase()
|
||||
@ -573,6 +775,425 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
|
||||
return { options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries all potential form and field elements from the DOM and returns
|
||||
* a collection of form and field elements. Leverages the TreeWalker API
|
||||
* to deep query Shadow DOM elements.
|
||||
* @returns {{formElements: Node[], formFieldElements: Node[]}}
|
||||
* @private
|
||||
*/
|
||||
private queryAutofillFormAndFieldElements(): {
|
||||
formElements: Node[];
|
||||
formFieldElements: Node[];
|
||||
} {
|
||||
const formElements: Node[] = [];
|
||||
const formFieldElements: Node[] = [];
|
||||
this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => {
|
||||
if (node instanceof HTMLFormElement) {
|
||||
formElements.push(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isNodeFormFieldElement(node)) {
|
||||
formFieldElements.push(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return { formElements, formFieldElements };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed node is a form field element.
|
||||
* @param {Node} node
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isNodeFormFieldElement(node: Node): boolean {
|
||||
const nodeIsSpanElementWithAutofillAttribute =
|
||||
node instanceof HTMLSpanElement && node.hasAttribute("data-bwautofill");
|
||||
|
||||
const ignoredInputTypes = new Set(["hidden", "submit", "reset", "button", "image", "file"]);
|
||||
const nodeIsValidInputElement =
|
||||
node instanceof HTMLInputElement && !ignoredInputTypes.has(node.type);
|
||||
|
||||
const nodeIsTextAreaOrSelectElement =
|
||||
node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
|
||||
|
||||
const nodeIsNonIgnoredFillableControlElement =
|
||||
(nodeIsTextAreaOrSelectElement || nodeIsValidInputElement) &&
|
||||
!node.hasAttribute("data-bwignore");
|
||||
|
||||
return nodeIsSpanElementWithAutofillAttribute || nodeIsNonIgnoredFillableControlElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the ShadowRoot of the passed node. If support for the
|
||||
* extension based openOrClosedShadowRoot API is available, it will be used.
|
||||
* @param {Node} node
|
||||
* @returns {ShadowRoot | null}
|
||||
* @private
|
||||
*/
|
||||
private getShadowRoot(node: Node): ShadowRoot | null {
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((chrome as any).dom?.openOrClosedShadowRoot) {
|
||||
return (chrome as any).dom.openOrClosedShadowRoot(node);
|
||||
}
|
||||
|
||||
return (node as any).openOrClosedShadowRoot || node.shadowRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds a collection of nodes that match the given filter callback.
|
||||
* If a node has a ShadowRoot, it will be observed for mutations.
|
||||
* @param {Node} rootNode
|
||||
* @param {Node[]} treeWalkerQueryResults
|
||||
* @param {Function} filterCallback
|
||||
* @param {boolean} isObservingShadowRoot
|
||||
* @private
|
||||
*/
|
||||
private buildTreeWalkerNodesQueryResults(
|
||||
rootNode: Node,
|
||||
treeWalkerQueryResults: Node[],
|
||||
filterCallback: CallableFunction,
|
||||
isObservingShadowRoot: boolean
|
||||
) {
|
||||
const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT);
|
||||
let currentNode = treeWalker?.currentNode;
|
||||
|
||||
while (currentNode) {
|
||||
if (filterCallback(currentNode)) {
|
||||
treeWalkerQueryResults.push(currentNode);
|
||||
}
|
||||
|
||||
const nodeShadowRoot = this.getShadowRoot(currentNode);
|
||||
if (nodeShadowRoot) {
|
||||
if (isObservingShadowRoot) {
|
||||
this.mutationObserver.observe(nodeShadowRoot, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.buildTreeWalkerNodesQueryResults(
|
||||
nodeShadowRoot,
|
||||
treeWalkerQueryResults,
|
||||
filterCallback,
|
||||
isObservingShadowRoot
|
||||
);
|
||||
}
|
||||
|
||||
currentNode = treeWalker?.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a mutation observer on the body of the document. Observes changes to
|
||||
* DOM elements to ensure we have an updated set of autofill field data.
|
||||
* @private
|
||||
*/
|
||||
private setupMutationObserver() {
|
||||
this.currentLocationHref = globalThis.location.href;
|
||||
this.mutationObserver = new MutationObserver(this.handleMutationObserverMutation);
|
||||
this.mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles observed DOM mutations and identifies if a mutation is related to
|
||||
* an autofill element. If so, it will update the autofill element data.
|
||||
* @param {MutationRecord[]} mutations
|
||||
* @private
|
||||
*/
|
||||
private handleMutationObserverMutation = (mutations: MutationRecord[]) => {
|
||||
if (this.currentLocationHref !== globalThis.location.href) {
|
||||
this.handleWindowLocationMutation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (let mutationsIndex = 0; mutationsIndex < mutations.length; mutationsIndex++) {
|
||||
const mutation = mutations[mutationsIndex];
|
||||
if (
|
||||
mutation.type === "childList" &&
|
||||
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
|
||||
this.isAutofillElementNodeMutated(mutation.addedNodes))
|
||||
) {
|
||||
this.domRecentlyMutated = true;
|
||||
this.noFieldsFound = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mutation.type === "attributes") {
|
||||
this.handleAutofillElementAttributeMutation(mutation);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.domRecentlyMutated) {
|
||||
this.updateAutofillElementsAfterMutation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a mutation to the window location. Clears the autofill elements
|
||||
* and updates the autofill elements after a timeout.
|
||||
* @private
|
||||
*/
|
||||
private handleWindowLocationMutation() {
|
||||
this.currentLocationHref = globalThis.location.href;
|
||||
|
||||
this.domRecentlyMutated = true;
|
||||
this.noFieldsFound = false;
|
||||
|
||||
this.autofillFormElements.clear();
|
||||
this.autofillFieldElements.clear();
|
||||
|
||||
this.updateAutofillElementsAfterMutation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed nodes either contain or are autofill elements.
|
||||
* @param {NodeList} nodes
|
||||
* @param {boolean} isRemovingNodes
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isAutofillElementNodeMutated(nodes: NodeList, isRemovingNodes = false): boolean {
|
||||
if (!nodes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let isElementMutated = false;
|
||||
const mutatedElements = [];
|
||||
for (let index = 0; index < nodes.length; index++) {
|
||||
const node = nodes[index];
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node instanceof HTMLFormElement || this.isNodeFormFieldElement(node)) {
|
||||
isElementMutated = true;
|
||||
mutatedElements.push(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
const childNodes = this.queryAllTreeWalkerNodes(
|
||||
node,
|
||||
(node: Node) => node instanceof HTMLFormElement || this.isNodeFormFieldElement(node)
|
||||
) as HTMLElement[];
|
||||
if (childNodes.length) {
|
||||
isElementMutated = true;
|
||||
mutatedElements.push(...childNodes);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRemovingNodes) {
|
||||
for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) {
|
||||
this.deleteCachedAutofillElement(
|
||||
mutatedElements[elementIndex] as
|
||||
| ElementWithOpId<HTMLFormElement>
|
||||
| ElementWithOpId<FormFieldElement>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return isElementMutated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any cached autofill elements that have been
|
||||
* removed from the DOM.
|
||||
* @param {ElementWithOpId<HTMLFormElement> | ElementWithOpId<FormFieldElement>} element
|
||||
* @private
|
||||
*/
|
||||
private deleteCachedAutofillElement(
|
||||
element: ElementWithOpId<HTMLFormElement> | ElementWithOpId<FormFieldElement>
|
||||
) {
|
||||
if (element instanceof HTMLFormElement && this.autofillFormElements.has(element)) {
|
||||
this.autofillFormElements.delete(element);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autofillFieldElements.has(element)) {
|
||||
this.autofillFieldElements.delete(element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autofill elements after a DOM mutation has occurred.
|
||||
* Is debounced to prevent excessive updates.
|
||||
* @private
|
||||
*/
|
||||
private updateAutofillElementsAfterMutation() {
|
||||
if (this.updateAutofillElementsAfterMutationTimeout) {
|
||||
clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
|
||||
}
|
||||
|
||||
this.updateAutofillElementsAfterMutationTimeout = setTimeout(
|
||||
this.getPageDetails.bind(this),
|
||||
this.updateAfterMutationTimeoutDelay
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles observed DOM mutations related to an autofill element attribute.
|
||||
* @param {MutationRecord} mutation
|
||||
* @private
|
||||
*/
|
||||
private handleAutofillElementAttributeMutation(mutation: MutationRecord) {
|
||||
const targetElement = mutation.target;
|
||||
if (!(targetElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeName = mutation.attributeName?.toLowerCase();
|
||||
const autofillForm = this.autofillFormElements.get(
|
||||
targetElement as ElementWithOpId<HTMLFormElement>
|
||||
);
|
||||
|
||||
if (autofillForm) {
|
||||
this.updateAutofillFormElementData(
|
||||
attributeName,
|
||||
targetElement as ElementWithOpId<HTMLFormElement>,
|
||||
autofillForm
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const autofillField = this.autofillFieldElements.get(
|
||||
targetElement as ElementWithOpId<FormFieldElement>
|
||||
);
|
||||
if (!autofillField) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateAutofillFieldElementData(
|
||||
attributeName,
|
||||
targetElement as ElementWithOpId<FormFieldElement>,
|
||||
autofillField
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autofill form element data based on the passed attribute name.
|
||||
* @param {string} attributeName
|
||||
* @param {ElementWithOpId<HTMLFormElement>} element
|
||||
* @param {AutofillForm} dataTarget
|
||||
* @private
|
||||
*/
|
||||
private updateAutofillFormElementData(
|
||||
attributeName: string,
|
||||
element: ElementWithOpId<HTMLFormElement>,
|
||||
dataTarget: AutofillForm
|
||||
) {
|
||||
const updateAttribute = (dataTargetKey: string) => {
|
||||
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
|
||||
};
|
||||
const updateActions: Record<string, CallableFunction> = {
|
||||
action: () => (dataTarget.htmlAction = this.getFormActionAttribute(element)),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
method: () => updateAttribute("htmlMethod"),
|
||||
};
|
||||
|
||||
if (!updateActions[attributeName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateActions[attributeName]();
|
||||
this.autofillFormElements.set(element, dataTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autofill field element data based on the passed attribute name.
|
||||
* @param {string} attributeName
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @param {AutofillField} dataTarget
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async updateAutofillFieldElementData(
|
||||
attributeName: string,
|
||||
element: ElementWithOpId<FormFieldElement>,
|
||||
dataTarget: AutofillField
|
||||
) {
|
||||
const updateAttribute = (dataTargetKey: string) => {
|
||||
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
|
||||
};
|
||||
const updateActions: Record<string, CallableFunction> = {
|
||||
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
tabindex: () => updateAttribute("tabindex"),
|
||||
title: () => updateAttribute("tabindex"),
|
||||
rel: () => updateAttribute("rel"),
|
||||
tagname: () => (dataTarget.tagName = this.getAttributeLowerCase(element, "tagName")),
|
||||
type: () => (dataTarget.type = this.getAttributeLowerCase(element, "type")),
|
||||
value: () => (dataTarget.value = this.getElementValue(element)),
|
||||
checked: () => (dataTarget.checked = this.getAttributeBoolean(element, "checked")),
|
||||
disabled: () => (dataTarget.disabled = this.getAttributeBoolean(element, "disabled")),
|
||||
readonly: () => (dataTarget.readonly = this.getAttributeBoolean(element, "readonly")),
|
||||
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
"data-label": () => updateAttribute("label-data"),
|
||||
"aria-label": () => updateAttribute("label-aria"),
|
||||
"aria-hidden": () =>
|
||||
(dataTarget["aria-hidden"] = this.getAttributeBoolean(element, "aria-hidden", true)),
|
||||
"aria-disabled": () =>
|
||||
(dataTarget["aria-disabled"] = this.getAttributeBoolean(element, "aria-disabled", true)),
|
||||
"aria-haspopup": () =>
|
||||
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(element, "aria-haspopup", true)),
|
||||
"data-stripe": () => updateAttribute("data-stripe"),
|
||||
};
|
||||
|
||||
if (!updateActions[attributeName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateActions[attributeName]();
|
||||
|
||||
const visibilityAttributesSet = new Set(["class", "style"]);
|
||||
if (
|
||||
visibilityAttributesSet.has(attributeName) &&
|
||||
!dataTarget.htmlClass?.includes("com-bitwarden-browser-animated-fill")
|
||||
) {
|
||||
dataTarget.viewable = await this.domElementVisibilityService.isFormFieldViewable(element);
|
||||
}
|
||||
|
||||
this.autofillFieldElements.set(element, dataTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attribute value for the passed element, and returns it. If the dataTarget
|
||||
* and dataTargetKey are passed, it will set the value of the dataTarget[dataTargetKey].
|
||||
* @param UpdateAutofillDataAttributeParams
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private updateAutofillDataAttribute({
|
||||
element,
|
||||
attributeName,
|
||||
dataTarget,
|
||||
dataTargetKey,
|
||||
}: UpdateAutofillDataAttributeParams) {
|
||||
const attributeValue = this.getPropertyOrAttribute(element, attributeName);
|
||||
if (dataTarget && dataTargetKey) {
|
||||
dataTarget[dataTargetKey] = attributeValue;
|
||||
}
|
||||
|
||||
return attributeValue;
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectAutofillContentService;
|
||||
|
@ -13,7 +13,6 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac
|
||||
*/
|
||||
async isFormFieldViewable(element: FormFieldElement): Promise<boolean> {
|
||||
const elementBoundingClientRect = element.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
this.isElementOutsideViewportBounds(element, elementBoundingClientRect) ||
|
||||
this.isElementHiddenByCss(element)
|
||||
@ -176,7 +175,10 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac
|
||||
): boolean {
|
||||
const elementBoundingClientRect =
|
||||
targetElementBoundingClientRect || targetElement.getBoundingClientRect();
|
||||
const elementAtCenterPoint = targetElement.ownerDocument.elementFromPoint(
|
||||
const elementRootNode = targetElement.getRootNode();
|
||||
const rootElement =
|
||||
elementRootNode instanceof ShadowRoot ? elementRootNode : targetElement.ownerDocument;
|
||||
const elementAtCenterPoint = rootElement.elementFromPoint(
|
||||
elementBoundingClientRect.left + elementBoundingClientRect.width / 2,
|
||||
elementBoundingClientRect.top + elementBoundingClientRect.height / 2
|
||||
);
|
||||
|
@ -82,7 +82,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
if (
|
||||
!savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) ||
|
||||
window.location.protocol !== "http:" ||
|
||||
!document.querySelectorAll("input[type=password]")?.length
|
||||
!this.isPasswordFieldWithinDocument()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@ -95,6 +95,22 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
return !confirm(confirmationWarning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a password field within the current document. Includes
|
||||
* password fields that are present within the shadow DOM.
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isPasswordFieldWithinDocument(): boolean {
|
||||
return Boolean(
|
||||
this.collectAutofillContentService.queryAllTreeWalkerNodes(
|
||||
document.documentElement,
|
||||
(node: Node) => node instanceof HTMLInputElement && node.type === "password",
|
||||
false
|
||||
)?.length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking if the autofill is occurring within an untrusted iframe. If the page is within an untrusted iframe,
|
||||
* the user is prompted to confirm that they want to autofill on the page. If the user cancels the autofill,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2023.9.0",
|
||||
"version": "2023.9.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2023.9.0",
|
||||
"version": "2023.9.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
@ -199,7 +199,10 @@ export class BrowserApi {
|
||||
BrowserApi.removeTab(tabToClose.id);
|
||||
}
|
||||
|
||||
// Keep track of all the events registered in a Safari popup so we can remove
|
||||
// them when the popup gets unloaded, otherwise we cause a memory leak
|
||||
private static registeredMessageListeners: any[] = [];
|
||||
private static registeredStorageChangeListeners: any[] = [];
|
||||
|
||||
static messageListener(
|
||||
name: string,
|
||||
@ -207,21 +210,38 @@ export class BrowserApi {
|
||||
) {
|
||||
chrome.runtime.onMessage.addListener(callback);
|
||||
|
||||
// Keep track of all the events registered in a Safari popup so we can remove
|
||||
// them when the popup gets unloaded, otherwise we cause a memory leak
|
||||
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
|
||||
BrowserApi.registeredMessageListeners.push(callback);
|
||||
|
||||
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
|
||||
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
|
||||
window.onpagehide = () => {
|
||||
for (const callback of BrowserApi.registeredMessageListeners) {
|
||||
chrome.runtime.onMessage.removeListener(callback);
|
||||
}
|
||||
};
|
||||
BrowserApi.setupUnloadListeners();
|
||||
}
|
||||
}
|
||||
|
||||
static storageChangeListener(
|
||||
callback: Parameters<typeof chrome.storage.onChanged.addListener>[0]
|
||||
) {
|
||||
chrome.storage.onChanged.addListener(callback);
|
||||
|
||||
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
|
||||
BrowserApi.registeredStorageChangeListeners.push(callback);
|
||||
BrowserApi.setupUnloadListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the event to destroy all the listeners when the popup gets unloaded in Safari, otherwise we get a memory leak
|
||||
private static setupUnloadListeners() {
|
||||
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
|
||||
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
|
||||
window.onpagehide = () => {
|
||||
for (const callback of BrowserApi.registeredMessageListeners) {
|
||||
chrome.runtime.onMessage.removeListener(callback);
|
||||
}
|
||||
|
||||
for (const callback of BrowserApi.registeredStorageChangeListeners) {
|
||||
chrome.storage.onChanged.removeListener(callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static sendMessage(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
return chrome.runtime.sendMessage(message);
|
||||
|
@ -14,6 +14,7 @@ import { Account } from "../../models/account";
|
||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
|
||||
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
|
||||
|
||||
import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service";
|
||||
@ -56,7 +57,7 @@ export class BrowserStateService
|
||||
// the background page that can get out of sync. We need to work out the
|
||||
// best way to handle caching with multiple instances of the state service.
|
||||
if (useAccountCache) {
|
||||
chrome.storage.onChanged.addListener((changes, namespace) => {
|
||||
BrowserApi.storageChangeListener((changes, namespace) => {
|
||||
if (namespace === "local") {
|
||||
for (const key of Object.keys(changes)) {
|
||||
if (key !== "accountActivity" && this.accountDiskCache.value[key]) {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import "core-js/stable";
|
||||
import "date-input-polyfill";
|
||||
import "zone.js";
|
||||
|
||||
import "../platform/polyfills/zone-patch-chrome-runtime";
|
||||
|
@ -96,9 +96,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
date-input-polyfill {
|
||||
&[data-open="true"] {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
}
|
||||
|
@ -12,3 +12,4 @@
|
||||
@import "environment.scss";
|
||||
@import "pages.scss";
|
||||
@import "@angular/cdk/overlay-prebuilt.css";
|
||||
@import "../../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||
|
@ -75,7 +75,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box" *ngIf="!comingFromAddEdit">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" role="radiogroup" aria-labelledby="typeHeading">
|
||||
<label id="typeHeading" class="radio-header">{{
|
||||
@ -90,7 +90,6 @@
|
||||
[value]="o.value"
|
||||
(change)="typeChanged()"
|
||||
[checked]="type === o.value"
|
||||
[disabled]="comingFromAddEdit && type !== o.value"
|
||||
/>
|
||||
<label for="type_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
|
@ -564,6 +564,7 @@
|
||||
<select
|
||||
id="autofillOnPageLoad"
|
||||
name="AutofillOnPageLoad"
|
||||
[disabled]="reprompt"
|
||||
[(ngModel)]="cipher.login.autofillOnPageLoad"
|
||||
>
|
||||
<option *ngFor="let o of autofillOnPageLoadOptions" [ngValue]="o.value">
|
||||
@ -572,6 +573,9 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer !tw-mb-0 !tw-pb-0" *ngIf="reprompt">
|
||||
{{ "turnOffMasterPasswordPromptToEditField" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
|
@ -264,4 +264,27 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
repromptChanged() {
|
||||
super.repromptChanged();
|
||||
|
||||
if (!this.showAutoFillOnPageLoadOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.reprompt) {
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("autofillOnPageLoadSetToDefault")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -209,6 +209,11 @@ export class LoginCommand {
|
||||
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||
);
|
||||
}
|
||||
if (response.requiresEncryptionKeyMigration) {
|
||||
return Response.error(
|
||||
"Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLogInCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2023.9.0",
|
||||
"version": "2023.9.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user