mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 02:19:18 +00:00
merged with master and fixed conflicts
This commit is contained in:
44
.github/workflows/build-browser.yml
vendored
44
.github/workflows/build-browser.yml
vendored
@@ -38,7 +38,7 @@ defaults:
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
repo_url: ${{ steps.gen_vars.outputs.repo_url }}
|
||||
adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
locales-test:
|
||||
name: Locales Test
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
defaults:
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-2019
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
- locales-test
|
||||
@@ -137,6 +137,7 @@ jobs:
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
node-gyp --version
|
||||
|
||||
- name: NPM setup
|
||||
run: npm ci
|
||||
@@ -152,24 +153,27 @@ jobs:
|
||||
run: gulp ci
|
||||
|
||||
- name: Build sources for reviewers
|
||||
shell: cmd
|
||||
run: |
|
||||
REM Remove ".git" directory
|
||||
rmdir /S /Q ".git"
|
||||
# Include hidden files in glob copy
|
||||
shopt -s dotglob
|
||||
|
||||
REM Copy root level files to source directory
|
||||
# Remove ".git" directory
|
||||
rm -r .git
|
||||
|
||||
# Copy root level files to source directory
|
||||
mkdir browser-source
|
||||
copy * browser-source
|
||||
FILES=$(find . -maxdepth 1 -type f)
|
||||
for FILE in $FILES; do cp "$FILE" browser-source/; done
|
||||
|
||||
REM Copy apps\browser to Browser source directory
|
||||
mkdir browser-source\apps\browser
|
||||
xcopy apps\browser\* browser-source\apps\browser /E
|
||||
# Copy apps/browser to Browser source directory
|
||||
mkdir -p browser-source/apps/browser
|
||||
cp -r apps/browser/* browser-source/apps/browser
|
||||
|
||||
REM Copy libs to Browser source directory
|
||||
mkdir browser-source\libs
|
||||
xcopy libs\* browser-source\libs /E
|
||||
# Copy libs to Browser source directory
|
||||
mkdir browser-source/libs
|
||||
cp -r libs/* browser-source/libs
|
||||
|
||||
call 7z a browser-source.zip "browser-source\*"
|
||||
zip -r browser-source.zip browser-source
|
||||
working-directory: ./
|
||||
|
||||
- name: Upload Opera artifact
|
||||
@@ -339,7 +343,7 @@ jobs:
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build
|
||||
- build-safari
|
||||
@@ -354,7 +358,7 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
@@ -374,7 +378,7 @@ jobs:
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- cloc
|
||||
- setup
|
||||
@@ -416,7 +420,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@37ffa14164a7308bc273829edfe75c97cd562375
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
||||
8
.github/workflows/release-browser.yml
vendored
8
.github/workflows/release-browser.yml
vendored
@@ -22,7 +22,7 @@ defaults:
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
@@ -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@58a2fdfbd3f1fc7e6727bc5dc51d159f4df07072
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
locales-test:
|
||||
name: Locales Test
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
- locales-test
|
||||
|
||||
40
.github/workflows/release-desktop.yml
vendored
40
.github/workflows/release-desktop.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Branch check
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then
|
||||
echo "==================================="
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
release-type: ${{ inputs.release_type }}
|
||||
project-type: ts
|
||||
file: apps/desktop/src/package.json
|
||||
monorepo: true
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
esac
|
||||
|
||||
- name: Create GitHub deployment
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
id: deployment
|
||||
with:
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
cf-prod-account"
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
path: apps/desktop/artifacts
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -146,17 +146,17 @@ jobs:
|
||||
run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive
|
||||
|
||||
- name: Set staged rollout percentage
|
||||
if: ${{ github.event.inputs.electron_publish }}
|
||||
if: ${{ inputs.electron_publish == 'true' }}
|
||||
env:
|
||||
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}
|
||||
ROLLOUT_PCT: ${{ github.event.inputs.rollout_percentage }}
|
||||
ROLLOUT_PCT: ${{ inputs.rollout_percentage }}
|
||||
run: |
|
||||
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml
|
||||
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml
|
||||
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml
|
||||
|
||||
- name: Publish artifacts to S3
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' && 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: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' && 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 }}
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@a2e71bdd4e7dab70ca26a852f29600c98b33153e # v1.12.0
|
||||
if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' && inputs.github_release }}
|
||||
if: ${{ steps.release-channel.outputs.channel == 'latest' && inputs.release_type != 'Dry Run' && 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: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||
if: ${{ 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: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||
if: ${{ 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
|
||||
if: ${{ inputs.snap_publish == 'true' }}
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
steps:
|
||||
@@ -278,7 +278,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Download Snap artifact
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Dry Run - Download Snap artifact
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Deploy to Snap Store
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ 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
|
||||
if: ${{ inputs.choco_publish == 'true' }}
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
steps:
|
||||
@@ -346,7 +346,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Download choco artifact
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -356,7 +356,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Dry Run - Download choco artifact
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
workflow: build-desktop.yml
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
path: apps/desktop/dist
|
||||
|
||||
- name: Push to Chocolatey
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
shell: pwsh
|
||||
run: choco push --source=https://push.chocolatey.org/
|
||||
working-directory: apps/desktop/dist
|
||||
|
||||
3
.github/workflows/version-auto-bump.yml
vendored
3
.github/workflows/version-auto-bump.yml
vendored
@@ -44,4 +44,5 @@ jobs:
|
||||
uses: ./.github/workflows/version-bump.yml
|
||||
with:
|
||||
version_number: ${{ needs.setup.outputs.version_number }}
|
||||
client: "Desktop"
|
||||
bump_desktop: true
|
||||
secrets: inherit
|
||||
|
||||
113
.github/workflows/version-bump.yml
vendored
113
.github/workflows/version-bump.yml
vendored
@@ -4,16 +4,22 @@ name: Version Bump
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
client:
|
||||
description: "Client Project"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- Browser
|
||||
- CLI
|
||||
- Desktop
|
||||
- Web
|
||||
- All
|
||||
bump_browser:
|
||||
description: "Browser Project Version Bump"
|
||||
type: boolean
|
||||
default: false
|
||||
bump_cli:
|
||||
description: "CLI Project Version Bump"
|
||||
type: boolean
|
||||
default: false
|
||||
bump_desktop:
|
||||
description: "Desktop Project Version Bump"
|
||||
type: boolean
|
||||
default: false
|
||||
bump_web:
|
||||
description: "Web Project Version Bump"
|
||||
type: boolean
|
||||
default: false
|
||||
version_number:
|
||||
description: "New Version"
|
||||
required: true
|
||||
@@ -23,9 +29,10 @@ on:
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
client:
|
||||
required: true
|
||||
type: string
|
||||
bump_desktop:
|
||||
description: "Desktop Project Version Bump"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -33,8 +40,8 @@ defaults:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: "Bump ${{ github.event.inputs.client }} Version"
|
||||
runs-on: ubuntu-20.04
|
||||
name: "Bump Version"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
@@ -42,7 +49,7 @@ jobs:
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -62,13 +69,27 @@ jobs:
|
||||
- name: Create Version Branch
|
||||
id: branch
|
||||
env:
|
||||
CLIENT_NAME: ${{ github.event.inputs.client }}
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
CLIENT=$(python -c "print('$CLIENT_NAME'.lower())")
|
||||
echo "client=$CLIENT" >> $GITHUB_OUTPUT
|
||||
CLIENTS=()
|
||||
if [[ ${{ inputs.bump_browser }} == true ]]; then
|
||||
CLIENTS+=("browser")
|
||||
fi
|
||||
if [[ ${{ inputs.bump_cli }} == true ]]; then
|
||||
CLIENTS+=("cli")
|
||||
fi
|
||||
if [[ ${{ inputs.bump_desktop }} == true ]]; then
|
||||
CLIENTS+=("desktop")
|
||||
fi
|
||||
if [[ ${{ inputs.bump_web }} == true ]]; then
|
||||
CLIENTS+=("web")
|
||||
fi
|
||||
printf -v joined '%s,' "${CLIENTS[@]}"
|
||||
echo "client=${joined%,}" >> $GITHUB_OUTPUT
|
||||
|
||||
git switch -c ${CLIENT}_version_bump_${VERSION}
|
||||
BRANCH=version_bump_${VERSION}_${GITHUB_SHA:0:7}
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
git switch -c ${BRANCH}
|
||||
|
||||
########################
|
||||
# VERSION BUMP SECTION #
|
||||
@@ -76,27 +97,27 @@ jobs:
|
||||
|
||||
### Browser
|
||||
- name: Bump Browser Version
|
||||
if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: npm version --workspace=@bitwarden/browser ${VERSION}
|
||||
|
||||
- name: Bump Browser Version - Manifest
|
||||
if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "apps/browser/src/manifest.json"
|
||||
|
||||
- name: Bump Browser Version - Manifest v3
|
||||
if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "apps/browser/src/manifest.v3.json"
|
||||
|
||||
- name: Run Prettier after Browser Version Bump
|
||||
if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_browser == true }}
|
||||
run: |
|
||||
npm install -g prettier
|
||||
prettier --write apps/browser/src/manifest.json
|
||||
@@ -104,30 +125,30 @@ jobs:
|
||||
|
||||
### CLI
|
||||
- name: Bump CLI Version
|
||||
if: ${{ github.event.inputs.client == 'CLI' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_cli == true }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: npm version --workspace=@bitwarden/cli ${VERSION}
|
||||
|
||||
### Desktop
|
||||
- name: Bump Desktop Version - Root
|
||||
if: ${{ github.event.inputs.client == 'Desktop' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_desktop == true }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: npm version --workspace=@bitwarden/desktop ${VERSION}
|
||||
|
||||
- name: Bump Desktop Version - App
|
||||
if: ${{ github.event.inputs.client == 'Desktop' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_desktop == true }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: npm version ${VERSION}
|
||||
working-directory: "apps/desktop/src"
|
||||
|
||||
### Web
|
||||
- name: Bump Web Version
|
||||
if: ${{ github.event.inputs.client == 'Web' || github.event.inputs.client == 'All' }}
|
||||
if: ${{ inputs.bump_web == true }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: npm version --workspace=@bitwarden/web-vault ${VERSION}
|
||||
|
||||
########################
|
||||
@@ -151,27 +172,26 @@ jobs:
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
CLIENT: ${{ steps.branch.outputs.client }}
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
VERSION: ${{ inputs.version_number }}
|
||||
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
CLIENT: ${{ steps.branch.outputs.client }}
|
||||
VERSION: ${{ github.event.inputs.version_number }}
|
||||
run: git push -u origin ${CLIENT}_version_bump_${VERSION}
|
||||
BRANCH: ${{ steps.branch.outputs.branch }}
|
||||
run: git push -u origin ${BRANCH}
|
||||
|
||||
- name: Create Bump Version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
PR_BRANCH: "${{ steps.branch.outputs.client }}_version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
BASE_BRANCH: master
|
||||
TITLE: "Bump ${{ github.event.inputs.client }} version to ${{ github.event.inputs.version_number }}"
|
||||
BRANCH: ${{ steps.branch.outputs.branch }}
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
TITLE: "Bump ${{ steps.branch.outputs.client }} version to ${{ inputs.version_number }}"
|
||||
run: |
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$BASE" \
|
||||
--head "$PR_BRANCH" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
--body "
|
||||
@@ -183,5 +203,4 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated ${{ github.event.inputs.client }} version bump to ${{ github.event.inputs.version_number }}"
|
||||
|
||||
Automated ${{ steps.branch.outputs.client }} version bump to ${{ inputs.version_number }}"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2023.8.2",
|
||||
"version": "2023.8.3",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
|
||||
"build:watch": "webpack --watch",
|
||||
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
|
||||
"build:watch:autofill": "cross-env AUTOFILL_VERSION=2 webpack --watch",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
||||
"dist": "npm run build:prod && gulp dist",
|
||||
@@ -19,6 +18,7 @@
|
||||
"dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev",
|
||||
"dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --coverageDirectory=coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"test:watch:all": "jest --watchAll"
|
||||
}
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "خيارات تسجيل الدخول الإضافية من خطوتين مثل YubiKey و FIDO U2F و Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "نظافة كلمة المرور، صحة الحساب، وتقارير خرق البيانات للحفاظ على سلامة خزنتك."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "إصدار الخادم"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "استضافة ذاتية"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi"
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey, FIDO U2F və Duo kimi iki mərhələli giriş seçimləri"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Anbarınızın təhlükəsiyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və verilənlərin pozulması hesabatları."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server Versiyası"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Öz-özünə sahiblik edən"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Üçüncü tərəf"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 ГБ зашыфраванага сховішча для далучаных файлаў."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Дадатковыя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Версія сервера"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Уласнае размяшчэнне"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Іншы пастаўшчык"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB пространство за файлове, които се шифрират."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Двустепенно удостоверяване чрез YubiKey, FIDO U2F и Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Частно двустепенно удостоверяване чрез YubiKey и Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Версия на сървъра"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Собствен хостинг"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "ফাইল সংযুক্তির জন্য ১ জিবি এনক্রিপ্টেড স্থান।"
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey, FIDO U2F, ও Duo এর মতো অতিরিক্ত দ্বি-পদক্ষেপ লগইন বিকল্পগুলি।"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "আপনার ভল্টটি সুরক্ষিত রাখতে পাসওয়ার্ড স্বাস্থ্যকরন, অ্যাকাউন্ট স্বাস্থ্য এবং ডেটা লঙ্ঘনের প্রতিবেদন।"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
"message": "Altres"
|
||||
},
|
||||
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
||||
"message": "Set up an unlock method to change your vault timeout action."
|
||||
"message": "Configura un mètode de desbloqueig per canviar l'acció del temps d'espera de la caixa forta."
|
||||
},
|
||||
"rateExtension": {
|
||||
"message": "Valora aquesta extensió"
|
||||
@@ -634,10 +634,10 @@
|
||||
"message": "Actualitza"
|
||||
},
|
||||
"notificationUnlockDesc": {
|
||||
"message": "Unlock your Bitwarden vault to complete the auto-fill request."
|
||||
"message": "Desbloquegeu la vostra caixa forta de Bitwarden per completar la sol·licitud d'emplenament automàtic."
|
||||
},
|
||||
"notificationUnlock": {
|
||||
"message": "Unlock"
|
||||
"message": "Desbloqueja"
|
||||
},
|
||||
"enableContextMenuItem": {
|
||||
"message": "Mostra les opcions del menú contextual"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Opcions addicionals d'inici de sessió en dues passes com ara YubiKey, FIDO U2F i Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Opcions propietàries de doble factor com ara YubiKey i Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Requisits d'higiene de la contrasenya, salut del compte i informe d'infraccions de dades per mantenir la seguretat de la vostra caixa forta."
|
||||
@@ -1606,10 +1606,10 @@
|
||||
"message": "La biometria del navegador no és compatible amb aquest dispositiu."
|
||||
},
|
||||
"biometricsFailedTitle": {
|
||||
"message": "Biometrics failed"
|
||||
"message": "La biometria ha fallat"
|
||||
},
|
||||
"biometricsFailedDesc": {
|
||||
"message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support."
|
||||
"message": "La biometria no es pot completar, considereu utilitzar una contrasenya mestra o tancar la sessió. Si això continua, poseu-vos en contacte amb el servei d'assistència de Bitwarden."
|
||||
},
|
||||
"nativeMessaginPermissionErrorTitle": {
|
||||
"message": "No s'ha proporcionat el permís"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versió del servidor"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Autoallotjat"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tercers"
|
||||
@@ -2153,7 +2153,7 @@
|
||||
"message": "S'ha enviat una notificació al vostre dispositiu."
|
||||
},
|
||||
"loginInitiated": {
|
||||
"message": "Login initiated"
|
||||
"message": "S'ha iniciat la sessió"
|
||||
},
|
||||
"exposedMasterPassword": {
|
||||
"message": "Contrasenya mestra exposada"
|
||||
@@ -2234,34 +2234,34 @@
|
||||
}
|
||||
},
|
||||
"loggingInOn": {
|
||||
"message": "Logging in on"
|
||||
"message": "Inici de sessió en"
|
||||
},
|
||||
"opensInANewWindow": {
|
||||
"message": "S'obri en una finestra nova"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Device approval required. Select an approval option below:"
|
||||
"message": "Cal l'aprovació del dispositiu. Seleccioneu una opció d'aprovació a continuació:"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Remember this device"
|
||||
"message": "Recorda aquest dispositiu"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Uncheck if using a public device"
|
||||
"message": "Desmarqueu si utilitzeu un dispositiu públic"
|
||||
},
|
||||
"approveFromYourOtherDevice": {
|
||||
"message": "Approve from your other device"
|
||||
"message": "Aproveu des d'un altre dispositiu vostre"
|
||||
},
|
||||
"requestAdminApproval": {
|
||||
"message": "Request admin approval"
|
||||
"message": "Sol·liciteu l'aprovació de l'administrador"
|
||||
},
|
||||
"approveWithMasterPassword": {
|
||||
"message": "Approve with master password"
|
||||
"message": "Aprova amb contrasenya mestra"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization SSO identifier is required."
|
||||
"message": "Es requereix un identificador SSO de l'organització."
|
||||
},
|
||||
"eu": {
|
||||
"message": "EU",
|
||||
"message": "UE",
|
||||
"description": "European Union"
|
||||
},
|
||||
"usDomain": {
|
||||
@@ -2280,28 +2280,28 @@
|
||||
"message": "Mostra"
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Account successfully created!"
|
||||
"message": "Compte creat correctament!"
|
||||
},
|
||||
"adminApprovalRequested": {
|
||||
"message": "Admin approval requested"
|
||||
"message": "S'ha sol·licitat l'aprovació de l'administrador"
|
||||
},
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Your request has been sent to your admin."
|
||||
"message": "La vostra sol·licitud s'ha enviat a l'administrador."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
"message": "Se us notificarà una vegada aprovat."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
"message": "Teniu problemes per iniciar la sessió?"
|
||||
},
|
||||
"loginApproved": {
|
||||
"message": "Login approved"
|
||||
"message": "S'ha aprovat l'inici de sessió"
|
||||
},
|
||||
"userEmailMissing": {
|
||||
"message": "User email missing"
|
||||
"message": "Falta el correu electrònic de l'usuari"
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Device trusted"
|
||||
"message": "Dispositiu de confiança"
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "L'entrada és obligatòria."
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB šifrovaného úložiště pro přílohy."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Další možnosti dvoufázového přihlášení, jako je například YubiKey, FIDO U2F a Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Reporty o hygieně Vašich hesel, zdraví účtu a narušeních bezpečnosti."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Verze serveru"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Vlastní hosting"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tretí strana"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||
},
|
||||
"extDesc": {
|
||||
"message": "A secure and free password manager for all of your devices.",
|
||||
"message": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"loginOrCreateNewAccount": {
|
||||
@@ -29,7 +29,7 @@
|
||||
"message": "Cau"
|
||||
},
|
||||
"submit": {
|
||||
"message": "Submit"
|
||||
"message": "Cyflwyno"
|
||||
},
|
||||
"emailAddress": {
|
||||
"message": "Cyfeiriad ebost"
|
||||
@@ -227,10 +227,10 @@
|
||||
"message": "Cell we Bitwarden"
|
||||
},
|
||||
"importItems": {
|
||||
"message": "Import items"
|
||||
"message": "Mewnforio eitemau"
|
||||
},
|
||||
"select": {
|
||||
"message": "Select"
|
||||
"message": "Dewis"
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Cynhyrchu cyfrinair"
|
||||
@@ -345,7 +345,7 @@
|
||||
"message": "Rate the extension"
|
||||
},
|
||||
"rateExtensionDesc": {
|
||||
"message": "Please consider helping us out with a good review!"
|
||||
"message": "Ystyriwch ein helpu ni gydag adolygiad da!"
|
||||
},
|
||||
"browserNotSupportClipboard": {
|
||||
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead."
|
||||
@@ -360,7 +360,7 @@
|
||||
"message": "Datgloi"
|
||||
},
|
||||
"loggedInAsOn": {
|
||||
"message": "Logged in as $EMAIL$ on $HOSTNAME$.",
|
||||
"message": "Wedi mewngofnodi gyda $EMAIL$ ar $HOSTNAME$.",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@@ -379,7 +379,7 @@
|
||||
"message": "Cloi'r gell"
|
||||
},
|
||||
"lockNow": {
|
||||
"message": "Lock now"
|
||||
"message": "Cloi nawr"
|
||||
},
|
||||
"immediately": {
|
||||
"message": "ar unwaith"
|
||||
@@ -427,7 +427,7 @@
|
||||
"message": "Diogelwch"
|
||||
},
|
||||
"errorOccurred": {
|
||||
"message": "An error has occurred"
|
||||
"message": "Bu gwall"
|
||||
},
|
||||
"emailRequired": {
|
||||
"message": "Mae angen cyfeiriad ebost."
|
||||
@@ -513,13 +513,13 @@
|
||||
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
|
||||
},
|
||||
"editedFolder": {
|
||||
"message": "Folder saved"
|
||||
"message": "Ffolder wedi'i chadw"
|
||||
},
|
||||
"deleteFolderConfirmation": {
|
||||
"message": "Are you sure you want to delete this folder?"
|
||||
},
|
||||
"deletedFolder": {
|
||||
"message": "Folder deleted"
|
||||
"message": "Ffolder wedi'i dileu"
|
||||
},
|
||||
"gettingStartedTutorial": {
|
||||
"message": "Getting started tutorial"
|
||||
@@ -534,7 +534,7 @@
|
||||
"message": "Syncing failed"
|
||||
},
|
||||
"passwordCopied": {
|
||||
"message": "Password copied"
|
||||
"message": "Cyfrinair wedi'i gopïo"
|
||||
},
|
||||
"uri": {
|
||||
"message": "URI"
|
||||
@@ -553,10 +553,10 @@
|
||||
"message": "URI newydd"
|
||||
},
|
||||
"addedItem": {
|
||||
"message": "Item added"
|
||||
"message": "Eitem wedi'i hychwanegu"
|
||||
},
|
||||
"editedItem": {
|
||||
"message": "Item saved"
|
||||
"message": "Eitem wedi'i chadw"
|
||||
},
|
||||
"deleteItemConfirmation": {
|
||||
"message": "Ydych chi wir eisiau anfon i'r sbwriel?"
|
||||
@@ -565,13 +565,13 @@
|
||||
"message": "Anfonwyd yr eitem i'r sbwriel"
|
||||
},
|
||||
"overwritePassword": {
|
||||
"message": "Overwrite password"
|
||||
"message": "Trosysgrifo'r cyfrinair"
|
||||
},
|
||||
"overwritePasswordConfirmation": {
|
||||
"message": "Are you sure you want to overwrite the current password?"
|
||||
},
|
||||
"overwriteUsername": {
|
||||
"message": "Overwrite username"
|
||||
"message": "Trosysgrifo'r enw defnyddiwr"
|
||||
},
|
||||
"overwriteUsernameConfirmation": {
|
||||
"message": "Are you sure you want to overwrite the current username?"
|
||||
@@ -608,7 +608,7 @@
|
||||
"message": "List identity items on the Tab page for easy auto-fill."
|
||||
},
|
||||
"clearClipboard": {
|
||||
"message": "Clear clipboard",
|
||||
"message": "Clirio'r clipfwrdd",
|
||||
"description": "Clipboard is the operating system thing where you copy/paste data to on your device."
|
||||
},
|
||||
"clearClipboardDesc": {
|
||||
@@ -637,7 +637,7 @@
|
||||
"message": "Unlock your Bitwarden vault to complete the auto-fill request."
|
||||
},
|
||||
"notificationUnlock": {
|
||||
"message": "Unlock"
|
||||
"message": "Datgloi"
|
||||
},
|
||||
"enableContextMenuItem": {
|
||||
"message": "Show context menu options"
|
||||
@@ -711,7 +711,7 @@
|
||||
"message": "Rhannu"
|
||||
},
|
||||
"movedItemToOrg": {
|
||||
"message": "$ITEMNAME$ moved to $ORGNAME$",
|
||||
"message": "Symudwyd $ITEMNAME$ i $ORGNAME$",
|
||||
"placeholders": {
|
||||
"itemname": {
|
||||
"content": "$1",
|
||||
@@ -751,10 +751,10 @@
|
||||
"message": "Attachment deleted"
|
||||
},
|
||||
"newAttachment": {
|
||||
"message": "Add new attachment"
|
||||
"message": "Ychwanegu atodiad newydd"
|
||||
},
|
||||
"noAttachments": {
|
||||
"message": "No attachments."
|
||||
"message": "Dim atodiadau."
|
||||
},
|
||||
"attachmentSaved": {
|
||||
"message": "Attachment saved"
|
||||
@@ -763,7 +763,7 @@
|
||||
"message": "Ffeil"
|
||||
},
|
||||
"selectFile": {
|
||||
"message": "Select a file"
|
||||
"message": "Dewis ffeil"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "Maximum file size is 500 MB."
|
||||
@@ -787,40 +787,40 @@
|
||||
"message": "Adnewyddu'ch aelodaeth"
|
||||
},
|
||||
"premiumNotCurrentMember": {
|
||||
"message": "You are not currently a Premium member."
|
||||
"message": "Does gennych chi ddim aeloaeth uwch ar hyn o bryd."
|
||||
},
|
||||
"premiumSignUpAndGet": {
|
||||
"message": "Sign up for a Premium membership and get:"
|
||||
"message": "Cofrestrwch ar gyfer aelodaeth uwch i gael:"
|
||||
},
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
"message": "Storfa 1GB wedi'i hamgryptio ar gyfer atodiadau ffeiliau."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Dewisiadau mewngofnodi dau gam perchenogol megis YubiKey a Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
},
|
||||
"ppremiumSignUpTotp": {
|
||||
"message": "TOTP verification code (2FA) generator for logins in your vault."
|
||||
"message": "Cynhyrchydd codau dilysu TOTP (2FA) ar gyfer manylion mewngofnodi yn eich cell."
|
||||
},
|
||||
"ppremiumSignUpSupport": {
|
||||
"message": "Priority customer support."
|
||||
"message": "Cymorth wedi'i flaenoriaethu."
|
||||
},
|
||||
"ppremiumSignUpFuture": {
|
||||
"message": "All future Premium features. More coming soon!"
|
||||
},
|
||||
"premiumPurchase": {
|
||||
"message": "Purchase Premium"
|
||||
"message": "Prynu aelodaeth uwch"
|
||||
},
|
||||
"premiumPurchaseAlert": {
|
||||
"message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?"
|
||||
},
|
||||
"premiumCurrentMember": {
|
||||
"message": "You are a Premium member!"
|
||||
"message": "Mae gennych aelodaeth uwch!"
|
||||
},
|
||||
"premiumCurrentMemberThanks": {
|
||||
"message": "Thank you for supporting Bitwarden."
|
||||
"message": "Diolch am gefnogi Bitwarden."
|
||||
},
|
||||
"premiumPrice": {
|
||||
"message": "Hyn oll am $PRICE$ y flwyddyn!",
|
||||
@@ -844,10 +844,10 @@
|
||||
"message": "Ask for biometrics on launch"
|
||||
},
|
||||
"premiumRequired": {
|
||||
"message": "Premium required"
|
||||
"message": "Mae angen aelodaeth uwch"
|
||||
},
|
||||
"premiumRequiredDesc": {
|
||||
"message": "A Premium membership is required to use this feature."
|
||||
"message": "Mae angen aelodaeth uwch i ddefnyddio'r nodwedd hon."
|
||||
},
|
||||
"enterVerificationCodeApp": {
|
||||
"message": "Enter the 6 digit verification code from your authenticator app."
|
||||
@@ -904,7 +904,7 @@
|
||||
"message": "Please use a supported web browser (such as Chrome) and/or add additional providers that are better supported across web browsers (such as an authenticator app)."
|
||||
},
|
||||
"twoStepOptions": {
|
||||
"message": "Two-step login options"
|
||||
"message": "Dewisiadau mewngofnodi dau gam"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers from your account."
|
||||
@@ -1033,7 +1033,7 @@
|
||||
"message": "Copy value"
|
||||
},
|
||||
"value": {
|
||||
"message": "Value"
|
||||
"message": "Gwerth"
|
||||
},
|
||||
"newCustomField": {
|
||||
"message": "Maes addasedig newydd"
|
||||
@@ -1048,7 +1048,7 @@
|
||||
"message": "Hidden"
|
||||
},
|
||||
"cfTypeBoolean": {
|
||||
"message": "Boolean"
|
||||
"message": "Gwerth Boole"
|
||||
},
|
||||
"cfTypeLinked": {
|
||||
"message": "Linked",
|
||||
@@ -1134,7 +1134,7 @@
|
||||
"message": "Cod diogelwch"
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex."
|
||||
"message": "engh."
|
||||
},
|
||||
"title": {
|
||||
"message": "Teitl"
|
||||
@@ -1297,7 +1297,7 @@
|
||||
"message": "Starts with"
|
||||
},
|
||||
"regEx": {
|
||||
"message": "Regular expression",
|
||||
"message": "Mynegiant rheolaidd",
|
||||
"description": "A programming term, also known as 'RegEx'."
|
||||
},
|
||||
"matchDetection": {
|
||||
@@ -1427,7 +1427,7 @@
|
||||
"message": "Vault timeout action"
|
||||
},
|
||||
"lock": {
|
||||
"message": "Lock",
|
||||
"message": "Cloi",
|
||||
"description": "Verb form: to make secure or inaccesible by"
|
||||
},
|
||||
"trash": {
|
||||
@@ -1438,13 +1438,13 @@
|
||||
"message": "Chwilio drwy'r sbwriel"
|
||||
},
|
||||
"permanentlyDeleteItem": {
|
||||
"message": "Permanently delete item"
|
||||
"message": "Dileu'r eitem yn barhaol"
|
||||
},
|
||||
"permanentlyDeleteItemConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this item?"
|
||||
},
|
||||
"permanentlyDeletedItem": {
|
||||
"message": "Item permanently deleted"
|
||||
"message": "Eitem wedi'i dileu'n barhaol"
|
||||
},
|
||||
"restoreItem": {
|
||||
"message": "Adfer yr eitem"
|
||||
@@ -1546,7 +1546,7 @@
|
||||
"message": "Terms of Service and Privacy Policy have not been acknowledged."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Terms of Service"
|
||||
"message": "Telerau gwasanaeth"
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"message": "Polisi preifatrwydd"
|
||||
@@ -1671,7 +1671,7 @@
|
||||
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
|
||||
},
|
||||
"expired": {
|
||||
"message": "Expired"
|
||||
"message": "Wedi dod i ben"
|
||||
},
|
||||
"pendingDeletion": {
|
||||
"message": "Pending deletion"
|
||||
@@ -1744,10 +1744,10 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"oneDay": {
|
||||
"message": "1 day"
|
||||
"message": "1 diwrnod"
|
||||
},
|
||||
"days": {
|
||||
"message": "$DAYS$ days",
|
||||
"message": "$DAYS$ o ddyddiau",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
@@ -1854,7 +1854,7 @@
|
||||
"message": "There was an error saving your deletion and expiration dates."
|
||||
},
|
||||
"hideEmail": {
|
||||
"message": "Hide my email address from recipients."
|
||||
"message": "Cuddio fy nghyfeiriad ebost rhag derbynwyr."
|
||||
},
|
||||
"sendOptionsPolicyInEffect": {
|
||||
"message": "One or more organization policies are affecting your Send options."
|
||||
@@ -2007,10 +2007,10 @@
|
||||
"message": "Regenerate username"
|
||||
},
|
||||
"generateUsername": {
|
||||
"message": "Generate username"
|
||||
"message": "Cynhyrchu enw defnyddiwr"
|
||||
},
|
||||
"usernameType": {
|
||||
"message": "Username type"
|
||||
"message": "Math o enw defnyddiwr"
|
||||
},
|
||||
"plusAddressedEmail": {
|
||||
"message": "Plus addressed email",
|
||||
@@ -2026,10 +2026,10 @@
|
||||
"message": "Use your domain's configured catch-all inbox."
|
||||
},
|
||||
"random": {
|
||||
"message": "Random"
|
||||
"message": "Hap"
|
||||
},
|
||||
"randomWord": {
|
||||
"message": "Random word"
|
||||
"message": "Gair ar hap"
|
||||
},
|
||||
"websiteName": {
|
||||
"message": "Website name"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
@@ -2129,7 +2129,7 @@
|
||||
"message": "New around here?"
|
||||
},
|
||||
"rememberEmail": {
|
||||
"message": "Remember email"
|
||||
"message": "Cofio'r ebost"
|
||||
},
|
||||
"loginWithDevice": {
|
||||
"message": "Mewngofnodi â dyfais"
|
||||
@@ -2165,19 +2165,19 @@
|
||||
"message": "Weak and Exposed Master Password"
|
||||
},
|
||||
"weakAndBreachedMasterPasswordDesc": {
|
||||
"message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?"
|
||||
"message": "Cyfrinair gwan a gafodd ei ganfod mewn achos o ddatgelu data. Defnyddiwch gyfrinair cryf ac unigryw i ddiogelu eich cyfrif. Ydych chi wir eisiau defnyddio cyfrinair sydd wedi'i ddatgelu?"
|
||||
},
|
||||
"checkForBreaches": {
|
||||
"message": "Check known data breaches for this password"
|
||||
"message": "Chwilio am achosion o ddatgelu data sy'n cynnwys y cyfrinair hwn"
|
||||
},
|
||||
"important": {
|
||||
"message": "Pwysig:"
|
||||
},
|
||||
"masterPasswordHint": {
|
||||
"message": "Your master password cannot be recovered if you forget it!"
|
||||
"message": "Allwch chi ddim adfer eich prif gyfrinair os caiff ei anghofio!"
|
||||
},
|
||||
"characterMinimum": {
|
||||
"message": "$LENGTH$ character minimum",
|
||||
"message": "Isafswm o $LENGTH$ nod",
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"content": "$1",
|
||||
@@ -2243,7 +2243,7 @@
|
||||
"message": "Device approval required. Select an approval option below:"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Remember this device"
|
||||
"message": "Cofio'r ddyfais hon"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Uncheck if using a public device"
|
||||
@@ -2261,7 +2261,7 @@
|
||||
"message": "Organization SSO identifier is required."
|
||||
},
|
||||
"eu": {
|
||||
"message": "EU",
|
||||
"message": "UE",
|
||||
"description": "European Union"
|
||||
},
|
||||
"usDomain": {
|
||||
@@ -2310,7 +2310,7 @@
|
||||
"message": "required"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search"
|
||||
"message": "Chwilio"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "Input must be at least $COUNT$ characters long.",
|
||||
@@ -2377,19 +2377,19 @@
|
||||
}
|
||||
},
|
||||
"selectPlaceholder": {
|
||||
"message": "-- Select --"
|
||||
"message": "-- Dewis --"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to filter --"
|
||||
"message": "-- Teipiwch i hidlo --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Retrieving options..."
|
||||
"message": "Yn nôl dewisiadau..."
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "No items found"
|
||||
"message": "Heb ganfod eitemau"
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
"message": "Clirio'r cyfan"
|
||||
},
|
||||
"plusNMore": {
|
||||
"message": "+ $QUANTITY$ more",
|
||||
@@ -2401,7 +2401,7 @@
|
||||
}
|
||||
},
|
||||
"submenu": {
|
||||
"message": "Submenu"
|
||||
"message": "Is-ddewislen"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB krypteret lager til vedhæftede filer."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Yderligere to-trins login muligheder såsom YubiKey, FIDO U2F og Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Adgangskodehygiejne, kontosundhed og rapporter om datalæk til at holde din boks sikker."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Selv-hostet"
|
||||
"selfHostedServer": {
|
||||
"message": "selv-hostet"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tredjepart"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB verschlüsselter Speicherplatz für Dateianhänge."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Zusätzliche Zweifaktor-Anmeldung über YubiKey, FIDO U2F, und Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Berichte über Kennworthygiene, Kontostatus und Datenschutzverletzungen, um deinen Tresor sicher zu halten."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server-Version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Selbst gehostet"
|
||||
"selfHostedServer": {
|
||||
"message": "selbst gehostet"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Drittanbieter"
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
"message": "Άλλες"
|
||||
},
|
||||
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
||||
"message": "Set up an unlock method to change your vault timeout action."
|
||||
"message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου θησαυ/κιου."
|
||||
},
|
||||
"rateExtension": {
|
||||
"message": "Βαθμολογήστε την επέκταση"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey, το FIDO U2F και το Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Ασφάλεια κωδικών, υγεία λογαριασμού και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλές το vault σας."
|
||||
@@ -1438,7 +1438,7 @@
|
||||
"message": "Αναζήτηση Κάδου"
|
||||
},
|
||||
"permanentlyDeleteItem": {
|
||||
"message": "Μόνιμη Διαγραφή Αντικειμένου"
|
||||
"message": "Οριστική διαγραφή αντικειμένου"
|
||||
},
|
||||
"permanentlyDeleteItemConfirmation": {
|
||||
"message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε μόνιμα αυτό το στοιχείο;"
|
||||
@@ -1471,13 +1471,13 @@
|
||||
"message": "Προειδοποίηση: Αυτή είναι μια μη ασφαλή σελίδα HTTP και οποιαδήποτε πληροφορία υποβάλλετε μπορεί να γίνει ορατή και επεμβάσιμη από άλλους. Αυτή η σύνδεση αποθηκεύτηκε αρχικά σε μια ασφαλή (HTTPS) σελίδα."
|
||||
},
|
||||
"insecurePageWarningFillPrompt": {
|
||||
"message": "Do you still wish to fill this login?"
|
||||
"message": "Θέλετε ακόμα να συμπληρώσετε αυτή τη σύνδεση;"
|
||||
},
|
||||
"autofillIframeWarning": {
|
||||
"message": "Η φόρμα φιλοξενείται από διαφορετικό τομέα (domain) από το λινκ (uri) της αποθηκευμένης σύνδεσης σας (login). Επιλέξτε OK για αυτόματη συμπλήρωση, ή Ακύρωση για να σταματήσετε."
|
||||
},
|
||||
"autofillIframeWarningTip": {
|
||||
"message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.",
|
||||
"message": "Για να αποτρέψετε αυτή την προειδοποίηση στο μέλλον, αποθηκεύστε αυτό το URI, $HOSTNAME$, στο στοιχείο σύνδεσης Bitwarden σας για αυτόν τον ιστότοπο.",
|
||||
"placeholders": {
|
||||
"hostname": {
|
||||
"content": "$1",
|
||||
@@ -1486,13 +1486,13 @@
|
||||
}
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Ορισμός Κύριου Κωδικού"
|
||||
"message": "Καθορισμός κύριου κωδικού"
|
||||
},
|
||||
"currentMasterPass": {
|
||||
"message": "Τρέχων Κύριος Κωδικός"
|
||||
},
|
||||
"newMasterPass": {
|
||||
"message": "Νέος Κύριος Κωδικός"
|
||||
"message": "Νέος κύριος κωδικός"
|
||||
},
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Επιβεβαίωση Νέου Κύριου Κωδικού"
|
||||
@@ -1606,10 +1606,10 @@
|
||||
"message": "Τα βιομετρικά στοιχεία του προγράμματος περιήγησης δεν υποστηρίζονται σε αυτήν τη συσκευή."
|
||||
},
|
||||
"biometricsFailedTitle": {
|
||||
"message": "Biometrics failed"
|
||||
"message": "Ο βιομετρικός έλεγχος απέτυχε"
|
||||
},
|
||||
"biometricsFailedDesc": {
|
||||
"message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support."
|
||||
"message": "Τα βιομετρικά δεν μπόρεσαν να ολοκληρωθούν, σκεφτείτε να χρησιμοποιήσετε έναν κύριο κωδικό πρόσβασης ή να αποσυνδεθείτε. Αν αυτό εξακολουθεί να συμβαίνει, παρακαλώ επικοινωνήστε με την υποστήριξη της Bitwarden."
|
||||
},
|
||||
"nativeMessaginPermissionErrorTitle": {
|
||||
"message": "Δεν Έχει Χορηγηθεί Άδεια"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Έκδοση διακομιστή"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Αυτο-φιλοξενείται"
|
||||
"selfHostedServer": {
|
||||
"message": "αυτο-φιλοξενούμενο"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Τρίτο μέρος"
|
||||
@@ -2153,7 +2153,7 @@
|
||||
"message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας."
|
||||
},
|
||||
"loginInitiated": {
|
||||
"message": "Login initiated"
|
||||
"message": "Η σύνδεση ξεκίνησε"
|
||||
},
|
||||
"exposedMasterPassword": {
|
||||
"message": "Εκτεθειμένος Κύριος Κωδικός Πρόσβασης"
|
||||
@@ -2240,28 +2240,28 @@
|
||||
"message": "Ανοίγει σε νέο παράθυρο"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Device approval required. Select an approval option below:"
|
||||
"message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Remember this device"
|
||||
"message": "Απομνημόνευση αυτής της συσκευής"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Uncheck if using a public device"
|
||||
"message": "Αποεπιλέξτε αν γίνεται χρήση δημόσιας συσκευής"
|
||||
},
|
||||
"approveFromYourOtherDevice": {
|
||||
"message": "Approve from your other device"
|
||||
"message": "Έγκριση από άλλη συσκευή σας"
|
||||
},
|
||||
"requestAdminApproval": {
|
||||
"message": "Request admin approval"
|
||||
"message": "Αίτηση έγκρισης διαχειριστή"
|
||||
},
|
||||
"approveWithMasterPassword": {
|
||||
"message": "Approve with master password"
|
||||
"message": "Έγκριση με τον κύριο κωδικό"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization SSO identifier is required."
|
||||
"message": "Απαιτείται αναγνωριστικό οργανισμού SSO."
|
||||
},
|
||||
"eu": {
|
||||
"message": "EU",
|
||||
"message": "ΕΕ",
|
||||
"description": "European Union"
|
||||
},
|
||||
"usDomain": {
|
||||
@@ -2274,46 +2274,46 @@
|
||||
"message": "Δεν επιτρέπεται η πρόσβαση. Δεν έχετε άδεια για να δείτε αυτή τη σελίδα."
|
||||
},
|
||||
"general": {
|
||||
"message": "General"
|
||||
"message": "Γενικά"
|
||||
},
|
||||
"display": {
|
||||
"message": "Display"
|
||||
"message": "Εμφάνιση"
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Account successfully created!"
|
||||
"message": "Επιτυχής δημιουργία λογαριασμού!"
|
||||
},
|
||||
"adminApprovalRequested": {
|
||||
"message": "Admin approval requested"
|
||||
"message": "Ζητήθηκε έγκριση διαχειριστή"
|
||||
},
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Your request has been sent to your admin."
|
||||
"message": "Το αίτημά σας εστάλη στον διαχειριστή σας."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
"message": "Θα ειδοποιηθείτε μόλις εγκριθεί."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
"message": "Δεν μπορείτε να συνδεθείτε;"
|
||||
},
|
||||
"loginApproved": {
|
||||
"message": "Login approved"
|
||||
"message": "Η σύνδεση εγκρίθηκε"
|
||||
},
|
||||
"userEmailMissing": {
|
||||
"message": "User email missing"
|
||||
"message": "Το email του χρήστη απουσιάζει"
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Device trusted"
|
||||
"message": "Αξιόπιστη συσκευή"
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "Input is required."
|
||||
"message": "Απαιτείται εισαγωγή."
|
||||
},
|
||||
"required": {
|
||||
"message": "required"
|
||||
"message": "απαιτείται"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search"
|
||||
"message": "Αναζήτηση"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "Input must be at least $COUNT$ characters long.",
|
||||
"message": "Η καταχώρηση πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxLength": {
|
||||
"message": "Input must not exceed $COUNT$ characters in length.",
|
||||
"message": "Η καταχώρηση δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες σε μήκος.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2331,7 +2331,7 @@
|
||||
}
|
||||
},
|
||||
"inputForbiddenCharacters": {
|
||||
"message": "The following characters are not allowed: $CHARACTERS$",
|
||||
"message": "Οι ακόλουθοι χαρακτήρες δεν επιτρέπονται: $CHARACTERS$",
|
||||
"placeholders": {
|
||||
"characters": {
|
||||
"content": "$1",
|
||||
@@ -2340,7 +2340,7 @@
|
||||
}
|
||||
},
|
||||
"inputMinValue": {
|
||||
"message": "Input value must be at least $MIN$.",
|
||||
"message": "Η τιμή καταχώρησης πρέπει να είναι τουλάχιστον $MIN$",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"content": "$1",
|
||||
@@ -2349,7 +2349,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxValue": {
|
||||
"message": "Input value must not exceed $MAX$.",
|
||||
"message": "Η τιμή καταχώρησης δεν πρέπει να υπερβαίνει το $MAX$.",
|
||||
"placeholders": {
|
||||
"max": {
|
||||
"content": "$1",
|
||||
@@ -2358,17 +2358,17 @@
|
||||
}
|
||||
},
|
||||
"multipleInputEmails": {
|
||||
"message": "1 or more emails are invalid"
|
||||
"message": "1 ή περισσότερα email δεν είναι έγκυρα"
|
||||
},
|
||||
"inputTrimValidator": {
|
||||
"message": "Input must not contain only whitespace.",
|
||||
"message": "Η καταχώρηση δεν πρέπει να περιέχει μόνο κενά.",
|
||||
"description": "Notification to inform the user that a form's input can't contain only whitespace."
|
||||
},
|
||||
"inputEmail": {
|
||||
"message": "Input is not an email address."
|
||||
"message": "Η καταχώρηση δεν είναι διεύθυνση email."
|
||||
},
|
||||
"fieldsNeedAttention": {
|
||||
"message": "$COUNT$ field(s) above need your attention.",
|
||||
"message": "$COUNT$ Το/α παραπάνω πεδίo/α χρειάζονται την προσοχή σας.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2377,22 +2377,22 @@
|
||||
}
|
||||
},
|
||||
"selectPlaceholder": {
|
||||
"message": "-- Select --"
|
||||
"message": "-- Επιλογή --"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to filter --"
|
||||
"message": "-- Πληκτρολογήστε για φιλτράρισμα --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Retrieving options..."
|
||||
"message": "Ανάκτηση επιλογών..."
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "No items found"
|
||||
"message": "Δεν βρέθηκαν αντικείμενα"
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
"message": "Εκκαθάριση όλων"
|
||||
},
|
||||
"plusNMore": {
|
||||
"message": "+ $QUANTITY$ more",
|
||||
"message": "+ $QUANTITY$ περισσότερα",
|
||||
"placeholders": {
|
||||
"quantity": {
|
||||
"content": "$1",
|
||||
@@ -2401,10 +2401,10 @@
|
||||
}
|
||||
},
|
||||
"submenu": {
|
||||
"message": "Submenu"
|
||||
"message": "Υπομενού"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
"message": "Εναλλαγή σύμπτυξης",
|
||||
"description": "Toggling an expand/collapse state."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2095,8 +2095,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server Version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-Hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-Party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB de espacio cifrado en disco para adjuntos."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Métodos de autenticación en dos pasos adicionales como YubiKey, FIDO U2F y Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Higiene de contraseña, salud de la cuenta e informes de violaciones de datos para mantener su caja fuerte segura."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versión del servidor"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Autoalojado"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Aplicaciones de terceros"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB ulatuses krüpteeritud salvestusruum."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Lisavõimalused kaheastmeliseks kinnitamiseks, näiteks YubiKey, FIDO U2F ja Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Parooli hügieen, konto seisukord ja andmelekete raportid aitavad hoidlat turvalisena hoida."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Serveri versioon"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Enda majutatud"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Kolmanda osapoole"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "Eranskinentzako 1GB-eko zifratutako biltegia."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey, FIDO U2F eta Duo bezalako bi urratseko saio hasierarako aukera gehigarriak."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Pasahitzaren higienea, kontuaren egoera eta datu-bortxaketen txostenak, kutxa gotorra seguru mantentzeko."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Zerbitzariaren bertsioa"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Ostatatze propioduna"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Hirugarrenen aplikazioak"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "۱ گیگابایت فضای ذخیره سازی رمزگذاری شده برای پیوست های پرونده."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "گزینههای ورود دو مرحلهای اضافی مانند YubiKey, FIDO U2F و Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "گزارشهای بهداشت رمز عبور، سلامت حساب و نقض دادهها برای ایمن نگهداشتن گاوصندوق شما."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "نسخه سرور"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "خود میزبان"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "شخص ثالث"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 Gt salattua tallennustilaa tiedostoliitteille."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Muita kaksivaiheisen kirjautumisen todennusmenetelmiä kuten YubiKey, FIDO U2F ja Duo Security."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Omisteiset kaksivaiheisen kirjautumisen vaihtoehdot, kuten YubiKey ja Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Palvelimen versio"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Itse ylläpidetty"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Ulkopuolinen taho"
|
||||
@@ -2246,7 +2246,7 @@
|
||||
"message": "Muista tämä laite"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Poista käytöstä julkisilla laitteilla"
|
||||
"message": "Poista valinta julkisilla laitteilla"
|
||||
},
|
||||
"approveFromYourOtherDevice": {
|
||||
"message": "Hyväksy muilta laitteiltasi"
|
||||
@@ -2286,7 +2286,7 @@
|
||||
"message": "Hyväksyntää pyydetty ylläpidolta"
|
||||
},
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Pyyntösi on välitetty ylläpidolle."
|
||||
"message": "Pyyntösi on välitetty ylläpidollesi."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "Saat ilmoituksen kun se on hyväksytty."
|
||||
@@ -2310,7 +2310,7 @@
|
||||
"message": "pakollinen"
|
||||
},
|
||||
"search": {
|
||||
"message": "Etsi"
|
||||
"message": "Hae"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "Syötteen tulee sisältää ainakin $COUNT$ merkkiä.",
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage para sa mga file attachment."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Dagdag na dalawang hakbang na login option gaya ng YubiKey, FIDO U2F, at Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Pasahod higiyena, kalusugan ng account, at mga ulat sa data breach upang panatilihing ligtas ang iyong vault."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Bersyon ng server"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Auto-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Ika-tatlong-partido"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 Go de stockage chiffré pour les fichiers joints."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Options additionnelles d'identification à deux étapes telles que YubiKey, FIDO U2F et Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Version du serveur"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Auto-hébergé"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tierce partie"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 ג'יגה של מקום אחסון עבור קבצים מצורפים."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "אפשרויות כניסה דו שלבית מתקדמות כמו YubiKey, FIDO U2F, וגם Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "היגיינת סיסמאות, מצב בריאות החשבון, ודיווחים מעודכנים על פרצות חדשות בכדי לשמור על הכספת שלך בטוחה."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB of encrypted file storage."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "अतिरिक्त दो-चरण लॉगिन विकल्प जैसे YubiKey, FIDO U2F, और डुओ।"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "अपनी वॉल्ट को सुरक्षित रखने के लिए पासवर्ड स्वच्छता, खाता स्वास्थ्य और डेटा उल्लंघन रिपोर्ट।"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB šifriranog prostora za pohranu podataka."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Dodatne mogućnosti za prijavu dvostrukom autentifikacijom kao što su YubiKey, FIDO U2F i Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Higijenu lozinki, zdravlje računa i izvještaje o krađi podatak radi zaštite svojeg trezora."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Verzija poslužitelja"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Vlastiti poslužitelj"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB titkosított tárhely a fájlmellékleteknek."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "További két lépcsős bejelentkezés lehetőségek, mint például YubiKey, FIDO U2F és Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Jelszó higiénia, fiók biztonság és adatszivárgási jelentések a széf biztonsága érdekében."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Szerver verzió"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Saját kiszolgáló"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Harmadik fél"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB penyimpanan berkas yang dienkripsi."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Pilihan info masuk dua langkah tambahan seperti YubiKey, FIDO U2F, dan Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Kebersihan kata sandi, kesehatan akun, dan laporan kebocoran data untuk tetap menjaga keamanan brankas Anda."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB di spazio di archiviazione criptato per gli allegati."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Più opzioni di verifica in due passaggi come YubiKey, FIDO U2F, e Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Sicurezza delle password, integrità dell'account, e rapporti su violazioni di dati per mantenere sicura la tua cassaforte."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versione Server"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Terze parti"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1GB の暗号化されたファイルストレージ"
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey、FIDO U2F、Duoなどの追加の2段階認証ログインオプション"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。"
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "保管庫を安全に保つための、パスワードやアカウントの健全性、データ侵害に関するレポート"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "サーバーのバージョン"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "セルフホスト"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "サードパーティー"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "ಫೈಲ್ ಲಗತ್ತುಗಳಿಗಾಗಿ 1 ಜಿಬಿ ಎನ್ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಸಂಗ್ರಹ."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "ಹೆಚ್ಚುವರಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಆಯ್ಕೆಗಳಾದ ಯೂಬಿಕೆ, ಎಫ್ಐಡಿಒ ಯು 2 ಎಫ್, ಮತ್ತು ಡ್ಯುವೋ."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸುರಕ್ಷಿತವಾಗಿರಿಸಲು ಪಾಸ್ವರ್ಡ್ ನೈರ್ಮಲ್ಯ, ಖಾತೆ ಆರೋಗ್ಯ ಮತ್ತು ಡೇಟಾ ಉಲ್ಲಂಘನೆ ವರದಿಗಳು."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1GB의 암호화된 파일 저장소."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey나 FIDO U2F, Duo 등의 추가적인 2단계 인증 옵션."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB užšifruotos vietos diske bylų prisegimams."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Papildomos dviejų žingsių prisijungimo opcijos, tokios kaip YubiKey, FIDO U2F ir Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Patentuotos dviejų žingsnių prisijungimo parinktys, tokios kaip YubiKey ir Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Slaptažodžio higiena, prieigos sveikata ir duomenų nutekinimo ataskaitos, kad tavo saugyklas būtų saugus."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Trečioji šalis"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB šifrētas krātuves datņu pielikumiem."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Tādas papildu divpakāpju pieteikšanās iespējas kā YubiKey, FIDO U2F un Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Paroļu higiēnas, konta veselības un datu noplūžu pārskati, lai uzturētu glabātavu drošu."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Servera versija"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Pašizvietots"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Trešās puses"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "ഫയൽ അറ്റാച്ചുമെന്റുകൾക്കായി 1 ജിബി എൻക്രിപ്റ്റുചെയ്ത സംഭരണം."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "രണ്ട്-ഘട്ട പ്രവേശന ഓപ്ഷനുകളായ Yubikey, FIDO U2F, Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "നിങ്ങളുടെ വാൾട് സൂക്ഷിക്കുന്നതിന്. പാസ്വേഡ് ശുചിത്വം, അക്കൗണ്ട് ആരോഗ്യം, ഡാറ്റ ലംഘന റിപ്പോർട്ടുകൾ."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -208,10 +208,10 @@
|
||||
"message": "संकालन"
|
||||
},
|
||||
"syncVaultNow": {
|
||||
"message": "Sync vault now"
|
||||
"message": "तिजोरी संकालन आता करा"
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "Last sync:"
|
||||
"message": "शेवटचे संकालन:"
|
||||
},
|
||||
"passGen": {
|
||||
"message": "पासवर्ड जनित्र"
|
||||
@@ -279,7 +279,7 @@
|
||||
"message": "Avoid ambiguous characters"
|
||||
},
|
||||
"searchVault": {
|
||||
"message": "Search vault"
|
||||
"message": "तिजोरीत शोधा"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB med kryptert fillagring for filvedlegg."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Ytterligere 2-trinnsinnloggingsmuligheter, slik som YubiKey, FIDO U2F, og Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Passordhygiene, kontohelse, og databruddsrapporter som holder hvelvet ditt trygt."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server Versjon"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Selvbetjent"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tredjepart"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB versleutelde opslag voor bijlagen."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Extra opties voor tweestapsaanmelding zoals YubiKey, FIDO U2F en Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Wachtwoordhygiëne, gezondheid van je account en datalekken om je kluis veilig te houden."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Serverversie"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Zelfgehost"
|
||||
"selfHostedServer": {
|
||||
"message": "zelfgehost"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "van derden"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB miejsca na zaszyfrowane załączniki."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Dodatkowe opcje logowania dwustopniowego, takie jak klucze YubiKey, FIDO U2F oraz Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Własnościowe opcje logowania dwuetapowego, takie jak YubiKey i Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Raporty bezpieczeństwa haseł, stanu konta i raporty wycieków danych, aby Twoje dane były bezpieczne."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Wersja serwera"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Samodzielnie hostowany"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Inny dostawca"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB de armazenamento de arquivos encriptados."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Opções de autenticação de duas etapas adicionais como YubiKey, FIDO U2F, e Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Higiene de senha, saúde da conta, e relatórios sobre violação de dados para manter o seu cofre seguro."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versão do servidor"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Auto-hospedado"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Terceiros"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB de armazenamento encriptado para anexos de ficheiros."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Opções adicionais de verificação de dois passos, como YubiKey, FIDO U2F e Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versão do servidor"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Auto-hospedado"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "De terceiros"
|
||||
|
||||
@@ -196,13 +196,13 @@
|
||||
"message": "Ajutor și feedback"
|
||||
},
|
||||
"helpCenter": {
|
||||
"message": "Bitwarden Help center"
|
||||
"message": "Centrul de Ajutor Bitwarden"
|
||||
},
|
||||
"communityForums": {
|
||||
"message": "Explore Bitwarden community forums"
|
||||
"message": "Explorează forumurile comunității Bitwarden"
|
||||
},
|
||||
"contactSupport": {
|
||||
"message": "Contact Bitwarden support"
|
||||
"message": "Contactați asistența Bitwarden"
|
||||
},
|
||||
"sync": {
|
||||
"message": "Sincronizare"
|
||||
@@ -442,7 +442,7 @@
|
||||
"message": "Este necesară rescrierea parolei principale."
|
||||
},
|
||||
"masterPasswordMinlength": {
|
||||
"message": "Master password must be at least $VALUE$ characters long.",
|
||||
"message": "Parola principală trebuie să aibă cel puțin $VALUE$ caractere.",
|
||||
"description": "The Master Password must be at least a specific number of characters long.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
@@ -634,10 +634,10 @@
|
||||
"message": "Actualizare"
|
||||
},
|
||||
"notificationUnlockDesc": {
|
||||
"message": "Unlock your Bitwarden vault to complete the auto-fill request."
|
||||
"message": "Deblochează seiful Bitwarden pentru a finaliza solicitarea de completare automată."
|
||||
},
|
||||
"notificationUnlock": {
|
||||
"message": "Unlock"
|
||||
"message": "Deblocare"
|
||||
},
|
||||
"enableContextMenuItem": {
|
||||
"message": "Afișați opțiunile meniului contextual"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB spațiu de stocare criptat pentru atașamente de fișiere."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Opțiuni adiționale de conectare în două etape, cum ar fi YubiKey, FIDO U2F și Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Rapoarte privind igiena parolelor, sănătatea contului și breșele de date pentru a vă păstra seiful în siguranță."
|
||||
@@ -985,7 +985,7 @@
|
||||
"message": "Dacă se detectează un formular de autentificare, completați-l automat la încărcarea paginii web."
|
||||
},
|
||||
"experimentalFeature": {
|
||||
"message": "Compromised or untrusted websites can exploit auto-fill on page load."
|
||||
"message": "Site-urile web compromise sau nesigure pot exploata funcția de autocompletare la încărcarea paginii."
|
||||
},
|
||||
"learnMoreAboutAutofill": {
|
||||
"message": "Learn more about auto-fill"
|
||||
@@ -1468,7 +1468,7 @@
|
||||
"message": "Articolul s-a completat automat "
|
||||
},
|
||||
"insecurePageWarning": {
|
||||
"message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page."
|
||||
"message": "Avertisment: Aceasta este o pagină HTTP nesecurizată și orice informație pe care o trimiteți poate fi văzută și modificată de alte persoane. Această Parolă a fost salvată inițial pe o pagină securizată (HTTPS)."
|
||||
},
|
||||
"insecurePageWarningFillPrompt": {
|
||||
"message": "Do you still wish to fill this login?"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Versiune server"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Autogăzduit"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Parte terță"
|
||||
|
||||
@@ -671,7 +671,7 @@
|
||||
"description": "'Solarized' is a noun and the name of a color scheme. It should not be translated."
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Экспортировать хранилище"
|
||||
"message": "Экспорт хранилища"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Формат файла"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 ГБ зашифрованного хранилища для вложенных файлов."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Дополнительные варианты двухэтапной аутентификации, такие как YubiKey, FIDO U2F и Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Гигиена паролей, здоровье аккаунта и отчеты об утечках данных для обеспечения безопасности вашего хранилища."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Версия сервера"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Собственный хостинг"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Сторонний"
|
||||
@@ -2141,7 +2141,7 @@
|
||||
"message": "Фраза отпечатка"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка пальца совпадает на другом устройстве."
|
||||
"message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка совпадает на другом устройстве."
|
||||
},
|
||||
"resendNotification": {
|
||||
"message": "Отправить уведомление повторно"
|
||||
@@ -2168,7 +2168,7 @@
|
||||
"message": "Обнаружен слабый пароль, найденный в утечке данных. Используйте надежный и уникальный пароль для защиты вашего аккаунта. Вы уверены, что хотите использовать этот пароль?"
|
||||
},
|
||||
"checkForBreaches": {
|
||||
"message": "Проверьте известные случаи утечки данных для этого пароля"
|
||||
"message": "Проверять известные случаи утечки данных для этого пароля"
|
||||
},
|
||||
"important": {
|
||||
"message": "Важно:"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "ගොනු ඇමුණුම් සඳහා 1 GB සංකේතාත්මක ගබඩා."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "එවැනි YuBiKey, FIDO U2F, සහ Duo ලෙස අතිරේක පියවර දෙකක් පිවිසුම් විකල්ප."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "ඔබගේ සුරක්ෂිතාගාරය ආරක්ෂිතව තබා ගැනීම සඳහා මුරපදය සනීපාරක්ෂාව, ගිණුම් සෞඛ්යය සහ දත්ත උල්ලං ach නය වාර්තා කරයි."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB šifrovaného úložiska."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Ďalšie možnosti dvojstupňového prihlásenia ako YubiKey, FIDO U2F a Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Verzia servera"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Vlastný hosting"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tretia strana"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB šifriranega prostora za shrambo podatkov."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Dodatne možnosti za prijavo v dveh korakih, n.pr. YubiKey, FIDO U2F in Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Higiena gesel, zdravje računa in poročila o kraji podatkov, ki vam pomagajo ohraniti varnost vašega trezorja."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Verzija strežnika"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
"message": "Остало"
|
||||
},
|
||||
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
||||
"message": "Set up an unlock method to change your vault timeout action."
|
||||
"message": "Подесите метод откључавања да бисте променили радњу временског ограничења сефа."
|
||||
},
|
||||
"rateExtension": {
|
||||
"message": "Оцени овај додатак"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1ГБ шифровано складиште за прилоге."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Додатне опције пријаве у два корака као што су YubiKey, FIDO U2F, и Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф."
|
||||
@@ -1606,10 +1606,10 @@
|
||||
"message": "Биометрија прегледача није подржана на овом уређају."
|
||||
},
|
||||
"biometricsFailedTitle": {
|
||||
"message": "Biometrics failed"
|
||||
"message": "Биометрија није успела"
|
||||
},
|
||||
"biometricsFailedDesc": {
|
||||
"message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support."
|
||||
"message": "Биометрија се не може завршити, размислите о коришћењу главне лозинке или одјавите се. Ако се ово настави, контактирајте подршку Bitwarden-а."
|
||||
},
|
||||
"nativeMessaginPermissionErrorTitle": {
|
||||
"message": "Дозвола није дата"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Верзија сервера"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Личан хостинг"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Трећа страна"
|
||||
@@ -2153,7 +2153,7 @@
|
||||
"message": "Обавештење је послато на ваш уређај."
|
||||
},
|
||||
"loginInitiated": {
|
||||
"message": "Login initiated"
|
||||
"message": "Пријава је покренута"
|
||||
},
|
||||
"exposedMasterPassword": {
|
||||
"message": "Изложена главна лозинка"
|
||||
@@ -2240,25 +2240,25 @@
|
||||
"message": "Отвара се у новом прозору"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Device approval required. Select an approval option below:"
|
||||
"message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Remember this device"
|
||||
"message": "Запамти овај уређај"
|
||||
},
|
||||
"uncheckIfPublicDevice": {
|
||||
"message": "Uncheck if using a public device"
|
||||
"message": "Искључите ако се користи јавни уређај"
|
||||
},
|
||||
"approveFromYourOtherDevice": {
|
||||
"message": "Approve from your other device"
|
||||
"message": "Одобри са мојим другим уређајем"
|
||||
},
|
||||
"requestAdminApproval": {
|
||||
"message": "Request admin approval"
|
||||
"message": "Затражити одобрење администратора"
|
||||
},
|
||||
"approveWithMasterPassword": {
|
||||
"message": "Approve with master password"
|
||||
"message": "Одобрити са главном лозинком"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization SSO identifier is required."
|
||||
"message": "Потребан је SSO идентификатор организације."
|
||||
},
|
||||
"eu": {
|
||||
"message": "EU",
|
||||
@@ -2280,40 +2280,40 @@
|
||||
"message": "Приказ"
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Account successfully created!"
|
||||
"message": "Налог је успешно креиран!"
|
||||
},
|
||||
"adminApprovalRequested": {
|
||||
"message": "Admin approval requested"
|
||||
"message": "Захтевано је одобрење администратора"
|
||||
},
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Your request has been sent to your admin."
|
||||
"message": "Ваш захтев је послат вашем администратору."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
"message": "Бићете обавештени када буде одобрено."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
"message": "Имате проблема са пријављивањем?"
|
||||
},
|
||||
"loginApproved": {
|
||||
"message": "Login approved"
|
||||
"message": "Пријава је одобрена"
|
||||
},
|
||||
"userEmailMissing": {
|
||||
"message": "User email missing"
|
||||
"message": "Недостаје имејл корисника"
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Device trusted"
|
||||
"message": "Уређај поуздан"
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "Input is required."
|
||||
"message": "Унос је потребан."
|
||||
},
|
||||
"required": {
|
||||
"message": "required"
|
||||
"message": "обавезно"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search"
|
||||
"message": "Тражи"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "Input must be at least $COUNT$ characters long.",
|
||||
"message": "Унос трба имати најмање $COUNT$ слова.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxLength": {
|
||||
"message": "Input must not exceed $COUNT$ characters in length.",
|
||||
"message": "Унос не сме бити већи од $COUNT$ карактера.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2331,7 +2331,7 @@
|
||||
}
|
||||
},
|
||||
"inputForbiddenCharacters": {
|
||||
"message": "The following characters are not allowed: $CHARACTERS$",
|
||||
"message": "Следећи знакови нису дозвољени: $CHARACTERS$",
|
||||
"placeholders": {
|
||||
"characters": {
|
||||
"content": "$1",
|
||||
@@ -2340,7 +2340,7 @@
|
||||
}
|
||||
},
|
||||
"inputMinValue": {
|
||||
"message": "Input value must be at least $MIN$.",
|
||||
"message": "Вредност мора бити најмање $MIN$.",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"content": "$1",
|
||||
@@ -2349,7 +2349,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxValue": {
|
||||
"message": "Input value must not exceed $MAX$.",
|
||||
"message": "Вредност не сме бити већа од $MAX$.",
|
||||
"placeholders": {
|
||||
"max": {
|
||||
"content": "$1",
|
||||
@@ -2358,17 +2358,17 @@
|
||||
}
|
||||
},
|
||||
"multipleInputEmails": {
|
||||
"message": "1 or more emails are invalid"
|
||||
"message": "1 или више имејлова су неважећи"
|
||||
},
|
||||
"inputTrimValidator": {
|
||||
"message": "Input must not contain only whitespace.",
|
||||
"message": "Унос не сме да садржи само размак.",
|
||||
"description": "Notification to inform the user that a form's input can't contain only whitespace."
|
||||
},
|
||||
"inputEmail": {
|
||||
"message": "Input is not an email address."
|
||||
"message": "Унос није имејл."
|
||||
},
|
||||
"fieldsNeedAttention": {
|
||||
"message": "$COUNT$ field(s) above need your attention.",
|
||||
"message": "$COUNT$ поље(а) изнад захтевај(у) вашу пажњу.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2377,22 +2377,22 @@
|
||||
}
|
||||
},
|
||||
"selectPlaceholder": {
|
||||
"message": "-- Select --"
|
||||
"message": "-- Одабрати --"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to filter --"
|
||||
"message": "-- Тип за филтрирање --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Retrieving options..."
|
||||
"message": "Преузимање опција..."
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "No items found"
|
||||
"message": "Нема предмета"
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
"message": "Обриши све"
|
||||
},
|
||||
"plusNMore": {
|
||||
"message": "+ $QUANTITY$ more",
|
||||
"message": "+ још $QUANTITY$",
|
||||
"placeholders": {
|
||||
"quantity": {
|
||||
"content": "$1",
|
||||
@@ -2401,10 +2401,10 @@
|
||||
}
|
||||
},
|
||||
"submenu": {
|
||||
"message": "Submenu"
|
||||
"message": "Под-мени"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
"message": "Промени проширење",
|
||||
"description": "Toggling an expand/collapse state."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB lagring av krypterade filer."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Ytterligare alternativ för tvåstegsverifiering såsom YubiKey, FIDO U2F och Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Lösenordshygien, kontohälsa och dataintrångsrapporter för att hålla ditt valv säkert."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Serverversion"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Lokalt installerad"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Tredje part"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB encrypted storage for file attachments."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB of encrypted file storage."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "ตัวเลือกการเข้าสู่ระบบแบบสองขั้นตอนเพิ่มเติม เช่น YubiKey, FIDO U2F และ Duo"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "สุขอนามัยของรหัสผ่าน ความสมบูรณ์ของบัญชี และรายงานการละเมิดข้อมูลเพื่อให้ตู้นิรภัยของคุณปลอดภัย"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Server version"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Self-hosted"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Third-party"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "Dosya ekleri için 1 GB şifrelenmiş depolama."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey, FIDO U2F ve Duo gibi iki aşamalı giriş seçenekleri."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Kasanızı güvende tutmak için parola hijyeni, hesap sağlığı ve veri ihlali raporları."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Sunucu sürümü"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Barındırılan"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Üçüncü taraf"
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
"message": "Змінити"
|
||||
},
|
||||
"view": {
|
||||
"message": "Перегляд"
|
||||
"message": "Переглянути"
|
||||
},
|
||||
"noItemsInList": {
|
||||
"message": "Немає записів."
|
||||
@@ -321,7 +321,7 @@
|
||||
"message": "Видалити запис"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Перегляд запису"
|
||||
"message": "Переглянути запис"
|
||||
},
|
||||
"launch": {
|
||||
"message": "Перейти"
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 ГБ зашифрованого сховища для файлів."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Додаткові можливості двоетапної перевірки, наприклад, YubiKey, FIDO U2F та Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Версія сервера"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Власне розміщення"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Сторонній"
|
||||
@@ -2304,16 +2304,16 @@
|
||||
"message": "Довірений пристрій"
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "Input is required."
|
||||
"message": "Необхідно ввести дані."
|
||||
},
|
||||
"required": {
|
||||
"message": "required"
|
||||
"message": "обов'язково"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search"
|
||||
"message": "Пошук"
|
||||
},
|
||||
"inputMinLength": {
|
||||
"message": "Input must be at least $COUNT$ characters long.",
|
||||
"message": "Введені дані мають бути довжиною принаймні $COUNT$ символів.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxLength": {
|
||||
"message": "Input must not exceed $COUNT$ characters in length.",
|
||||
"message": "Вхідне значення не повинно перевищувати $COUNT$ символів.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2331,7 +2331,7 @@
|
||||
}
|
||||
},
|
||||
"inputForbiddenCharacters": {
|
||||
"message": "The following characters are not allowed: $CHARACTERS$",
|
||||
"message": "Вказані символи заборонені: $CHARACTERS$",
|
||||
"placeholders": {
|
||||
"characters": {
|
||||
"content": "$1",
|
||||
@@ -2340,7 +2340,7 @@
|
||||
}
|
||||
},
|
||||
"inputMinValue": {
|
||||
"message": "Input value must be at least $MIN$.",
|
||||
"message": "Значення має бути принаймні $MIN$.",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"content": "$1",
|
||||
@@ -2349,7 +2349,7 @@
|
||||
}
|
||||
},
|
||||
"inputMaxValue": {
|
||||
"message": "Input value must not exceed $MAX$.",
|
||||
"message": "Значення не може перевищувати $MAX$.",
|
||||
"placeholders": {
|
||||
"max": {
|
||||
"content": "$1",
|
||||
@@ -2358,17 +2358,17 @@
|
||||
}
|
||||
},
|
||||
"multipleInputEmails": {
|
||||
"message": "1 or more emails are invalid"
|
||||
"message": "1 або більше адрес е-пошти недійсні"
|
||||
},
|
||||
"inputTrimValidator": {
|
||||
"message": "Input must not contain only whitespace.",
|
||||
"message": "Введене значення не повинно містити лише пробіл.",
|
||||
"description": "Notification to inform the user that a form's input can't contain only whitespace."
|
||||
},
|
||||
"inputEmail": {
|
||||
"message": "Input is not an email address."
|
||||
"message": "Введені дані не є адресою е-пошти."
|
||||
},
|
||||
"fieldsNeedAttention": {
|
||||
"message": "$COUNT$ field(s) above need your attention.",
|
||||
"message": "$COUNT$ поле (поля) вище потребують вашої уваги.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -2377,22 +2377,22 @@
|
||||
}
|
||||
},
|
||||
"selectPlaceholder": {
|
||||
"message": "-- Select --"
|
||||
"message": "-- Оберіть--"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to filter --"
|
||||
"message": "-- Введіть для фільтрування --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Retrieving options..."
|
||||
"message": "Параметри отримання..."
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "No items found"
|
||||
"message": "Нічого не знайдено"
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
"message": "Очистити все"
|
||||
},
|
||||
"plusNMore": {
|
||||
"message": "+ $QUANTITY$ more",
|
||||
"message": "+ ще $QUANTITY$",
|
||||
"placeholders": {
|
||||
"quantity": {
|
||||
"content": "$1",
|
||||
@@ -2401,10 +2401,10 @@
|
||||
}
|
||||
},
|
||||
"submenu": {
|
||||
"message": "Submenu"
|
||||
"message": "Підменю"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle collapse",
|
||||
"message": "Згорнути/розгорнути",
|
||||
"description": "Toggling an expand/collapse state."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1GB bộ nhớ lưu trữ tập tin được mã hóa."
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "Tuỳ chọn đăng nhập 2 bước bổ sung như YubiKey, FIDO U2F, và Duo."
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rĩ dữ liệu là để giữ cho kho của bạn an toàn."
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "Phiên bản máy chủ"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "Tự lưu trữ"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "Bên thứ ba"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "1 GB 文件附件加密存储。"
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "额外的两步登录选项,如 YubiKey、FIDO U2F 和 Duo。"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "专有的两步登录选项,如 YubiKey 和 Duo。"
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "服务器版本"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "自托管"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "第三方"
|
||||
@@ -2135,7 +2135,7 @@
|
||||
"message": "设备登录"
|
||||
},
|
||||
"loginWithDeviceEnabledInfo": {
|
||||
"message": "设备登录必须在 Bitwarden 应用程序的设置中设启用。需要其他选项吗?"
|
||||
"message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?"
|
||||
},
|
||||
"fingerprintPhraseHeader": {
|
||||
"message": "指纹短语"
|
||||
|
||||
@@ -795,8 +795,8 @@
|
||||
"ppremiumSignUpStorage": {
|
||||
"message": "用於檔案附件的 1 GB 加密儲存空間。"
|
||||
},
|
||||
"ppremiumSignUpTwoStep": {
|
||||
"message": "YubiKey、FIDO U2F 和 Duo 等額外的兩步驟登入選項。"
|
||||
"premiumSignUpTwoStepOptions": {
|
||||
"message": "Proprietary two-step login options such as YubiKey and Duo."
|
||||
},
|
||||
"ppremiumSignUpReports": {
|
||||
"message": "密碼健康度檢查、提供帳戶體檢以及資料外洩報告,以保障您的密碼庫安全。"
|
||||
@@ -2092,8 +2092,8 @@
|
||||
"serverVersion": {
|
||||
"message": "伺服器版本"
|
||||
},
|
||||
"selfHosted": {
|
||||
"message": "自我裝載"
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"thirdParty": {
|
||||
"message": "第三方"
|
||||
|
||||
@@ -44,7 +44,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
<iframe
|
||||
id="hcaptcha_iframe"
|
||||
height="80"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
|
||||
@@ -120,7 +120,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div>
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="box last" *ngIf="showTerms">
|
||||
<div class="box-content">
|
||||
<div
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn && !webAuthnNewTab">
|
||||
<div id="web-authn-frame">
|
||||
<iframe id="webauthn_iframe"></iframe>
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
@@ -112,7 +112,9 @@
|
||||
selectedProviderType === providerType.OrganizationDuo
|
||||
"
|
||||
>
|
||||
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
|
||||
<div id="duo-frame">
|
||||
<iframe id="duo_iframe" sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
@@ -123,7 +125,7 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="box-content-row" [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="content" *ngIf="selectedProviderType == null">
|
||||
<p class="text-center">{{ "noTwoStepProviders" | i18n }}</p>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
@@ -14,7 +13,6 @@ describe("CipherContextMenuHandler", () => {
|
||||
let mainContextMenuHandler: MockProxy<MainContextMenuHandler>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
|
||||
let sut: CipherContextMenuHandler;
|
||||
|
||||
@@ -22,17 +20,10 @@ describe("CipherContextMenuHandler", () => {
|
||||
mainContextMenuHandler = mock();
|
||||
authService = mock();
|
||||
cipherService = mock();
|
||||
userVerificationService = mock();
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
||||
|
||||
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
|
||||
|
||||
sut = new CipherContextMenuHandler(
|
||||
mainContextMenuHandler,
|
||||
authService,
|
||||
cipherService,
|
||||
userVerificationService
|
||||
);
|
||||
sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService);
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
@@ -78,7 +69,7 @@ describe("CipherContextMenuHandler", () => {
|
||||
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("only adds valid ciphers", async () => {
|
||||
it("only adds login ciphers including ciphers that require reprompt", async () => {
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||
|
||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
||||
@@ -90,47 +81,6 @@ describe("CipherContextMenuHandler", () => {
|
||||
name: "Test Cipher",
|
||||
login: { username: "Test Username" },
|
||||
};
|
||||
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([
|
||||
null, // invalid cipher
|
||||
undefined, // invalid cipher
|
||||
{ type: CipherType.Card }, // invalid cipher
|
||||
{ type: CipherType.Login, reprompt: CipherRepromptType.Password }, // invalid cipher
|
||||
realCipher, // valid cipher
|
||||
] as any[]);
|
||||
|
||||
await sut.update("https://test.com");
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||
"Test Cipher (Test Username)",
|
||||
"5",
|
||||
"https://test.com",
|
||||
realCipher
|
||||
);
|
||||
});
|
||||
|
||||
it("adds ciphers with master password reprompt if the user does not have a master password", async () => {
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||
|
||||
// User does not have a master password, or has one but hasn't logged in with it (key connector user or TDE user)
|
||||
userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false);
|
||||
|
||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
||||
|
||||
const realCipher = {
|
||||
id: "5",
|
||||
type: CipherType.Login,
|
||||
reprompt: CipherRepromptType.None,
|
||||
name: "Test Cipher",
|
||||
login: { username: "Test Username" },
|
||||
};
|
||||
|
||||
const repromptCipher = {
|
||||
id: "6",
|
||||
type: CipherType.Login,
|
||||
@@ -143,8 +93,8 @@ describe("CipherContextMenuHandler", () => {
|
||||
null, // invalid cipher
|
||||
undefined, // invalid cipher
|
||||
{ type: CipherType.Card }, // invalid cipher
|
||||
repromptCipher, // valid cipher
|
||||
realCipher, // valid cipher
|
||||
repromptCipher,
|
||||
] as any[]);
|
||||
|
||||
await sut.update("https://test.com");
|
||||
@@ -153,7 +103,6 @@ describe("CipherContextMenuHandler", () => {
|
||||
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||
|
||||
// Should call this twice, once for each valid cipher
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
authServiceFactory,
|
||||
AuthServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/auth-service.factory";
|
||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
||||
import { Account } from "../../models/account";
|
||||
import { CachedServices } from "../../platform/background/service-factories/factory-options";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
@@ -39,8 +37,7 @@ export class CipherContextMenuHandler {
|
||||
constructor(
|
||||
private mainContextMenuHandler: MainContextMenuHandler,
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private userVerificationService: UserVerificationService
|
||||
private cipherService: CipherService
|
||||
) {}
|
||||
|
||||
static async create(cachedServices: CachedServices) {
|
||||
@@ -69,9 +66,6 @@ export class CipherContextMenuHandler {
|
||||
clipboardWriteCallback: NOT_IMPLEMENTED,
|
||||
win: self,
|
||||
},
|
||||
stateMigrationServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
stateServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
@@ -79,8 +73,7 @@ export class CipherContextMenuHandler {
|
||||
return new CipherContextMenuHandler(
|
||||
await MainContextMenuHandler.mv3Create(cachedServices),
|
||||
await authServiceFactory(cachedServices, serviceOptions),
|
||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
||||
await userVerificationServiceFactory(cachedServices, serviceOptions)
|
||||
await cipherServiceFactory(cachedServices, serviceOptions)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,11 +173,7 @@ export class CipherContextMenuHandler {
|
||||
}
|
||||
|
||||
private async updateForCipher(url: string, cipher: CipherView) {
|
||||
if (
|
||||
cipher == null ||
|
||||
cipher.type !== CipherType.Login ||
|
||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
||||
) {
|
||||
if (cipher == null || cipher.type !== CipherType.Login) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
@@ -63,6 +64,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let totpService: MockProxy<TotpService>;
|
||||
let eventCollectionService: MockProxy<EventCollectionService>;
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
|
||||
let sut: ContextMenuClickedHandler;
|
||||
|
||||
@@ -82,7 +84,8 @@ describe("ContextMenuClickedHandler", () => {
|
||||
authService,
|
||||
cipherService,
|
||||
totpService,
|
||||
eventCollectionService
|
||||
eventCollectionService,
|
||||
userVerificationService
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
AuthServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/auth-service.factory";
|
||||
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
||||
import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
|
||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||
import { Account } from "../../models/account";
|
||||
@@ -56,7 +58,8 @@ export class ContextMenuClickedHandler {
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private totpService: TotpService,
|
||||
private eventCollectionService: EventCollectionService
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
static async mv3Create(cachedServices: CachedServices) {
|
||||
@@ -85,9 +88,6 @@ export class ContextMenuClickedHandler {
|
||||
clipboardWriteCallback: NOT_IMPLEMENTED,
|
||||
win: self,
|
||||
},
|
||||
stateMigrationServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
stateServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
@@ -109,7 +109,8 @@ export class ContextMenuClickedHandler {
|
||||
await authServiceFactory(cachedServices, serviceOptions),
|
||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
||||
await totpServiceFactory(cachedServices, serviceOptions),
|
||||
await eventCollectionServiceFactory(cachedServices, serviceOptions)
|
||||
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
||||
await userVerificationServiceFactory(cachedServices, serviceOptions)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,7 +205,7 @@ export class ContextMenuClickedHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: AUTOFILL_ID,
|
||||
@@ -218,7 +219,7 @@ export class ContextMenuClickedHandler {
|
||||
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
||||
break;
|
||||
case COPY_PASSWORD_ID:
|
||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: COPY_PASSWORD_ID,
|
||||
@@ -230,7 +231,7 @@ export class ContextMenuClickedHandler {
|
||||
|
||||
break;
|
||||
case COPY_VERIFICATIONCODE_ID:
|
||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: COPY_VERIFICATIONCODE_ID,
|
||||
@@ -246,6 +247,13 @@ export class ContextMenuClickedHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async isPasswordRepromptRequired(cipher: CipherView): Promise<boolean> {
|
||||
return (
|
||||
cipher.reprompt === CipherRepromptType.Password &&
|
||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
||||
);
|
||||
}
|
||||
|
||||
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
BrowserApi.sendTabsMessage(
|
||||
|
||||
@@ -79,9 +79,6 @@ export class MainContextMenuHandler {
|
||||
logServiceOptions: {
|
||||
isDev: false,
|
||||
},
|
||||
stateMigrationServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
stateServiceOptions: {
|
||||
stateFactory: stateFactory,
|
||||
},
|
||||
|
||||
13
apps/browser/src/autofill/constants.ts
Normal file
13
apps/browser/src/autofill/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const TYPE_CHECK = {
|
||||
FUNCTION: "function",
|
||||
NUMBER: "number",
|
||||
STRING: "string",
|
||||
} as const;
|
||||
|
||||
export const EVENTS = {
|
||||
CHANGE: "change",
|
||||
INPUT: "input",
|
||||
KEYDOWN: "keydown",
|
||||
KEYPRESS: "keypress",
|
||||
KEYUP: "keyup",
|
||||
} as const;
|
||||
@@ -0,0 +1,21 @@
|
||||
import AutofillScript from "../../models/autofill-script";
|
||||
|
||||
type AutofillExtensionMessage = {
|
||||
command: string;
|
||||
tab?: chrome.tabs.Tab;
|
||||
sender?: string;
|
||||
fillScript?: AutofillScript;
|
||||
};
|
||||
|
||||
type AutofillExtensionMessageHandlers = {
|
||||
[key: string]: CallableFunction;
|
||||
collectPageDetails: (message: { message: AutofillExtensionMessage }) => void;
|
||||
collectPageDetailsImmediately: (message: { message: AutofillExtensionMessage }) => void;
|
||||
fillForm: (message: { message: AutofillExtensionMessage }) => void;
|
||||
};
|
||||
|
||||
interface AutofillInit {
|
||||
init(): void;
|
||||
}
|
||||
|
||||
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };
|
||||
175
apps/browser/src/autofill/content/autofill-init.spec.ts
Normal file
175
apps/browser/src/autofill/content/autofill-init.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript from "../models/autofill-script";
|
||||
|
||||
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
||||
|
||||
describe("AutofillInit", () => {
|
||||
let bitwardenAutofillInit: any;
|
||||
|
||||
beforeEach(() => {
|
||||
require("../content/autofill-init");
|
||||
bitwardenAutofillInit = window.bitwardenAutofillInit;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("sets up the extension message listeners", () => {
|
||||
jest.spyOn(bitwardenAutofillInit, "setupExtensionMessageListeners");
|
||||
|
||||
bitwardenAutofillInit.init();
|
||||
|
||||
expect(bitwardenAutofillInit.setupExtensionMessageListeners).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPageDetails", () => {
|
||||
let extensionMessage: AutofillExtensionMessage;
|
||||
let pageDetails: AutofillPageDetails;
|
||||
|
||||
beforeEach(() => {
|
||||
extensionMessage = {
|
||||
command: "collectPageDetails",
|
||||
tab: mock<chrome.tabs.Tab>(),
|
||||
sender: "sender",
|
||||
};
|
||||
pageDetails = {
|
||||
title: "title",
|
||||
url: "http://example.com",
|
||||
documentUrl: "documentUrl",
|
||||
forms: {},
|
||||
fields: [],
|
||||
collectedTimestamp: 0,
|
||||
};
|
||||
jest
|
||||
.spyOn(bitwardenAutofillInit.collectAutofillContentService, "getPageDetails")
|
||||
.mockReturnValue(pageDetails);
|
||||
});
|
||||
|
||||
it("returns collected page details for autofill if set to send the details in the response", async () => {
|
||||
const response = await bitwardenAutofillInit["collectPageDetails"](extensionMessage, true);
|
||||
|
||||
expect(bitwardenAutofillInit.collectAutofillContentService.getPageDetails).toHaveBeenCalled();
|
||||
expect(response).toEqual(pageDetails);
|
||||
});
|
||||
|
||||
it("sends the collected page details for autofill using a background script message", async () => {
|
||||
jest.spyOn(chrome.runtime, "sendMessage");
|
||||
|
||||
await bitwardenAutofillInit["collectPageDetails"](extensionMessage);
|
||||
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
command: "collectPageDetailsResponse",
|
||||
tab: extensionMessage.tab,
|
||||
details: pageDetails,
|
||||
sender: extensionMessage.sender,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fillForm", () => {
|
||||
it("will call the InsertAutofillContentService to fill the form", () => {
|
||||
const fillScript = mock<AutofillScript>();
|
||||
jest
|
||||
.spyOn(bitwardenAutofillInit.insertAutofillContentService, "fillForm")
|
||||
.mockImplementation();
|
||||
|
||||
bitwardenAutofillInit.fillForm(fillScript);
|
||||
|
||||
expect(bitwardenAutofillInit.insertAutofillContentService.fillForm).toHaveBeenCalledWith(
|
||||
fillScript
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupExtensionMessageListeners", () => {
|
||||
it("sets up a chrome runtime on message listener", () => {
|
||||
jest.spyOn(chrome.runtime.onMessage, "addListener");
|
||||
|
||||
bitwardenAutofillInit["setupExtensionMessageListeners"]();
|
||||
|
||||
expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith(
|
||||
bitwardenAutofillInit["handleExtensionMessage"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleExtensionMessage", () => {
|
||||
let message: AutofillExtensionMessage;
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
const sendResponse = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
message = {
|
||||
command: "collectPageDetails",
|
||||
tab: mock<chrome.tabs.Tab>(),
|
||||
sender: "sender",
|
||||
};
|
||||
sender = mock<chrome.runtime.MessageSender>();
|
||||
});
|
||||
|
||||
it("returns a false value if a extension message handler is not found with the given message command", () => {
|
||||
message.command = "unknownCommand";
|
||||
|
||||
const response = bitwardenAutofillInit["handleExtensionMessage"](
|
||||
message,
|
||||
sender,
|
||||
sendResponse
|
||||
);
|
||||
|
||||
expect(response).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a false value if the message handler does not return a response", async () => {
|
||||
const response1 = await bitwardenAutofillInit["handleExtensionMessage"](
|
||||
message,
|
||||
sender,
|
||||
sendResponse
|
||||
);
|
||||
await Promise.resolve(response1);
|
||||
|
||||
expect(response1).not.toBe(false);
|
||||
|
||||
message.command = "fillForm";
|
||||
message.fillScript = mock<AutofillScript>();
|
||||
|
||||
const response2 = await bitwardenAutofillInit["handleExtensionMessage"](
|
||||
message,
|
||||
sender,
|
||||
sendResponse
|
||||
);
|
||||
|
||||
expect(response2).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a true value and calls sendResponse if the message handler returns a response", async () => {
|
||||
message.command = "collectPageDetailsImmediately";
|
||||
const pageDetails: AutofillPageDetails = {
|
||||
title: "title",
|
||||
url: "http://example.com",
|
||||
documentUrl: "documentUrl",
|
||||
forms: {},
|
||||
fields: [],
|
||||
collectedTimestamp: 0,
|
||||
};
|
||||
jest
|
||||
.spyOn(bitwardenAutofillInit.collectAutofillContentService, "getPageDetails")
|
||||
.mockReturnValue(pageDetails);
|
||||
|
||||
const response = await bitwardenAutofillInit["handleExtensionMessage"](
|
||||
message,
|
||||
sender,
|
||||
sendResponse
|
||||
);
|
||||
await Promise.resolve(response);
|
||||
|
||||
expect(response).toBe(true);
|
||||
expect(sendResponse).toHaveBeenCalledWith(pageDetails);
|
||||
});
|
||||
});
|
||||
});
|
||||
130
apps/browser/src/autofill/content/autofill-init.ts
Normal file
130
apps/browser/src/autofill/content/autofill-init.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript from "../models/autofill-script";
|
||||
import CollectAutofillContentService from "../services/collect-autofill-content.service";
|
||||
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||
import InsertAutofillContentService from "../services/insert-autofill-content.service";
|
||||
|
||||
import {
|
||||
AutofillExtensionMessage,
|
||||
AutofillExtensionMessageHandlers,
|
||||
AutofillInit as AutofillInitInterface,
|
||||
} from "./abstractions/autofill-init";
|
||||
|
||||
class AutofillInit implements AutofillInitInterface {
|
||||
private readonly domElementVisibilityService: DomElementVisibilityService;
|
||||
private readonly collectAutofillContentService: CollectAutofillContentService;
|
||||
private readonly insertAutofillContentService: InsertAutofillContentService;
|
||||
private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = {
|
||||
collectPageDetails: ({ message }) => this.collectPageDetails(message),
|
||||
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
|
||||
fillForm: ({ message }) => this.fillForm(message.fillScript),
|
||||
};
|
||||
|
||||
/**
|
||||
* AutofillInit constructor. Initializes the DomElementVisibilityService,
|
||||
* CollectAutofillContentService and InsertAutofillContentService classes.
|
||||
*/
|
||||
constructor() {
|
||||
this.domElementVisibilityService = new DomElementVisibilityService();
|
||||
this.collectAutofillContentService = new CollectAutofillContentService(
|
||||
this.domElementVisibilityService
|
||||
);
|
||||
this.insertAutofillContentService = new InsertAutofillContentService(
|
||||
this.domElementVisibilityService,
|
||||
this.collectAutofillContentService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the autofill content script, setting up
|
||||
* the extension message listeners. This method should
|
||||
* be called once when the content script is loaded.
|
||||
* @public
|
||||
*/
|
||||
init() {
|
||||
this.setupExtensionMessageListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the page details and sends them to the
|
||||
* extension background script. If the `sendDetailsInResponse`
|
||||
* parameter is set to true, the page details will be
|
||||
* returned to facilitate sending the details in the
|
||||
* response to the extension message.
|
||||
* @param {AutofillExtensionMessage} message
|
||||
* @param {boolean} sendDetailsInResponse
|
||||
* @returns {AutofillPageDetails | void}
|
||||
* @private
|
||||
*/
|
||||
private async collectPageDetails(
|
||||
message: AutofillExtensionMessage,
|
||||
sendDetailsInResponse = false
|
||||
): Promise<AutofillPageDetails | void> {
|
||||
const pageDetails: AutofillPageDetails =
|
||||
await this.collectAutofillContentService.getPageDetails();
|
||||
if (sendDetailsInResponse) {
|
||||
return pageDetails;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
command: "collectPageDetailsResponse",
|
||||
tab: message.tab,
|
||||
details: pageDetails,
|
||||
sender: message.sender,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the form with the given fill script.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @private
|
||||
*/
|
||||
private fillForm(fillScript: AutofillScript) {
|
||||
this.insertAutofillContentService.fillForm(fillScript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extension message listeners
|
||||
* for the content script.
|
||||
* @private
|
||||
*/
|
||||
private setupExtensionMessageListeners() {
|
||||
chrome.runtime.onMessage.addListener(this.handleExtensionMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the extension messages
|
||||
* sent to the content script.
|
||||
* @param {AutofillExtensionMessage} message
|
||||
* @param {chrome.runtime.MessageSender} sender
|
||||
* @param {(response?: any) => void} sendResponse
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private handleExtensionMessage = (
|
||||
message: AutofillExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void
|
||||
): boolean => {
|
||||
const command: string = message.command;
|
||||
const handler: CallableFunction | undefined = this.extensionMessageHandlers[command];
|
||||
if (!handler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageResponse = handler({ message, sender });
|
||||
if (!messageResponse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
(function () {
|
||||
if (!window.bitwardenAutofillInit) {
|
||||
window.bitwardenAutofillInit = new AutofillInit();
|
||||
window.bitwardenAutofillInit.init();
|
||||
}
|
||||
})();
|
||||
@@ -1,4 +1,10 @@
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadAutofiller);
|
||||
} else {
|
||||
loadAutofiller();
|
||||
}
|
||||
|
||||
function loadAutofiller() {
|
||||
let pageHref: string = null;
|
||||
let filledThisHref = false;
|
||||
let delayFillTimeout: number;
|
||||
@@ -49,4 +55,4 @@ document.addEventListener("DOMContentLoaded", (event) => {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,67 +27,13 @@ interface HTMLElementWithFormOpId extends HTMLElement {
|
||||
* and async scripts to finish loading.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", async (event) => {
|
||||
// These are preferences for whether to show the notification bar based on the user's settings
|
||||
// and they are set in the Settings > Options page in the browser extension.
|
||||
let disabledAddLoginNotification = false;
|
||||
let disabledChangedPasswordNotification = false;
|
||||
let showNotificationBar = true;
|
||||
|
||||
// Look up the active user id from storage
|
||||
const activeUserIdKey = "activeUserId";
|
||||
let activeUserId: string;
|
||||
await chrome.storage.local.get(activeUserIdKey, (obj: any) => {
|
||||
if (obj == null || obj[activeUserIdKey] == null) {
|
||||
return;
|
||||
}
|
||||
activeUserId = obj[activeUserIdKey];
|
||||
});
|
||||
|
||||
// Look up the user's settings from storage
|
||||
await chrome.storage.local.get(activeUserId, (obj: any) => {
|
||||
if (obj?.[activeUserId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userSettings: UserSettings = obj[activeUserId].settings;
|
||||
|
||||
// Do not show the notification bar on the Bitwarden vault
|
||||
// because they can add logins and change passwords there
|
||||
if (window.location.origin === userSettings.serverConfig.environment.vault) {
|
||||
showNotificationBar = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// NeverDomains is a dictionary of domains that the user has chosen to never
|
||||
// show the notification bar on (for login detail collection or password change).
|
||||
// It is managed in the Settings > Excluded Domains page in the browser extension.
|
||||
// Example: '{"bitwarden.com":null}'
|
||||
const excludedDomainsDict = userSettings.neverDomains;
|
||||
|
||||
if (
|
||||
excludedDomainsDict != null &&
|
||||
// eslint-disable-next-line
|
||||
excludedDomainsDict.hasOwnProperty(window.location.hostname)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set local disabled preferences
|
||||
disabledAddLoginNotification = userSettings.disableAddLoginNotification;
|
||||
disabledChangedPasswordNotification = userSettings.disableChangedPasswordNotification;
|
||||
|
||||
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
|
||||
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
||||
handlePageChange();
|
||||
}
|
||||
});
|
||||
|
||||
if (!showNotificationBar) {
|
||||
return;
|
||||
}
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadNotificationBar);
|
||||
} else {
|
||||
loadNotificationBar();
|
||||
}
|
||||
|
||||
async function loadNotificationBar() {
|
||||
// Initialize required variables and set default values
|
||||
const watchedForms: WatchedForm[] = [];
|
||||
let barType: string = null;
|
||||
@@ -132,6 +78,53 @@ document.addEventListener("DOMContentLoaded", async (event) => {
|
||||
]);
|
||||
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
|
||||
|
||||
// These are preferences for whether to show the notification bar based on the user's settings
|
||||
// and they are set in the Settings > Options page in the browser extension.
|
||||
let disabledAddLoginNotification = false;
|
||||
let disabledChangedPasswordNotification = false;
|
||||
let showNotificationBar = true;
|
||||
|
||||
// Look up the active user id from storage
|
||||
const activeUserIdKey = "activeUserId";
|
||||
let activeUserId: string;
|
||||
|
||||
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
|
||||
if (activeUserStorageValue[activeUserIdKey]) {
|
||||
activeUserId = activeUserStorageValue[activeUserIdKey];
|
||||
}
|
||||
|
||||
// Look up the user's settings from storage
|
||||
const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
|
||||
if (userSettingsStorageValue[activeUserId]) {
|
||||
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
|
||||
|
||||
// Do not show the notification bar on the Bitwarden vault
|
||||
// because they can add logins and change passwords there
|
||||
if (window.location.origin === userSettings.serverConfig.environment.vault) {
|
||||
showNotificationBar = false;
|
||||
} else {
|
||||
// NeverDomains is a dictionary of domains that the user has chosen to never
|
||||
// show the notification bar on (for login detail collection or password change).
|
||||
// It is managed in the Settings > Excluded Domains page in the browser extension.
|
||||
// Example: '{"bitwarden.com":null}'
|
||||
const excludedDomainsDict = userSettings.neverDomains;
|
||||
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
|
||||
// Set local disabled preferences
|
||||
disabledAddLoginNotification = userSettings.disableAddLoginNotification;
|
||||
disabledChangedPasswordNotification = userSettings.disableChangedPasswordNotification;
|
||||
|
||||
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
|
||||
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
||||
handlePageChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!showNotificationBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Message Processing
|
||||
|
||||
// Listen for messages from the background script
|
||||
@@ -1002,4 +995,10 @@ document.addEventListener("DOMContentLoaded", async (event) => {
|
||||
}
|
||||
|
||||
// End Helper Functions
|
||||
});
|
||||
}
|
||||
|
||||
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
describe("TriggerAutofillScriptInjection", () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("sends a message to the extension background", () => {
|
||||
require("../content/trigger-autofill-script-injection");
|
||||
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
command: "triggerAutofillScriptInjection",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
(function () {
|
||||
chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
|
||||
})();
|
||||
7
apps/browser/src/autofill/globals.d.ts
vendored
Normal file
7
apps/browser/src/autofill/globals.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AutofillInit } from "./content/abstractions/autofill-init";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
bitwardenAutofillInit?: AutofillInit;
|
||||
}
|
||||
}
|
||||
131
apps/browser/src/autofill/jest/autofill-mocks.ts
Normal file
131
apps/browser/src/autofill/jest/autofill-mocks.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { UriMatchType } from "@bitwarden/common/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript, { FillScript } from "../models/autofill-script";
|
||||
import { GenerateFillScriptOptions } from "../services/abstractions/autofill.service";
|
||||
|
||||
function createAutofillFieldMock(customFields = {}): AutofillField {
|
||||
return {
|
||||
opid: "default-input-field-opid",
|
||||
elementNumber: 0,
|
||||
viewable: true,
|
||||
htmlID: "default-htmlID",
|
||||
htmlName: "default-htmlName",
|
||||
htmlClass: "default-htmlClass",
|
||||
tabindex: "0",
|
||||
title: "default-title",
|
||||
"label-left": "default-label-left",
|
||||
"label-right": "default-label-right",
|
||||
"label-top": "default-label-top",
|
||||
"label-tag": "default-label-tag",
|
||||
"label-aria": "default-label-aria",
|
||||
placeholder: "default-placeholder",
|
||||
type: "text",
|
||||
value: "default-value",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
onePasswordFieldType: "",
|
||||
form: "invalidFormId",
|
||||
autoCompleteType: "off",
|
||||
selectInfo: "",
|
||||
maxLength: 0,
|
||||
tagName: "input",
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
||||
function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails {
|
||||
return {
|
||||
title: "title",
|
||||
url: "url",
|
||||
documentUrl: "documentUrl",
|
||||
forms: {
|
||||
validFormId: {
|
||||
opid: "opid",
|
||||
htmlName: "htmlName",
|
||||
htmlID: "htmlID",
|
||||
htmlAction: "htmlAction",
|
||||
htmlMethod: "htmlMethod",
|
||||
},
|
||||
},
|
||||
fields: [createAutofillFieldMock({ opid: "non-password-field" })],
|
||||
collectedTimestamp: 0,
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
||||
function createChromeTabMock(customFields = {}): chrome.tabs.Tab {
|
||||
return {
|
||||
id: 1,
|
||||
index: 1,
|
||||
pinned: false,
|
||||
highlighted: false,
|
||||
windowId: 2,
|
||||
active: true,
|
||||
incognito: false,
|
||||
selected: true,
|
||||
discarded: false,
|
||||
autoDiscardable: false,
|
||||
groupId: 2,
|
||||
url: "https://tacos.com",
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
||||
function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions {
|
||||
return {
|
||||
skipUsernameOnlyFill: false,
|
||||
onlyEmptyFields: false,
|
||||
onlyVisibleFields: false,
|
||||
fillNewPassword: false,
|
||||
allowTotpAutofill: false,
|
||||
cipher: mock<CipherView>(),
|
||||
tabUrl: "https://tacos.com",
|
||||
defaultUriMatch: UriMatchType.Domain,
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
||||
function createAutofillScriptMock(
|
||||
customFields = {},
|
||||
scriptTypes?: Record<string, string>
|
||||
): AutofillScript {
|
||||
let script: FillScript[] = [
|
||||
["click_on_opid", "default-field"],
|
||||
["focus_by_opid", "default-field"],
|
||||
["fill_by_opid", "default-field", "default"],
|
||||
];
|
||||
if (scriptTypes) {
|
||||
script = [];
|
||||
for (const scriptType in scriptTypes) {
|
||||
script.push(["click_on_opid", scriptType]);
|
||||
script.push(["focus_by_opid", scriptType]);
|
||||
script.push(["fill_by_opid", scriptType, scriptTypes[scriptType]]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
autosubmit: null,
|
||||
metadata: {},
|
||||
properties: {
|
||||
delay_between_operations: 20,
|
||||
},
|
||||
savedUrls: [],
|
||||
script,
|
||||
itemType: "",
|
||||
untrustedIframe: false,
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
createAutofillFieldMock,
|
||||
createAutofillPageDetailsMock,
|
||||
createChromeTabMock,
|
||||
createGenerateFillScriptOptionsMock,
|
||||
createAutofillScriptMock,
|
||||
};
|
||||
5
apps/browser/src/autofill/jest/testing-utils.ts
Normal file
5
apps/browser/src/autofill/jest/testing-utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
function triggerTestFailure() {
|
||||
expect(true).toBe("Test has failed.");
|
||||
}
|
||||
|
||||
export { triggerTestFailure };
|
||||
@@ -2,6 +2,7 @@
|
||||
* Represents a single field that is collected from the page source and is potentially autofilled.
|
||||
*/
|
||||
export default class AutofillField {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* The unique identifier assigned to this field during collection of the page details
|
||||
*/
|
||||
@@ -11,10 +12,6 @@ export default class AutofillField {
|
||||
* Used to do perform proximal checks for username and password fields on the DOM.
|
||||
*/
|
||||
elementNumber: number;
|
||||
/**
|
||||
* Designates whether the field is visible, based on the element's style
|
||||
*/
|
||||
visible: boolean;
|
||||
/**
|
||||
* Designates whether the field is viewable on the current part of the DOM that the user can see
|
||||
*/
|
||||
@@ -22,80 +19,91 @@ export default class AutofillField {
|
||||
/**
|
||||
* The HTML `id` attribute of the field
|
||||
*/
|
||||
htmlID: string;
|
||||
htmlID: string | null;
|
||||
/**
|
||||
* The HTML `name` attribute of the field
|
||||
*/
|
||||
htmlName: string;
|
||||
htmlName: string | null;
|
||||
/**
|
||||
* The HTML `class` attribute of the field
|
||||
*/
|
||||
htmlClass: string;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all the elements that are to the "left" of the field in the DOM
|
||||
*/
|
||||
"label-left": string;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all the elements that are to the "right" of the field in the DOM
|
||||
*/
|
||||
"label-right": string;
|
||||
/**
|
||||
* For fields in a data table, the contents of the table row immediately above the field
|
||||
*/
|
||||
"label-top": string;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all elements that are HTML labels for the field
|
||||
*/
|
||||
"label-tag": string;
|
||||
/**
|
||||
* The `aria-label` attribute for the field
|
||||
*/
|
||||
"label-aria": string;
|
||||
/**
|
||||
* The HTML `placeholder` attribute for the field
|
||||
*/
|
||||
placeholder: string;
|
||||
/**
|
||||
* The HTML `type` attribute for the field
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* The HTML `value` for the field
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The `disabled` status of the field
|
||||
*/
|
||||
disabled: boolean;
|
||||
/**
|
||||
* The `readonly` status of the field
|
||||
*/
|
||||
readonly: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
* The `onePasswordFieldType` from the `dataset` on the element.
|
||||
* If empty it contains the HTML `type` attribute for the field.
|
||||
*/
|
||||
onePasswordFieldType: string;
|
||||
/**
|
||||
* The `opid` attribute value of the form that contains the field
|
||||
*/
|
||||
form: string;
|
||||
/**
|
||||
* The `x-autocompletetype`, `autocompletetype`, or `autocomplete` attribute for the field
|
||||
*/
|
||||
autoCompleteType: string;
|
||||
/**
|
||||
* For `<select>` elements, an array of the element's option `text` values
|
||||
*/
|
||||
selectInfo: any;
|
||||
/**
|
||||
* The `maxLength` attribute for the field
|
||||
*/
|
||||
maxLength: number;
|
||||
htmlClass: string | null;
|
||||
|
||||
tabindex: string | null;
|
||||
|
||||
title: string | null;
|
||||
/**
|
||||
* The `tagName` for the field
|
||||
*/
|
||||
tagName: string;
|
||||
[key: string]: any;
|
||||
tagName?: string | null;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all the elements that are to the "left" of the field in the DOM
|
||||
*/
|
||||
"label-left"?: string;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all the elements that are to the "right" of the field in the DOM
|
||||
*/
|
||||
"label-right"?: string;
|
||||
/**
|
||||
* For fields in a data table, the contents of the table row immediately above the field
|
||||
*/
|
||||
"label-top"?: string;
|
||||
/**
|
||||
* The concatenated `innerText` or `textContent` of all elements that are HTML labels for the field
|
||||
*/
|
||||
"label-tag"?: string;
|
||||
/**
|
||||
* The `aria-label` attribute for the field
|
||||
*/
|
||||
"label-aria"?: string | null;
|
||||
|
||||
"label-data"?: string | null;
|
||||
|
||||
"aria-hidden"?: boolean;
|
||||
|
||||
"aria-disabled"?: boolean;
|
||||
|
||||
"aria-haspopup"?: boolean;
|
||||
|
||||
"data-stripe"?: string | null;
|
||||
/**
|
||||
* The HTML `placeholder` attribute for the field
|
||||
*/
|
||||
placeholder?: string | null;
|
||||
/**
|
||||
* The HTML `type` attribute for the field
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* The HTML `value` for the field
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* The `disabled` status of the field
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The `readonly` status of the field
|
||||
*/
|
||||
readonly?: boolean;
|
||||
/**
|
||||
* The `opid` attribute value of the form that contains the field
|
||||
*/
|
||||
form?: string;
|
||||
/**
|
||||
* The `x-autocompletetype`, `autocompletetype`, or `autocomplete` attribute for the field
|
||||
*/
|
||||
autoCompleteType?: string | null;
|
||||
/**
|
||||
* For `<select>` elements, an array of the element's option `text` values
|
||||
*/
|
||||
selectInfo?: any;
|
||||
/**
|
||||
* The `maxLength` attribute for the field
|
||||
*/
|
||||
maxLength?: number | null;
|
||||
|
||||
rel?: string | null;
|
||||
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import AutofillForm from "./autofill-form";
|
||||
* The details of a page that have been collected and can be used for autofill
|
||||
*/
|
||||
export default class AutofillPageDetails {
|
||||
/**
|
||||
* A unique identifier for the page
|
||||
*/
|
||||
documentUUID: string;
|
||||
title: string;
|
||||
url: string;
|
||||
documentUrl: string;
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
// String values affect code flow in autofill.ts and must not be changed
|
||||
export type FillScriptOp = "click_on_opid" | "focus_by_opid" | "fill_by_opid" | "delay";
|
||||
export type FillScriptActions = "click_on_opid" | "focus_by_opid" | "fill_by_opid";
|
||||
|
||||
export type FillScript = [op: FillScriptOp, opid: string, value?: string];
|
||||
|
||||
export type AutofillScriptOptions = {
|
||||
animate?: boolean;
|
||||
markFilling?: boolean;
|
||||
};
|
||||
export type FillScript = [action: FillScriptActions, opid: string, value?: string];
|
||||
|
||||
export type AutofillScriptProperties = {
|
||||
delay_between_operations?: number;
|
||||
};
|
||||
|
||||
export type AutofillInsertActions = {
|
||||
fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void;
|
||||
click_on_opid: ({ opid }: { opid: string }) => void;
|
||||
focus_by_opid: ({ opid }: { opid: string }) => void;
|
||||
};
|
||||
|
||||
export default class AutofillScript {
|
||||
script: FillScript[] = [];
|
||||
documentUUID = "";
|
||||
properties: AutofillScriptProperties = {};
|
||||
options: AutofillScriptOptions = {};
|
||||
metadata: any = {}; // Unused, not written or read
|
||||
autosubmit: any = null; // Appears to be unused, read but not written
|
||||
savedUrls: string[];
|
||||
untrustedIframe: boolean;
|
||||
itemType: string; // Appears to be unused, read but not written
|
||||
|
||||
constructor(documentUUID: string) {
|
||||
this.documentUUID = documentUUID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UriMatchType } from "@bitwarden/common/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
@@ -31,13 +32,28 @@ export interface FormData {
|
||||
passwords: AutofillField[];
|
||||
}
|
||||
|
||||
export interface GenerateFillScriptOptions {
|
||||
skipUsernameOnlyFill: boolean;
|
||||
onlyEmptyFields: boolean;
|
||||
onlyVisibleFields: boolean;
|
||||
fillNewPassword: boolean;
|
||||
allowTotpAutofill: boolean;
|
||||
cipher: CipherView;
|
||||
tabUrl: string;
|
||||
defaultUriMatch: UriMatchType;
|
||||
}
|
||||
|
||||
export abstract class AutofillService {
|
||||
injectAutofillScripts: (
|
||||
sender: chrome.runtime.MessageSender,
|
||||
autofillV2?: boolean
|
||||
) => Promise<void>;
|
||||
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
||||
doAutoFill: (options: AutoFillOptions) => Promise<string>;
|
||||
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
||||
doAutoFillOnTab: (
|
||||
pageDetails: PageDetail[],
|
||||
tab: chrome.tabs.Tab,
|
||||
fromCommand: boolean
|
||||
) => Promise<string>;
|
||||
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string>;
|
||||
) => Promise<string | null>;
|
||||
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string | null>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
interface CollectAutofillContentService {
|
||||
getPageDetails(): Promise<AutofillPageDetails>;
|
||||
getAutofillFieldElementByOpid(opid: string): HTMLElement | null;
|
||||
}
|
||||
|
||||
export { CollectAutofillContentService };
|
||||
@@ -0,0 +1,6 @@
|
||||
interface DomElementVisibilityService {
|
||||
isFormFieldViewable: (element: HTMLElement) => Promise<boolean>;
|
||||
isElementHiddenByCss: (element: HTMLElement) => boolean;
|
||||
}
|
||||
|
||||
export { DomElementVisibilityService };
|
||||
@@ -0,0 +1,7 @@
|
||||
import AutofillScript from "../../models/autofill-script";
|
||||
|
||||
interface InsertAutofillContentService {
|
||||
fillForm(fillScript: AutofillScript): void;
|
||||
}
|
||||
|
||||
export { InsertAutofillContentService };
|
||||
4226
apps/browser/src/autofill/services/autofill.service.spec.ts
Normal file
4226
apps/browser/src/autofill/services/autofill.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import {
|
||||
AutofillService as AutofillServiceInterface,
|
||||
PageDetail,
|
||||
FormData,
|
||||
GenerateFillScriptOptions,
|
||||
} from "./abstractions/autofill.service";
|
||||
import {
|
||||
AutoFillConstants,
|
||||
@@ -28,17 +29,6 @@ import {
|
||||
IdentityAutoFillConstants,
|
||||
} from "./autofill-constants";
|
||||
|
||||
export interface GenerateFillScriptOptions {
|
||||
skipUsernameOnlyFill: boolean;
|
||||
onlyEmptyFields: boolean;
|
||||
onlyVisibleFields: boolean;
|
||||
fillNewPassword: boolean;
|
||||
allowTotpAutofill: boolean;
|
||||
cipher: CipherView;
|
||||
tabUrl: string;
|
||||
defaultUriMatch: UriMatchType;
|
||||
}
|
||||
|
||||
export default class AutofillService implements AutofillServiceInterface {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
@@ -50,6 +40,40 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Injects the autofill scripts into the current tab and all frames
|
||||
* found within the tab. Temporarily, will conditionally inject
|
||||
* the refactor of the core autofill script if the feature flag
|
||||
* is enabled.
|
||||
* @param {chrome.runtime.MessageSender} sender
|
||||
* @param {boolean} autofillV2
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async injectAutofillScripts(sender: chrome.runtime.MessageSender, autofillV2 = false) {
|
||||
const mainAutofillScript = autofillV2 ? `autofill-init.js` : "autofill.js";
|
||||
|
||||
const injectedScripts = [
|
||||
mainAutofillScript,
|
||||
"autofiller.js",
|
||||
"notificationBar.js",
|
||||
"contextMenuHandler.js",
|
||||
];
|
||||
|
||||
for (const injectedScript of injectedScripts) {
|
||||
await BrowserApi.executeScriptInTab(sender.tab.id, {
|
||||
file: `content/${injectedScript}`,
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all forms with password fields and formats the data
|
||||
* for both forms and password input elements.
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @returns {FormData[]}
|
||||
*/
|
||||
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] {
|
||||
const formData: FormData[] = [];
|
||||
|
||||
@@ -114,11 +138,11 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofills a given tab with a given login item
|
||||
* @param options Instructions about the autofill operation, including tab and login item
|
||||
* @returns The TOTP code of the successfully autofilled login, if any
|
||||
* Autofill a given tab with a given login item
|
||||
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
|
||||
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
||||
*/
|
||||
async doAutoFill(options: AutoFillOptions): Promise<string> {
|
||||
async doAutoFill(options: AutoFillOptions): Promise<string | null> {
|
||||
const tab = options.tab;
|
||||
if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) {
|
||||
throw new Error("Nothing to auto-fill.");
|
||||
@@ -210,17 +234,17 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofills the specified tab with the next login item from the cache
|
||||
* @param pageDetails The data scraped from the page
|
||||
* @param tab The tab to be autofilled
|
||||
* @param fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||
* @returns The TOTP code of the successfully autofilled login, if any
|
||||
* Autofill the specified tab with the next login item from the cache
|
||||
* @param {PageDetail[]} pageDetails The data scraped from the page
|
||||
* @param {chrome.tabs.Tab} tab The tab to be autofilled
|
||||
* @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
||||
*/
|
||||
async doAutoFillOnTab(
|
||||
pageDetails: PageDetail[],
|
||||
tab: chrome.tabs.Tab,
|
||||
fromCommand: boolean
|
||||
): Promise<string> {
|
||||
): Promise<string | null> {
|
||||
let cipher: CipherView;
|
||||
if (fromCommand) {
|
||||
cipher = await this.cipherService.getNextCipherForUrl(tab.url);
|
||||
@@ -241,9 +265,14 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
if (
|
||||
cipher.reprompt !== CipherRepromptType.None &&
|
||||
cipher.reprompt === CipherRepromptType.Password &&
|
||||
// If the master password has is not available, reprompt will error
|
||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
||||
) {
|
||||
if (fromCommand) {
|
||||
this.cipherService.updateLastUsedIndexForUrl(tab.url);
|
||||
}
|
||||
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
cipherId: cipher.id,
|
||||
action: "autofill",
|
||||
@@ -265,7 +294,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
allowTotpAutofill: fromCommand,
|
||||
});
|
||||
|
||||
// Update last used index as autofill has succeed
|
||||
// Update last used index as autofill has succeeded
|
||||
if (fromCommand) {
|
||||
this.cipherService.updateLastUsedIndexForUrl(tab.url);
|
||||
}
|
||||
@@ -274,22 +303,29 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofills the active tab with the next login item from the cache
|
||||
* @param pageDetails The data scraped from the page
|
||||
* @param fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||
* @returns The TOTP code of the successfully autofilled login, if any
|
||||
* Autofill the active tab with the next login item from the cache
|
||||
* @param {PageDetail[]} pageDetails The data scraped from the page
|
||||
* @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
||||
*/
|
||||
async doAutoFillActiveTab(pageDetails: PageDetail[], fromCommand: boolean): Promise<string> {
|
||||
async doAutoFillActiveTab(
|
||||
pageDetails: PageDetail[],
|
||||
fromCommand: boolean
|
||||
): Promise<string | null> {
|
||||
const tab = await this.getActiveTab();
|
||||
if (!tab || !tab.url) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/**
|
||||
* Gets the active tab from the current window.
|
||||
* Throws an error if no tab is found.
|
||||
* @returns {Promise<chrome.tabs.Tab>}
|
||||
* @private
|
||||
*/
|
||||
private async getActiveTab(): Promise<chrome.tabs.Tab> {
|
||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (!tab) {
|
||||
@@ -299,15 +335,22 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the autofill script for the specified page details and cipher.
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {GenerateFillScriptOptions} options
|
||||
* @returns {Promise<AutofillScript | null>}
|
||||
* @private
|
||||
*/
|
||||
private async generateFillScript(
|
||||
pageDetails: AutofillPageDetails,
|
||||
options: GenerateFillScriptOptions
|
||||
): Promise<AutofillScript> {
|
||||
): Promise<AutofillScript | null> {
|
||||
if (!pageDetails || !options.cipher) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fillScript = new AutofillScript(pageDetails.documentUUID);
|
||||
let fillScript = new AutofillScript();
|
||||
const filledFields: { [id: string]: AutofillField } = {};
|
||||
const fields = options.cipher.fields;
|
||||
|
||||
@@ -377,12 +420,21 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the autofill script for the specified page details and login cipher item.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @param {GenerateFillScriptOptions} options
|
||||
* @returns {Promise<AutofillScript | null>}
|
||||
* @private
|
||||
*/
|
||||
private async generateLoginFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
options: GenerateFillScriptOptions
|
||||
): Promise<AutofillScript> {
|
||||
): Promise<AutofillScript | null> {
|
||||
if (!options.cipher.login) {
|
||||
return null;
|
||||
}
|
||||
@@ -551,12 +603,21 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the autofill script for the specified page details and credit card cipher item.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @param {GenerateFillScriptOptions} options
|
||||
* @returns {AutofillScript|null}
|
||||
* @private
|
||||
*/
|
||||
private generateCardFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
options: GenerateFillScriptOptions
|
||||
): AutofillScript {
|
||||
): AutofillScript | null {
|
||||
if (!options.cipher.card) {
|
||||
return null;
|
||||
}
|
||||
@@ -872,9 +933,10 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
|
||||
/**
|
||||
* Determines whether an iframe is potentially dangerous ("untrusted") to autofill
|
||||
* @param pageUrl The url of the page/iframe, usually from AutofillPageDetails
|
||||
* @param options The GenerateFillScript options
|
||||
* @returns `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
|
||||
* @param {string} pageUrl The url of the page/iframe, usually from AutofillPageDetails
|
||||
* @param {GenerateFillScriptOptions} options The GenerateFillScript options
|
||||
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
|
||||
* @private
|
||||
*/
|
||||
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean {
|
||||
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
|
||||
@@ -895,7 +957,15 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return !matchesUri;
|
||||
}
|
||||
|
||||
private fieldAttrsContain(field: AutofillField, containsVal: string) {
|
||||
/**
|
||||
* Used when handling autofill on credit card fields. Determines whether
|
||||
* the field has an attribute that matches the given value.
|
||||
* @param {AutofillField} field
|
||||
* @param {string} containsVal
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private fieldAttrsContain(field: AutofillField, containsVal: string): boolean {
|
||||
if (!field) {
|
||||
return false;
|
||||
}
|
||||
@@ -915,6 +985,15 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return doesContain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the autofill script for the specified page details and identify cipher item.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @param {GenerateFillScriptOptions} options
|
||||
* @returns {AutofillScript}
|
||||
* @private
|
||||
*/
|
||||
private generateIdentityFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
@@ -1149,10 +1228,29 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an HTMLInputElement type value and a list of
|
||||
* excluded types and returns true if the type is excluded.
|
||||
* @param {string} type
|
||||
* @param {string[]} excludedTypes
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isExcludedType(type: string, excludedTypes: string[]) {
|
||||
return excludedTypes.indexOf(type) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the value of a field, a list of possible options that define if
|
||||
* a field can be matched to a vault cipher, and a secondary optional list
|
||||
* of options that define if a field can be matched to a vault cipher. Returns
|
||||
* true if the field value matches one of the options.
|
||||
* @param {string} value
|
||||
* @param {string[]} options
|
||||
* @param {string[]} containsOptions
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private static isFieldMatch(
|
||||
value: string,
|
||||
options: string[],
|
||||
@@ -1174,6 +1272,17 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method used to create a script action for a field. Conditionally
|
||||
* accepts a fieldProp value that will be used in place of the dataProp value.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param cipherData
|
||||
* @param {{[p: string]: AutofillField}} fillFields
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @param {string} dataProp
|
||||
* @param {string} fieldProp
|
||||
* @private
|
||||
*/
|
||||
private makeScriptAction(
|
||||
fillScript: AutofillScript,
|
||||
cipherData: any,
|
||||
@@ -1191,6 +1300,17 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updating the list of filled fields and adding a script action
|
||||
* to the fill script. If a select field is passed as part of the fill options,
|
||||
* we iterate over the options to check if the passed value matches one of the
|
||||
* options. If it does, we add a script action to select the option.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param dataValue
|
||||
* @param {AutofillField} field
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @private
|
||||
*/
|
||||
private makeScriptActionWithValue(
|
||||
fillScript: AutofillScript,
|
||||
dataValue: any,
|
||||
@@ -1230,6 +1350,16 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a pageDetails object with a list of fields and returns a list of
|
||||
* fields that are likely to be password fields.
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {boolean} canBeHidden
|
||||
* @param {boolean} canBeReadOnly
|
||||
* @param {boolean} mustBeEmpty
|
||||
* @param {boolean} fillNewPassword
|
||||
* @returns {AutofillField[]}
|
||||
*/
|
||||
static loadPasswordFields(
|
||||
pageDetails: AutofillPageDetails,
|
||||
canBeHidden: boolean,
|
||||
@@ -1291,13 +1421,24 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a pageDetails object with a list of fields and returns a list of
|
||||
* fields that are likely to be username fields.
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {AutofillField} passwordField
|
||||
* @param {boolean} canBeHidden
|
||||
* @param {boolean} canBeReadOnly
|
||||
* @param {boolean} withoutForm
|
||||
* @returns {AutofillField}
|
||||
* @private
|
||||
*/
|
||||
private findUsernameField(
|
||||
pageDetails: AutofillPageDetails,
|
||||
passwordField: AutofillField,
|
||||
canBeHidden: boolean,
|
||||
canBeReadOnly: boolean,
|
||||
withoutForm: boolean
|
||||
) {
|
||||
): AutofillField | null {
|
||||
let usernameField: AutofillField = null;
|
||||
for (let i = 0; i < pageDetails.fields.length; i++) {
|
||||
const f = pageDetails.fields[i];
|
||||
@@ -1328,13 +1469,24 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return usernameField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a pageDetails object with a list of fields and returns a list of
|
||||
* fields that are likely to be TOTP fields.
|
||||
* @param {AutofillPageDetails} pageDetails
|
||||
* @param {AutofillField} passwordField
|
||||
* @param {boolean} canBeHidden
|
||||
* @param {boolean} canBeReadOnly
|
||||
* @param {boolean} withoutForm
|
||||
* @returns {AutofillField}
|
||||
* @private
|
||||
*/
|
||||
private findTotpField(
|
||||
pageDetails: AutofillPageDetails,
|
||||
passwordField: AutofillField,
|
||||
canBeHidden: boolean,
|
||||
canBeReadOnly: boolean,
|
||||
withoutForm: boolean
|
||||
) {
|
||||
): AutofillField | null {
|
||||
let totpField: AutofillField = null;
|
||||
for (let i = 0; i < pageDetails.fields.length; i++) {
|
||||
const f = pageDetails.fields[i];
|
||||
@@ -1365,6 +1517,14 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return totpField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a field and returns the index of the first matching property
|
||||
* present in a list of attribute names.
|
||||
* @param {AutofillField} field
|
||||
* @param {string[]} names
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
private findMatchingFieldIndex(field: AutofillField, names: string[]): number {
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (names[i].indexOf("=") > -1) {
|
||||
@@ -1417,6 +1577,17 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a field, property, name, and prefix and returns true if the field
|
||||
* contains a value that matches the given prefixed property.
|
||||
* @param field
|
||||
* @param {string} property
|
||||
* @param {string} name
|
||||
* @param {string} prefix
|
||||
* @param {string} separator
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private fieldPropertyIsPrefixMatch(
|
||||
field: any,
|
||||
property: string,
|
||||
@@ -1432,6 +1603,18 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a given property within a field matches the value
|
||||
* of the passed "name" parameter. If the name starts with "regex=",
|
||||
* the value is tested against a case-insensitive regular expression.
|
||||
* If the name starts with "csv=", the value is treated as a
|
||||
* comma-separated list of values to match.
|
||||
* @param field
|
||||
* @param {string} property
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private fieldPropertyIsMatch(field: any, property: string, name: string): boolean {
|
||||
let fieldVal = field[property] as string;
|
||||
if (!AutofillService.hasValue(fieldVal)) {
|
||||
@@ -1466,6 +1649,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fieldVal.toLowerCase() === name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a field and returns true if the field contains a
|
||||
* value that matches any of the names in the provided list.
|
||||
* @param {AutofillField} field
|
||||
* @param {string[]} names
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean {
|
||||
if (AutofillService.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) {
|
||||
return true;
|
||||
@@ -1504,6 +1694,14 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a list of options and a value and returns
|
||||
* true if the value matches any of the options.
|
||||
* @param {string[]} options
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private static fuzzyMatch(options: string[], value: string): boolean {
|
||||
if (options == null || options.length === 0 || value == null || value === "") {
|
||||
return false;
|
||||
@@ -1523,10 +1721,23 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a string and returns true if the
|
||||
* string is not falsy and not empty.
|
||||
* @param {string} str
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static hasValue(str: string): boolean {
|
||||
return Boolean(str && str !== "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `focus_by_opid` autofill script
|
||||
* action to the last field that was filled.
|
||||
* @param {{[p: string]: AutofillField}} filledFields
|
||||
* @param {AutofillScript} fillScript
|
||||
* @returns {AutofillScript}
|
||||
*/
|
||||
static setFillScriptForFocus(
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
fillScript: AutofillScript
|
||||
@@ -1555,6 +1766,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a fill script to place the `cilck_on_opid`, `focus_on_opid`, and `fill_by_opid`
|
||||
* fill script actions associated with the provided field.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param {AutofillField} field
|
||||
* @param {string} value
|
||||
*/
|
||||
static fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void {
|
||||
if (field.maxLength && value && value.length > field.maxLength) {
|
||||
value = value.substr(0, value.length);
|
||||
@@ -1566,6 +1784,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
fillScript.script.push(["fill_by_opid", field.opid, value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the field is a custom field, a custom
|
||||
* field is defined as a field that is a `span` element.
|
||||
* @param {AutofillField} field
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static forCustomFieldsOnly(field: AutofillField): boolean {
|
||||
return field.tagName === "span";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,578 @@
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillForm from "../models/autofill-form";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import {
|
||||
ElementWithOpId,
|
||||
FillableFormFieldElement,
|
||||
FormFieldElement,
|
||||
FormElementWithAttribute,
|
||||
} from "../types";
|
||||
|
||||
import { CollectAutofillContentService as CollectAutofillContentServiceInterface } from "./abstractions/collect-autofill-content.service";
|
||||
import DomElementVisibilityService from "./dom-element-visibility.service";
|
||||
|
||||
class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
|
||||
private readonly domElementVisibilityService: DomElementVisibilityService;
|
||||
|
||||
constructor(domElementVisibilityService: DomElementVisibilityService) {
|
||||
this.domElementVisibilityService = domElementVisibilityService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the data for all the forms and fields
|
||||
* that are found within the page DOM.
|
||||
* @returns {Promise<AutofillPageDetails>}
|
||||
* @public
|
||||
*/
|
||||
async getPageDetails(): Promise<AutofillPageDetails> {
|
||||
const autofillFormsData: Record<string, AutofillForm> = this.buildAutofillFormsData();
|
||||
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData();
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
url: (document.defaultView || window).location.href,
|
||||
documentUrl: document.location.href,
|
||||
forms: autofillFormsData,
|
||||
fields: autofillFieldsData,
|
||||
collectedTimestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an AutofillField element by its opid, will only return the first
|
||||
* element if there are multiple elements with the same opid. If no
|
||||
* element is found, null will be returned.
|
||||
* @param {string} opid
|
||||
* @returns {FormFieldElement | null}
|
||||
*/
|
||||
getAutofillFieldElementByOpid(opid: string): FormFieldElement | null {
|
||||
const fieldElements = this.getAutofillFieldElements();
|
||||
const fieldElementsWithOpid = fieldElements.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;
|
||||
}
|
||||
|
||||
if (fieldElementsWithOpid.length > 1) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`More than one element found with opid ${opid}`);
|
||||
}
|
||||
|
||||
return fieldElementsWithOpid[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
formElement.opid = `__form__${index}`;
|
||||
|
||||
autofillForms[formElement.opid] = {
|
||||
opid: formElement.opid,
|
||||
htmlAction: new URL(
|
||||
this.getPropertyOrAttribute(formElement, "action"),
|
||||
window.location.href
|
||||
).href,
|
||||
htmlName: this.getPropertyOrAttribute(formElement, "name"),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, "id"),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
|
||||
};
|
||||
});
|
||||
|
||||
return autofillForms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the DOM for all the field elements and
|
||||
* returns a list of AutofillField objects.
|
||||
* @returns {Promise<AutofillField[]>}
|
||||
* @private
|
||||
*/
|
||||
private async buildAutofillFieldsData(): Promise<AutofillField[]> {
|
||||
const autofillFieldElements = this.getAutofillFieldElements(50);
|
||||
const autofillFieldDataPromises = autofillFieldElements.map(this.buildAutofillFieldItem);
|
||||
|
||||
return Promise.all(autofillFieldDataPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the DOM for all the field elements that can be autofilled,
|
||||
* 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
|
||||
* @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>),
|
||||
];
|
||||
|
||||
if (!fieldsLimit || formFieldElements.length <= fieldsLimit) {
|
||||
return formFieldElements;
|
||||
}
|
||||
|
||||
const priorityFormFields: FormFieldElement[] = [];
|
||||
const unimportantFormFields: FormFieldElement[] = [];
|
||||
const unimportantFieldTypesSet = new Set(["checkbox", "radio"]);
|
||||
for (const element of formFieldElements) {
|
||||
if (priorityFormFields.length >= fieldsLimit) {
|
||||
return priorityFormFields;
|
||||
}
|
||||
|
||||
const fieldType = this.getPropertyOrAttribute(element, "type")?.toLowerCase();
|
||||
if (unimportantFieldTypesSet.has(fieldType)) {
|
||||
unimportantFormFields.push(element);
|
||||
continue;
|
||||
}
|
||||
|
||||
priorityFormFields.push(element);
|
||||
}
|
||||
|
||||
const numberUnimportantFieldsToInclude = fieldsLimit - priorityFormFields.length;
|
||||
for (let index = 0; index < numberUnimportantFieldsToInclude; index++) {
|
||||
priorityFormFields.push(unimportantFormFields[index]);
|
||||
}
|
||||
|
||||
return priorityFormFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an AutofillField object from the given form element. Will only return
|
||||
* shared field values if the element is a span element. Will not return any label
|
||||
* values if the element is a hidden input element.
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @param {number} index
|
||||
* @returns {Promise<AutofillField>}
|
||||
* @private
|
||||
*/
|
||||
private buildAutofillFieldItem = async (
|
||||
element: ElementWithOpId<FormFieldElement>,
|
||||
index: number
|
||||
): Promise<AutofillField> => {
|
||||
element.opid = `__${index}`;
|
||||
|
||||
const autofillFieldBase = {
|
||||
opid: element.opid,
|
||||
elementNumber: index,
|
||||
maxLength: this.getAutofillFieldMaxLength(element),
|
||||
viewable: await this.domElementVisibilityService.isFormFieldViewable(element),
|
||||
htmlID: this.getPropertyOrAttribute(element, "id"),
|
||||
htmlName: this.getPropertyOrAttribute(element, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
||||
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
||||
title: this.getPropertyOrAttribute(element, "title"),
|
||||
tagName: this.getPropertyOrAttribute(element, "tagName")?.toLowerCase(),
|
||||
};
|
||||
|
||||
if (element instanceof HTMLSpanElement) {
|
||||
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();
|
||||
if (elementType !== "hidden") {
|
||||
autofillFieldLabels = {
|
||||
"label-tag": this.createAutofillFieldLabelTag(element),
|
||||
"label-data": this.getPropertyOrAttribute(element, "data-label"),
|
||||
"label-aria": this.getPropertyOrAttribute(element, "aria-label"),
|
||||
"label-top": this.createAutofillFieldTopLabel(element),
|
||||
"label-right": this.createAutofillFieldRightLabel(element),
|
||||
"label-left": this.createAutofillFieldLeftLabel(element),
|
||||
placeholder: this.getPropertyOrAttribute(element, "placeholder"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...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")),
|
||||
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",
|
||||
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* associated description term element if no other labels can be found.
|
||||
* Returns a string containing all the `textContent` or `innerText`
|
||||
* values of the label elements.
|
||||
* @param {FillableFormFieldElement} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
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));
|
||||
|
||||
let currentElement: HTMLElement | null = element;
|
||||
while (currentElement && currentElement !== document.documentElement) {
|
||||
if (currentElement instanceof HTMLLabelElement) {
|
||||
labelElementsSet.add(currentElement);
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement.closest("label");
|
||||
}
|
||||
|
||||
if (
|
||||
!labelElementsSet.size &&
|
||||
element.parentElement?.tagName.toLowerCase() === "dd" &&
|
||||
element.parentElement.previousElementSibling?.tagName.toLowerCase() === "dt"
|
||||
) {
|
||||
labelElementsSet.add(element.parentElement.previousElementSibling as HTMLElement);
|
||||
}
|
||||
|
||||
return this.createLabelElementsTag(labelElementsSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the DOM for label elements associated with the given element
|
||||
* by id or name. Returns a NodeList of label elements or null if none
|
||||
* are found.
|
||||
* @param {FillableFormFieldElement} element
|
||||
* @returns {NodeListOf<HTMLLabelElement> | null}
|
||||
* @private
|
||||
*/
|
||||
private queryElementLabels(
|
||||
element: FillableFormFieldElement
|
||||
): NodeListOf<HTMLLabelElement> | null {
|
||||
let labelQuerySelectors = element.id ? `label[for="${element.id}"]` : "";
|
||||
if (element.name) {
|
||||
const forElementNameSelector = `label[for="${element.name}"]`;
|
||||
labelQuerySelectors = labelQuerySelectors
|
||||
? `${labelQuerySelectors}, ${forElementNameSelector}`
|
||||
: forElementNameSelector;
|
||||
}
|
||||
|
||||
if (!labelQuerySelectors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return document.querySelectorAll(labelQuerySelectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over all the label elements and creates a
|
||||
* string of the text content of each label element.
|
||||
* @param {Set<HTMLElement>} labelElementsSet
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private createLabelElementsTag = (labelElementsSet: Set<HTMLElement>): string => {
|
||||
return [...labelElementsSet]
|
||||
.map((labelElement) => {
|
||||
const textContent: string | null = labelElement
|
||||
? labelElement.textContent || labelElement.innerText
|
||||
: null;
|
||||
|
||||
return this.trimAndRemoveNonPrintableText(textContent || "");
|
||||
})
|
||||
.join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the maxLength property of the passed FormFieldElement and
|
||||
* returns the value or null if the element does not have a
|
||||
* maxLength property. If the element has a maxLength property
|
||||
* greater than 999, it will return 999.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {number | null}
|
||||
* @private
|
||||
*/
|
||||
private getAutofillFieldMaxLength(element: FormFieldElement): number | null {
|
||||
const elementHasMaxLengthProperty =
|
||||
element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement;
|
||||
const elementMaxLength =
|
||||
elementHasMaxLengthProperty && element.maxLength > -1 ? element.maxLength : 999;
|
||||
|
||||
return elementHasMaxLengthProperty ? Math.min(elementMaxLength, 999) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the next siblings of the passed element and
|
||||
* returns a string of the text content of each element. Will
|
||||
* stop iterating if it encounters a new section element.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private createAutofillFieldRightLabel(element: FormFieldElement): string {
|
||||
const labelTextContent: string[] = [];
|
||||
let currentElement: ChildNode = element;
|
||||
|
||||
while (currentElement && currentElement.nextSibling) {
|
||||
currentElement = currentElement.nextSibling;
|
||||
if (this.isNewSectionElement(currentElement)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const textContent = this.getTextContentFromElement(currentElement);
|
||||
if (textContent) {
|
||||
labelTextContent.push(textContent);
|
||||
}
|
||||
}
|
||||
|
||||
return labelTextContent.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively gets the text content from an element's previous siblings
|
||||
* and returns a string of the text content of each element.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private createAutofillFieldLeftLabel(element: FormFieldElement): string {
|
||||
const labelTextContent: string[] = this.recursivelyGetTextFromPreviousSiblings(element);
|
||||
|
||||
return labelTextContent.reverse().join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes that the input elements that are to be autofilled are within a
|
||||
* table structure. Queries the previous sibling of the parent row that
|
||||
* the input element is in and returns the text content of the cell that
|
||||
* is in the same column as the input element.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {string | null}
|
||||
* @private
|
||||
*/
|
||||
private createAutofillFieldTopLabel(element: FormFieldElement): string | null {
|
||||
const tableDataElement = element.closest("td");
|
||||
if (!tableDataElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tableDataElementIndex = tableDataElement.cellIndex;
|
||||
const parentSiblingTableRowElement = tableDataElement.closest("tr")
|
||||
?.previousElementSibling as HTMLTableRowElement;
|
||||
|
||||
return parentSiblingTableRowElement?.cells?.length > tableDataElementIndex
|
||||
? this.getTextContentFromElement(parentSiblingTableRowElement.cells[tableDataElementIndex])
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the element's tag indicates that a transition to a new section of the
|
||||
* page is occurring. If so, we should not use the element or its children in order
|
||||
* to get autofill context for the previous element.
|
||||
* @param {HTMLElement} currentElement
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isNewSectionElement(currentElement: HTMLElement | Node): boolean {
|
||||
if (!currentElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const transitionalElementTagsSet = new Set([
|
||||
"html",
|
||||
"body",
|
||||
"button",
|
||||
"form",
|
||||
"head",
|
||||
"iframe",
|
||||
"input",
|
||||
"option",
|
||||
"script",
|
||||
"select",
|
||||
"table",
|
||||
"textarea",
|
||||
]);
|
||||
return (
|
||||
"tagName" in currentElement &&
|
||||
transitionalElementTagsSet.has(currentElement.tagName.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text content from a passed element, regardless of whether it is a
|
||||
* text node, an element node or an HTMLElement.
|
||||
* @param {Node | HTMLElement} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getTextContentFromElement(element: Node | HTMLElement): string {
|
||||
if (element.nodeType === Node.TEXT_NODE) {
|
||||
return this.trimAndRemoveNonPrintableText(element.nodeValue);
|
||||
}
|
||||
|
||||
return this.trimAndRemoveNonPrintableText(
|
||||
element.textContent || (element as HTMLElement).innerText
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes non-printable characters from the passed text
|
||||
* content and trims leading and trailing whitespace.
|
||||
* @param {string} textContent
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private trimAndRemoveNonPrintableText(textContent: string): string {
|
||||
return (textContent || "")
|
||||
.replace(/[^\x20-\x7E]+|\s+/g, " ") // Strip out non-primitive characters and replace multiple spaces with a single space
|
||||
.trim(); // Trim leading and trailing whitespace
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text content from the previous siblings of the element. If
|
||||
* no text content is found, recursively get the text content from the
|
||||
* previous siblings of the parent element.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
private recursivelyGetTextFromPreviousSiblings(element: Node | HTMLElement): string[] {
|
||||
const textContentItems: string[] = [];
|
||||
let currentElement = element;
|
||||
while (currentElement && currentElement.previousSibling) {
|
||||
// Ensure we are capturing text content from nodes and elements.
|
||||
currentElement = currentElement.previousSibling;
|
||||
|
||||
if (this.isNewSectionElement(currentElement)) {
|
||||
return textContentItems;
|
||||
}
|
||||
|
||||
const textContent = this.getTextContentFromElement(currentElement);
|
||||
if (textContent) {
|
||||
textContentItems.push(textContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentElement || textContentItems.length) {
|
||||
return textContentItems;
|
||||
}
|
||||
|
||||
// Prioritize capturing text content from elements rather than nodes.
|
||||
currentElement = currentElement.parentElement || currentElement.parentNode;
|
||||
|
||||
let siblingElement =
|
||||
currentElement instanceof HTMLElement
|
||||
? currentElement.previousElementSibling
|
||||
: currentElement.previousSibling;
|
||||
while (siblingElement?.lastChild && !this.isNewSectionElement(siblingElement)) {
|
||||
siblingElement = siblingElement.lastChild;
|
||||
}
|
||||
|
||||
if (this.isNewSectionElement(siblingElement)) {
|
||||
return textContentItems;
|
||||
}
|
||||
|
||||
const textContent = this.getTextContentFromElement(siblingElement);
|
||||
if (textContent) {
|
||||
textContentItems.push(textContent);
|
||||
return textContentItems;
|
||||
}
|
||||
|
||||
return this.recursivelyGetTextFromPreviousSiblings(siblingElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property or attribute from a FormFieldElement.
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} attributeName
|
||||
* @returns {string | null}
|
||||
* @private
|
||||
*/
|
||||
private getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null {
|
||||
if (attributeName in element) {
|
||||
return (element as FormElementWithAttribute)[attributeName];
|
||||
}
|
||||
|
||||
return element.getAttribute(attributeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the element. If the element is a checkbox, returns a checkmark if the
|
||||
* checkbox is checked, or an empty string if it is not checked. If the element is a hidden
|
||||
* input, returns the value of the input if it is less than 254 characters, or a truncated
|
||||
* value if it is longer than 254 characters.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getElementValue(element: FormFieldElement): string {
|
||||
if (element instanceof HTMLSpanElement) {
|
||||
const spanTextContent = element.textContent || element.innerText;
|
||||
return spanTextContent || "";
|
||||
}
|
||||
|
||||
const elementValue = element.value || "";
|
||||
const elementType = String(element.type).toLowerCase();
|
||||
if ("checked" in element && elementType === "checkbox") {
|
||||
return element.checked ? "✓" : "";
|
||||
}
|
||||
|
||||
if (elementType === "hidden") {
|
||||
const inputValueMaxLength = 254;
|
||||
|
||||
return elementValue.length > inputValueMaxLength
|
||||
? `${elementValue.substring(0, inputValueMaxLength)}...SNIPPED`
|
||||
: elementValue;
|
||||
}
|
||||
|
||||
return elementValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options from a select element and return them as an array
|
||||
* of arrays indicating the select element option text and value.
|
||||
* @param {HTMLSelectElement} element
|
||||
* @returns {{options: (string | null)[][]}}
|
||||
* @private
|
||||
*/
|
||||
private getSelectElementOptions(element: HTMLSelectElement): { options: (string | null)[][] } {
|
||||
const options = [...element.options].map((option) => {
|
||||
const optionText = option.text
|
||||
? String(option.text)
|
||||
.toLowerCase()
|
||||
.replace(/[\s~`!@$%^&#*()\-_+=:;'"[\]|\\,<.>?]/gm, "") // Remove whitespace and punctuation
|
||||
: null;
|
||||
|
||||
return [optionText, option.value];
|
||||
});
|
||||
|
||||
return { options };
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectAutofillContentService;
|
||||
@@ -0,0 +1,409 @@
|
||||
import { FormFieldElement } from "../types";
|
||||
|
||||
import DomElementVisibilityService from "./dom-element-visibility.service";
|
||||
|
||||
function createBoundingClientRectMock(customProperties: Partial<any> = {}): DOMRectReadOnly {
|
||||
return {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: 500,
|
||||
height: 500,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: jest.fn(),
|
||||
...customProperties,
|
||||
};
|
||||
}
|
||||
|
||||
describe("DomElementVisibilityService", () => {
|
||||
let domElementVisibilityService: DomElementVisibilityService;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<form id="root">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" id="username">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password">
|
||||
</form>
|
||||
`;
|
||||
domElementVisibilityService = new DomElementVisibilityService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("isFormFieldViewable", () => {
|
||||
it("returns false if the element is outside viewport bounds", async () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds")
|
||||
.mockResolvedValueOnce(true);
|
||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss");
|
||||
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
||||
|
||||
const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(
|
||||
usernameElement
|
||||
);
|
||||
|
||||
expect(isFormFieldViewable).toEqual(false);
|
||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||
usernameElement,
|
||||
usernameElement.getBoundingClientRect()
|
||||
);
|
||||
expect(domElementVisibilityService["isElementHiddenByCss"]).not.toHaveBeenCalled();
|
||||
expect(
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"]
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if the element is hidden by CSS", async () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds")
|
||||
.mockReturnValueOnce(false);
|
||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(true);
|
||||
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
||||
|
||||
const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(
|
||||
usernameElement
|
||||
);
|
||||
|
||||
expect(isFormFieldViewable).toEqual(false);
|
||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||
usernameElement,
|
||||
usernameElement.getBoundingClientRect()
|
||||
);
|
||||
expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith(
|
||||
usernameElement
|
||||
);
|
||||
expect(
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"]
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if the element is hidden behind another element", async () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds")
|
||||
.mockReturnValueOnce(false);
|
||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false);
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
||||
.mockReturnValueOnce(false);
|
||||
|
||||
const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(
|
||||
usernameElement
|
||||
);
|
||||
|
||||
expect(isFormFieldViewable).toEqual(false);
|
||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||
usernameElement,
|
||||
usernameElement.getBoundingClientRect()
|
||||
);
|
||||
expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith(
|
||||
usernameElement
|
||||
);
|
||||
expect(
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"]
|
||||
).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect());
|
||||
});
|
||||
|
||||
it("returns true if the form field is viewable", async () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds")
|
||||
.mockReturnValueOnce(false);
|
||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false);
|
||||
jest
|
||||
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
||||
.mockReturnValueOnce(true);
|
||||
|
||||
const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(
|
||||
usernameElement
|
||||
);
|
||||
|
||||
expect(isFormFieldViewable).toEqual(true);
|
||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||
usernameElement,
|
||||
usernameElement.getBoundingClientRect()
|
||||
);
|
||||
expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith(
|
||||
usernameElement
|
||||
);
|
||||
expect(
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"]
|
||||
).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect());
|
||||
});
|
||||
});
|
||||
|
||||
describe("isElementHiddenByCss", () => {
|
||||
it("returns true when a non-hidden element is passed", () => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" name="username" id="username" />
|
||||
`;
|
||||
const usernameElement = document.getElementById("username");
|
||||
|
||||
const isElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement);
|
||||
|
||||
expect(isElementHidden).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns true when the element has a `visibility: hidden;` CSS rule applied to it either inline or in a computed style", () => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" name="username" id="username" style="visibility: hidden;" />
|
||||
<input type="password" name="password" id="password" />
|
||||
<style>
|
||||
#password {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
const usernameElement = document.getElementById("username");
|
||||
const passwordElement = document.getElementById("password");
|
||||
jest.spyOn(usernameElement.style, "getPropertyValue");
|
||||
jest.spyOn(usernameElement.ownerDocument.defaultView, "getComputedStyle");
|
||||
jest.spyOn(passwordElement.style, "getPropertyValue");
|
||||
jest.spyOn(passwordElement.ownerDocument.defaultView, "getComputedStyle");
|
||||
|
||||
const isUsernameElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](usernameElement);
|
||||
const isPasswordElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](passwordElement);
|
||||
|
||||
expect(isUsernameElementHidden).toEqual(true);
|
||||
expect(usernameElement.style.getPropertyValue).toHaveBeenCalled();
|
||||
expect(usernameElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith(
|
||||
usernameElement
|
||||
);
|
||||
expect(isPasswordElementHidden).toEqual(true);
|
||||
expect(passwordElement.style.getPropertyValue).toHaveBeenCalled();
|
||||
expect(passwordElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith(
|
||||
passwordElement
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true when the element has a `display: none;` CSS rule applied to it either inline or in a computed style", () => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" name="username" id="username" style="display: none;" />
|
||||
<input type="password" name="password" id="password" />
|
||||
<style>
|
||||
#password {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
const usernameElement = document.getElementById("username");
|
||||
const passwordElement = document.getElementById("password");
|
||||
|
||||
const isUsernameElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](usernameElement);
|
||||
const isPasswordElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](passwordElement);
|
||||
|
||||
expect(isUsernameElementHidden).toEqual(true);
|
||||
expect(isPasswordElementHidden).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true when the element has a `opacity: 0;` CSS rule applied to it either inline or in a computed style", () => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" name="username" id="username" style="opacity: 0;" />
|
||||
<input type="password" name="password" id="password" />
|
||||
<style>
|
||||
#password {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
const usernameElement = document.getElementById("username");
|
||||
const passwordElement = document.getElementById("password");
|
||||
|
||||
const isUsernameElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](usernameElement);
|
||||
const isPasswordElementHidden =
|
||||
domElementVisibilityService["isElementHiddenByCss"](passwordElement);
|
||||
|
||||
expect(isUsernameElementHidden).toEqual(true);
|
||||
expect(isPasswordElementHidden).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true when the element has a `clip-path` CSS rule applied to it that hides the element either inline or in a computed style", () => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" name="username" id="username" style="clip-path: inset(50%);" />
|
||||
<input type="password" name="password" id="password" />
|
||||
<input type="text" >
|
||||
<style>
|
||||
#password {
|
||||
clip-path: inset(100%);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isElementOutsideViewportBounds", () => {
|
||||
const mockViewportWidth = 1920;
|
||||
const mockViewportHeight = 1080;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(document.documentElement, "scrollWidth", {
|
||||
writable: true,
|
||||
value: mockViewportWidth,
|
||||
});
|
||||
Object.defineProperty(document.documentElement, "scrollHeight", {
|
||||
writable: true,
|
||||
value: mockViewportHeight,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true if the passed element's size is not sufficient for visibility", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({
|
||||
width: 9,
|
||||
height: 9,
|
||||
});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true if the passed element is overflowing the left viewport", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({
|
||||
left: -1,
|
||||
});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true if the passed element is overflowing the right viewport", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({
|
||||
left: mockViewportWidth + 1,
|
||||
});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true if the passed element is overflowing the top viewport", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({
|
||||
top: -1,
|
||||
});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true if the passed element is overflowing the bottom viewport", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({
|
||||
top: mockViewportHeight + 1,
|
||||
});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false if the passed element is not outside of the viewport bounds", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const elementBoundingClientRect = createBoundingClientRectMock({});
|
||||
|
||||
const isElementOutsideViewportBounds = domElementVisibilityService[
|
||||
"isElementOutsideViewportBounds"
|
||||
](usernameElement, elementBoundingClientRect);
|
||||
|
||||
expect(isElementOutsideViewportBounds).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formFieldIsNotHiddenBehindAnotherElement", () => {
|
||||
it("returns true if the element found at the center point of the passed targetElement is the targetElement itself", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
document.elementFromPoint = jest.fn(() => usernameElement);
|
||||
|
||||
const formFieldIsNotHiddenBehindAnotherElement =
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement);
|
||||
|
||||
expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true);
|
||||
expect(document.elementFromPoint).toHaveBeenCalled();
|
||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns true if the element found at the center point of the passed targetElement is an implicit label of the element", () => {
|
||||
document.body.innerHTML = `
|
||||
<label>
|
||||
<span>Username</span>
|
||||
<input type="text" name="username" id="username" />
|
||||
</label>
|
||||
`;
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const labelTextElement = document.querySelector("span");
|
||||
document.elementFromPoint = jest.fn(() => labelTextElement);
|
||||
|
||||
const formFieldIsNotHiddenBehindAnotherElement =
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement);
|
||||
|
||||
expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true if the element found at the center point of the passed targetElement is a label of the targetElement", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
const labelElement = document.querySelector("label[for='username']") as FormFieldElement;
|
||||
const mockBoundingRect = createBoundingClientRectMock({});
|
||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||
document.elementFromPoint = jest.fn(() => labelElement);
|
||||
|
||||
const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService[
|
||||
"formFieldIsNotHiddenBehindAnotherElement"
|
||||
](usernameElement, mockBoundingRect);
|
||||
|
||||
expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true);
|
||||
expect(document.elementFromPoint).toHaveBeenCalledWith(
|
||||
mockBoundingRect.left + mockBoundingRect.width / 2,
|
||||
mockBoundingRect.top + mockBoundingRect.height / 2
|
||||
);
|
||||
expect(usernameElement.getBoundingClientRect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if the element found at the center point is not the passed targetElement or a label of that element", () => {
|
||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||
document.elementFromPoint = jest.fn(() => document.createElement("div"));
|
||||
|
||||
const formFieldIsNotHiddenBehindAnotherElement =
|
||||
domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement);
|
||||
|
||||
expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
|
||||
import { DomElementVisibilityService as domElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service";
|
||||
|
||||
class DomElementVisibilityService implements domElementVisibilityServiceInterface {
|
||||
private cachedComputedStyle: CSSStyleDeclaration | null = null;
|
||||
|
||||
/**
|
||||
* Checks if a form field is viewable. This is done by checking if the element is within the
|
||||
* viewport bounds, not hidden by CSS, and not hidden behind another element.
|
||||
* @param {FormFieldElement} element
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isFormFieldViewable(element: FormFieldElement): Promise<boolean> {
|
||||
const elementBoundingClientRect = element.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
this.isElementOutsideViewportBounds(element, elementBoundingClientRect) ||
|
||||
this.isElementHiddenByCss(element)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.formFieldIsNotHiddenBehindAnotherElement(element, elementBoundingClientRect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the target element is hidden using CSS. This is done by checking the opacity, display,
|
||||
* visibility, and clip-path CSS properties of the element. We also check the opacity of all
|
||||
* parent elements to ensure that the target element is not hidden by a parent element.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
* @public
|
||||
*/
|
||||
isElementHiddenByCss(element: HTMLElement): boolean {
|
||||
this.cachedComputedStyle = null;
|
||||
|
||||
if (
|
||||
this.isElementInvisible(element) ||
|
||||
this.isElementNotDisplayed(element) ||
|
||||
this.isElementNotVisible(element) ||
|
||||
this.isElementClipped(element)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let parentElement = element.parentElement;
|
||||
while (parentElement && parentElement !== element.ownerDocument.documentElement) {
|
||||
this.cachedComputedStyle = null;
|
||||
if (this.isElementInvisible(parentElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
parentElement = parentElement.parentElement;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed style of a given element, will only calculate the computed
|
||||
* style if the element's style has not been previously cached.
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} styleProperty
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private getElementStyle(element: HTMLElement, styleProperty: string): string {
|
||||
if (!this.cachedComputedStyle) {
|
||||
this.cachedComputedStyle = (element.ownerDocument.defaultView || window).getComputedStyle(
|
||||
element
|
||||
);
|
||||
}
|
||||
|
||||
return this.cachedComputedStyle.getPropertyValue(styleProperty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the opacity of the target element is less than 0.1.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isElementInvisible(element: HTMLElement): boolean {
|
||||
return parseFloat(this.getElementStyle(element, "opacity")) < 0.1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the target element has a display property of none.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isElementNotDisplayed(element: HTMLElement): boolean {
|
||||
return this.getElementStyle(element, "display") === "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the target element has a visibility property of hidden or collapse.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isElementNotVisible(element: HTMLElement): boolean {
|
||||
return new Set(["hidden", "collapse"]).has(this.getElementStyle(element, "visibility"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the target element has a clip-path property that hides the element.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isElementClipped(element: HTMLElement): boolean {
|
||||
return new Set([
|
||||
"inset(50%)",
|
||||
"inset(100%)",
|
||||
"circle(0)",
|
||||
"circle(0px)",
|
||||
"circle(0px at 50% 50%)",
|
||||
"polygon(0 0, 0 0, 0 0, 0 0)",
|
||||
"polygon(0px 0px, 0px 0px, 0px 0px, 0px 0px)",
|
||||
]).has(this.getElementStyle(element, "clipPath"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the target element is outside the viewport bounds. This is done by checking if the
|
||||
* element is too small or is overflowing the viewport bounds.
|
||||
* @param {HTMLElement} targetElement
|
||||
* @param {DOMRectReadOnly | null} targetElementBoundingClientRect
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private isElementOutsideViewportBounds(
|
||||
targetElement: HTMLElement,
|
||||
targetElementBoundingClientRect: DOMRectReadOnly | null = null
|
||||
): boolean {
|
||||
const documentElement = targetElement.ownerDocument.documentElement;
|
||||
const documentElementWidth = documentElement.scrollWidth;
|
||||
const documentElementHeight = documentElement.scrollHeight;
|
||||
const elementBoundingClientRect =
|
||||
targetElementBoundingClientRect || targetElement.getBoundingClientRect();
|
||||
const elementTopOffset = elementBoundingClientRect.top - documentElement.clientTop;
|
||||
const elementLeftOffset = elementBoundingClientRect.left - documentElement.clientLeft;
|
||||
|
||||
const isElementSizeInsufficient =
|
||||
elementBoundingClientRect.width < 10 || elementBoundingClientRect.height < 10;
|
||||
const isElementOverflowingLeftViewport = elementLeftOffset < 0;
|
||||
const isElementOverflowingRightViewport =
|
||||
elementLeftOffset + elementBoundingClientRect.width > documentElementWidth;
|
||||
const isElementOverflowingTopViewport = elementTopOffset < 0;
|
||||
const isElementOverflowingBottomViewport =
|
||||
elementTopOffset + elementBoundingClientRect.height > documentElementHeight;
|
||||
|
||||
return (
|
||||
isElementSizeInsufficient ||
|
||||
isElementOverflowingLeftViewport ||
|
||||
isElementOverflowingRightViewport ||
|
||||
isElementOverflowingTopViewport ||
|
||||
isElementOverflowingBottomViewport
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a passed FormField is not hidden behind another element. This is done by
|
||||
* checking if the element at the center point of the FormField is the FormField itself
|
||||
* or one of its labels.
|
||||
* @param {FormFieldElement} targetElement
|
||||
* @param {DOMRectReadOnly | null} targetElementBoundingClientRect
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private formFieldIsNotHiddenBehindAnotherElement(
|
||||
targetElement: FormFieldElement,
|
||||
targetElementBoundingClientRect: DOMRectReadOnly | null = null
|
||||
): boolean {
|
||||
const elementBoundingClientRect =
|
||||
targetElementBoundingClientRect || targetElement.getBoundingClientRect();
|
||||
const elementAtCenterPoint = targetElement.ownerDocument.elementFromPoint(
|
||||
elementBoundingClientRect.left + elementBoundingClientRect.width / 2,
|
||||
elementBoundingClientRect.top + elementBoundingClientRect.height / 2
|
||||
);
|
||||
|
||||
if (elementAtCenterPoint === targetElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetElementLabelsSet = new Set((targetElement as FillableFormFieldElement).labels);
|
||||
if (targetElementLabelsSet.has(elementAtCenterPoint as HTMLLabelElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label");
|
||||
|
||||
return targetElementLabelsSet.has(closestParentLabel);
|
||||
}
|
||||
}
|
||||
|
||||
export default DomElementVisibilityService;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user