1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

Merge branch 'master' into EC-598-beeep-properly-store-passkeys-in-bitwarden

This commit is contained in:
Andreas Coroiu
2023-01-25 15:24:42 +01:00
1208 changed files with 94078 additions and 16018 deletions

View File

@@ -86,7 +86,7 @@
"error",
{
"zones": [
// Do not allow angular/node/electron code to be imported into common
// Do not allow angular/node code to be imported into common
{
"target": "./libs/common/**/*",
"from": "./libs/angular/**/*"
@@ -94,10 +94,6 @@
{
"target": "./libs/common/**/*",
"from": "./libs/node/**/*"
},
{
"target": "./libs/common/**/*",
"from": "./libs/electron/**/*"
}
]
}
@@ -131,12 +127,6 @@
"rules": {
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/node/*", "src/**/*"] }]
}
},
{
"files": ["libs/electron/src/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/electron/*", "src/**/*"] }]
}
}
]
}

5
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,5 @@
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/pod-sm-dev

View File

@@ -4,14 +4,10 @@
./apps/browser/src/safari/desktop/Base.lproj
./apps/browser/src/services/vaultTimeout
./apps/browser/store/windows/Assets
./apps/desktop/src/models/nativeMessaging
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses
./libs/common/spec/misc/logInStrategies
./libs/common/src/abstractions/fileDownload
./libs/common/src/abstractions/userVerification
./libs/common/src/abstractions/vaultTimeout
./libs/common/src/emailForwarders
./libs/common/src/misc/logInStrategies
./libs/common/src/services/userVerification
./libs/common/src/services/vaultTimeout
@@ -64,6 +60,7 @@
./libs/common/src/misc/nodeUtils.ts
./libs/common/src/misc/linkedFieldOption.decorator.ts
./libs/common/src/misc/serviceUtils.ts
./libs/common/src/misc/serviceUtils.spec.ts
./libs/common/src/types/twoFactorResponse.ts
./libs/common/src/types/authResponse.ts
./libs/common/src/types/syncEventArgs.ts
@@ -112,12 +109,6 @@
./libs/common/src/enums/nativeMessagingVersion.ts
./libs/common/src/enums/cipherRepromptType.ts
./libs/common/src/enums/organizationUserType.ts
./libs/common/src/emailForwarders/fastmailForwarder.ts
./libs/common/src/emailForwarders/duckDuckGoForwarder.ts
./libs/common/src/emailForwarders/firefoxRelayForwarder.ts
./libs/common/src/emailForwarders/anonAddyForwarder.ts
./libs/common/src/emailForwarders/simpleLoginForwarder.ts
./libs/common/src/emailForwarders/forwarderOptions.ts
./libs/common/src/factories/accountFactory.ts
./libs/common/src/factories/globalStateFactory.ts
./libs/common/src/factories/stateFactory.ts
@@ -162,27 +153,6 @@
./libs/common/src/services/bitwardenFileUpload.service.ts
./libs/common/src/services/webCryptoFunction.service.ts
./libs/common/src/interfaces/IEncrypted.ts
./libs/node/spec/cli/consoleLog.service.spec.ts
./libs/node/src/cli/models/response/baseResponse.ts
./libs/node/src/cli/models/response/stringResponse.ts
./libs/node/src/cli/models/response/fileResponse.ts
./libs/node/src/cli/models/response/messageResponse.ts
./libs/node/src/cli/models/response/listResponse.ts
./libs/node/src/cli/baseProgram.ts
./libs/node/src/cli/services/consoleLog.service.ts
./libs/node/src/cli/services/cliPlatformUtils.service.ts
./libs/node/src/services/nodeApi.service.ts
./libs/node/src/services/lowdbStorage.service.ts
./libs/electron/spec/services/electronLog.service.spec.ts
./libs/electron/src/baseMenu.ts
./libs/electron/src/services/electronLog.service.ts
./libs/electron/src/services/electronStorage.service.ts
./libs/electron/src/services/electronRendererMessaging.service.ts
./libs/electron/src/services/electronMainMessaging.service.ts
./libs/electron/src/services/electronPlatformUtils.service.ts
./libs/electron/src/services/electronRendererStorage.service.ts
./libs/electron/src/services/electronCrypto.service.ts
./libs/electron/src/services/electronRendererSecureStorage.service.ts
./README.md
./LICENSE_BITWARDEN.txt
./CONTRIBUTING.md
@@ -197,64 +167,11 @@
./apps/desktop/resources/appx/StoreLogo.png
./apps/desktop/resources/appx/Wide310x150Logo.png
./apps/desktop/resources/appx/Square44x44Logo.png
./apps/desktop/native-messaging-test-runner/src/ipcService.ts
./apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts
./apps/desktop/native-messaging-test-runner/src/logUtils.ts
./apps/desktop/README.md
./apps/desktop/desktop_native/Cargo.toml
./apps/desktop/desktop_native/Cargo.lock
./apps/desktop/src/app/services/desktopFileDownloadService.ts
./apps/desktop/src/models/nativeMessaging/unencryptedCommand.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessage.ts
./apps/desktop/src/models/nativeMessaging/legacyMessageWrapper.ts
./apps/desktop/src/models/nativeMessaging/unencryptedMessage.ts
./apps/desktop/src/models/nativeMessaging/unencryptedMessageResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedCommand.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/successStatusResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/generateResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/failureStatusResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/cannotDecryptErrorResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/userStatusErrorResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/accountStatusResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/encryptedMessageResponse.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/cipherResponse.ts
./apps/desktop/src/models/nativeMessaging/decryptedCommandData.ts
./apps/desktop/src/models/nativeMessaging/messageCommon.ts
./apps/desktop/src/models/nativeMessaging/legacyMessage.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/passwordGeneratePayload.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload.ts
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialRetrievePayload.ts
./apps/desktop/src/main/desktopCredentialStorageListener.ts
./apps/desktop/src/main/powerMonitor.main.ts
./apps/desktop/src/main/nativeMessaging.main.ts
./apps/desktop/src/services/passwordReprompt.service.ts
./apps/desktop/src/services/encryptedMessageHandlerService.ts
./apps/desktop/src/services/nativeMessaging.service.ts
./apps/desktop/src/services/nativeMessageHandler.service.ts
./apps/cli/stores/chocolatey/tools/VERIFICATION.txt
./apps/cli/README.md
./apps/cli/src/models/response/sendFileResponse.ts
./apps/cli/src/models/response/organizationCollectionResponse.ts
./apps/cli/src/models/response/collectionResponse.ts
./apps/cli/src/models/response/templateResponse.ts
./apps/cli/src/models/response/passwordHistoryResponse.ts
./apps/cli/src/models/response/folderResponse.ts
./apps/cli/src/models/response/loginResponse.ts
./apps/cli/src/models/response/sendAccessResponse.ts
./apps/cli/src/models/response/sendResponse.ts
./apps/cli/src/models/response/cipherResponse.ts
./apps/cli/src/models/response/organizationResponse.ts
./apps/cli/src/models/response/organizationUserResponse.ts
./apps/cli/src/models/response/sendTextResponse.ts
./apps/cli/src/models/response/attachmentResponse.ts
./apps/cli/src/models/selectionReadOnly.ts
./apps/cli/src/models/request/organizationCollectionRequest.ts
./apps/cli/src/commands/convertToKeyConnector.command.ts
./apps/cli/src/commands/send/removePassword.command.ts
./apps/cli/src/services/lowdbStorage.service.ts
./apps/cli/src/services/nodeEnvSecureStorage.service.ts
./apps/browser/README.md
./apps/browser/store/windows/AppxManifest.xml
./apps/browser/src/background/nativeMessaging.background.ts
@@ -291,7 +208,6 @@
./apps/browser/src/safari/safari/SafariWebExtensionHandler.swift
./apps/browser/src/safari/safari/Info.plist
./apps/browser/src/safari/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
./apps/browser/src/commands/autoFillActiveTabCommand.ts
./apps/browser/src/listeners/onCommandListener.ts
./apps/browser/src/listeners/onInstallListener.ts
./apps/browser/src/services/browserFileDownloadService.ts

View File

@@ -63,8 +63,8 @@ jobs:
repo_url=https://github.com/$GITHUB_REPOSITORY.git
adj_build_num=${GITHUB_SHA:0:7}
echo "::set-output name=repo_url::$repo_url"
echo "::set-output name=adj_build_number::$adj_build_num"
echo "repo_url=$repo_url" >> $GITHUB_OUTPUT
echo "adj_build_number=$adj_build_num" >> $GITHUB_OUTPUT
locales-test:

View File

@@ -62,7 +62,7 @@ jobs:
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version package.json)
echo "::set-output name=package_version::$PKG_VERSION"
echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT
cli:
@@ -72,7 +72,7 @@ jobs:
- setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 16.15.0
_WIN_PKG_FETCH_VERSION: 16.16.0
_WIN_PKG_VERSION: 3.4
steps:
- name: Checkout repo

View File

@@ -87,29 +87,29 @@ jobs:
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT
- name: Increment Version
id: increment-version
run: |
BUILD_NUMBER=$(expr 3000 + $GITHUB_RUN_NUMBER)
echo "Setting build number to $BUILD_NUMBER"
echo "::set-output name=build_number::$BUILD_NUMBER"
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Get Version Channel
id: release-channel
run: |
case "${{ steps.retrieve-version.outputs.package_version }}" in
*"alpha"*)
echo "::set-output name=channel::alpha"
echo "channel=alpha" >> $GITHUB_OUTPUT
echo "[!] We do not yet support 'alpha'"
exit 1
;;
*"beta"*)
echo "::set-output name=channel::beta"
echo "channel=beta" >> $GITHUB_OUTPUT
;;
*)
echo "::set-output name=channel::latest"
echo "channel=latest" >> $GITHUB_OUTPUT
;;
esac
@@ -117,15 +117,15 @@ jobs:
id: branch-check
run: |
if [[ $(git ls-remote --heads origin rc) ]]; then
echo "::set-output name=rc_branch_exists::1"
echo "rc_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "::set-output name=rc_branch_exists::0"
echo "rc_branch_exists=0" >> $GITHUB_OUTPUT
fi
if [[ $(git ls-remote --heads origin hotfix-rc-desktop) ]]; then
echo "::set-output name=hotfix_branch_exists::1"
echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "::set-output name=hotfix_branch_exists::0"
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
fi

View File

@@ -60,7 +60,7 @@ jobs:
- name: Get GitHub sha as version
id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
run: echo "value=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
build-artifacts:
name: Build artifacts
@@ -112,7 +112,7 @@ jobs:
if: matrix.name == 'cloud-QA'
run: |
VERSION=$( jq -r ".version" package.json)
jq --arg version "$VERSION - ${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Build ${{ matrix.name }}
@@ -303,7 +303,7 @@ jobs:
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi
echo "::set-output name=value::$IMAGE_TAG"
echo "value=$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Tag image
env:

View File

@@ -266,7 +266,7 @@ jobs:
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "cli-npm-api-key"
secrets: "npm-api-key"
- name: Download artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
@@ -293,7 +293,7 @@ jobs:
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
env:
NPM_TOKEN: ${{ steps.retrieve-secrets.outputs.cli-npm-api-key }}
NPM_TOKEN: ${{ steps.retrieve-secrets.outputs.npm-api-key }}
- name: Install Husky
run: npm install -g husky

View File

@@ -60,22 +60,22 @@ jobs:
run: |
BUILD_NUMBER=$(expr 3000 + $GITHUB_RUN_NUMBER)
echo "Setting build number to $BUILD_NUMBER"
echo "::set-output name=build_number::$BUILD_NUMBER"
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Get Version Channel
id: release-channel
run: |
case "${{ steps.version.outputs.version }}" in
*"alpha"*)
echo "::set-output name=channel::alpha"
echo "channel=alpha" >> $GITHUB_OUTPUT
echo "[!] We do not yet support 'alpha'"
exit 1
;;
*"beta"*)
echo "::set-output name=channel::beta"
echo "channel=beta" >> $GITHUB_OUTPUT
;;
*)
echo "::set-output name=channel::latest"
echo "channel=latest" >> $GITHUB_OUTPUT
;;
esac
@@ -102,7 +102,7 @@ jobs:
git push -u origin $branch_name
echo "::set-output name=branch-name::$branch_name"
echo "branch-name=$branch_name" >> $GITHUB_OUTPUT
linux:
name: Linux Build

View File

@@ -29,6 +29,16 @@ on:
required: true
default: true
type: boolean
electron_publish:
description: 'Publish electron to S3 bucket'
required: true
default: true
type: boolean
github_release:
description: 'Publish github release'
required: true
default: true
type: boolean
defaults:
run:
@@ -70,15 +80,15 @@ jobs:
run: |
case "${{ steps.version.outputs.version }}" in
*"alpha"*)
echo "::set-output name=channel::alpha"
echo "channel=alpha" >> $GITHUB_OUTPUT
echo "[!] We do not yet support 'alpha'"
exit 1
;;
*"beta"*)
echo "::set-output name=channel::beta"
echo "channel=beta" >> $GITHUB_OUTPUT
;;
*)
echo "::set-output name=channel::latest"
echo "channel=latest" >> $GITHUB_OUTPUT
;;
esac
@@ -136,6 +146,7 @@ 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 }}
env:
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}
ROLLOUT_PCT: ${{ github.event.inputs.rollout_percentage }}
@@ -145,7 +156,7 @@ jobs:
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml
- name: Publish artifacts to S3
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }}
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 }}
@@ -159,7 +170,7 @@ jobs:
--quiet
- name: Publish artifacts to R2
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }}
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 }}
@@ -175,7 +186,7 @@ jobs:
- name: Create Release
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }}
if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' && github.event.inputs.github_release }}
env:
PKG_VERSION: ${{ steps.version.outputs.version }}
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}

View File

@@ -15,26 +15,9 @@ defaults:
shell: bash
jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Branch check
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then
echo "==================================="
echo "[!] Can only increase rollout from the 'rc' or 'hotfix-rc-desktop' branches"
echo "==================================="
exit 1
fi
rollout:
name: Update Rollout Percentage
runs-on: ubuntu-22.04
needs: setup
steps:
- name: Login to Azure
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
@@ -54,20 +37,6 @@ jobs:
r2-electron-bucket-name,
cf-prod-account"
- name: Download channel update info files from S3
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 }}
AWS_DEFAULT_REGION: 'us-west-2'
AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }}
run: |
aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \
--quiet
aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \
--quiet
aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \
--quiet
- name: Download channel update info files from R2
env:
AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }}
@@ -99,7 +68,7 @@ jobs:
echo
echo "If you want to pull a staged release because it hasnt gone well, you must increment the version \
number higher than your broken release. Because some of your users will be on the broken 1.0.1, \
releasing a new 1.0.1 would result in them staying on a broken version.
releasing a new 1.0.1 would result in them staying on a broken version."
exit 1
fi
@@ -118,10 +87,14 @@ jobs:
AWS_DEFAULT_REGION: 'us-west-2'
AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }}
run: |
aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \
--include "latest*.yml" \
--acl "public-read" \
--quiet
aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \
--acl "public-read"
aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \
--acl "public-read"
aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \
--acl "public-read"
- name: Publish channel update info files to R2
env:
@@ -131,7 +104,11 @@ jobs:
AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }}
CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }}
run: |
aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \
--include "latest*.yml" \
--quiet \
aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \
--endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com
aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \
--endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com
aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \
--endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com

View File

@@ -53,8 +53,16 @@ jobs:
done
- name: Run tests
run: |
npm run test
run: npm run test
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
if: always()
with:
name: Test Results
path: "junit.xml"
reporter: jest-junit
fail-on-error: true
rust:
name: rust - ${{ matrix.os }}
@@ -102,6 +110,4 @@ jobs:
- name: Test Windows / macOS
if: ${{ matrix.os!='ubuntu-latest' }}
working-directory: ./apps/desktop/desktop_native
run: |
cargo test -- --test-threads=1
run: cargo test -- --test-threads=1

View File

@@ -25,8 +25,8 @@ jobs:
env:
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
@@ -36,7 +36,7 @@ jobs:
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "::set-output name=new-version::$NEW_VER"
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump:
name: "Trigger desktop version bump workflow"

View File

@@ -56,7 +56,7 @@ jobs:
VERSION: ${{ github.event.inputs.version_number }}
run: |
CLIENT=$(python -c "print('$CLIENT_NAME'.lower())")
echo "::set-output name=client::$CLIENT"
echo "client=$CLIENT" >> $GITHUB_OUTPUT
git switch -c ${CLIENT}_version_bump_${VERSION}
@@ -131,9 +131,9 @@ jobs:
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::set-output name=changes_to_commit::TRUE"
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
else
echo "::set-output name=changes_to_commit::FALSE"
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
echo "No changes to commit!";
fi
@@ -142,8 +142,7 @@ jobs:
env:
CLIENT: ${{ steps.branch.outputs.client }}
VERSION: ${{ github.event.inputs.version_number }}
run: |
git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ build
# Testing
coverage
junit.xml
# Misc
*.crx

View File

@@ -6,6 +6,8 @@ module.exports = {
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
"../apps/web/src/**/*.stories.mdx",
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
"../bitwarden_license/bit-web/src/**/*.stories.mdx",
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
"@storybook/addon-links",
@@ -18,6 +20,12 @@ module.exports = {
builder: "webpack5",
disableTelemetry: true,
},
env: (config) => ({
...config,
FLAGS: JSON.stringify({
secretsManager: true,
}),
}),
webpackFinal: async (config, { configType }) => {
config.resolve.plugins = [new TsconfigPathsPlugin()];
return config;

View File

@@ -25,7 +25,7 @@
This repository houses all Bitwarden client applications except the [Mobile application](https://github.com/bitwarden/mobile).
Please refer to the [Clients section](https://contributing.bitwarden.com/clients/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
Please refer to the [Clients section](https://contributing.bitwarden.com/getting-started/clients/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
## Related projects:

View File

@@ -19,4 +19,4 @@ The Bitwarden browser extension is written using the Web Extension API and Angul
## Documentation
Please refer to the [Browser section](https://contributing.bitwarden.com/clients/browser/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
Please refer to the [Browser section](https://contributing.bitwarden.com/getting-started/clients/browser/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2022.10.2",
"version": "2023.1.0",
"scripts": {
"build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",

View File

@@ -482,7 +482,7 @@
"message": "الاسم مطلوب."
},
"addedFolder": {
"message": "Folder added"
"message": "أُضيف المجلد"
},
"changeMasterPass": {
"message": "تغيير كلمة المرور الرئيسية"
@@ -494,7 +494,7 @@
"message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟"
},
"editedFolder": {
"message": "Folder saved"
"message": "حُفظ المجلد"
},
"deleteFolderConfirmation": {
"message": "هل أنت متأكد من حذف هذا المجلّد؟"
@@ -521,7 +521,7 @@
"message": "عنوان الـ URI"
},
"uriPosition": {
"message": "URI $POSITION$",
"message": "رابط $POSITION$",
"description": "A listing of URIs. Ex: URI 1, URI 2, URI 3, etc.",
"placeholders": {
"position": {
@@ -571,22 +571,22 @@
"description": "This is the folder for uncategorized items"
},
"enableAddLoginNotification": {
"message": "Ask to add login"
"message": "اطلب إضافة تسجيل الدخول"
},
"addLoginNotificationDesc": {
"message": "Ask to add an item if one isn't found in your vault."
"message": "اطلب إضافة عنصر إذا لم يُعثر عليه في خزنتك."
},
"showCardsCurrentTab": {
"message": "Show cards on Tab page"
"message": "أظهر البطاقات في صفحة التبويبات"
},
"showCardsCurrentTabDesc": {
"message": "List card items on the Tab page for easy auto-fill."
"message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية."
},
"showIdentitiesCurrentTab": {
"message": "Show identities on Tab page"
"message": "إظهار الهويات على صفحة التبويب"
},
"showIdentitiesCurrentTabDesc": {
"message": "List identity items on the Tab page for easy auto-fill."
"message": "قائمة عناصر الهوية في صفحة التبويب لسهولة الملء التلقائي."
},
"clearClipboard": {
"message": "مسح الحافظة",
@@ -615,23 +615,23 @@
"message": "تحديث"
},
"enableContextMenuItem": {
"message": "Show context menu options"
"message": "إظهار خيارات قائمة السياق"
},
"contextMenuItemDesc": {
"message": "Use a secondary click to access password generation and matching logins for the website. "
"message": "استخدم نقرة ثانوية للوصول إلى توليد كلمة المرور ومطابقة تسجيلات الدخول للموقع. "
},
"defaultUriMatchDetection": {
"message": "Default URI match detection",
"message": "الكشف الافتراضي عن تطابق URI",
"description": "Default URI match detection for auto-fill."
},
"defaultUriMatchDetectionDesc": {
"message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill."
"message": "اختر الطريقة الافتراضية التي يتم التعامل بها مع الكشف عن مطابقة URI لتسجيل الدخول عند تنفيذ إجراءات مثل التعبئة التلقائية."
},
"theme": {
"message": "Theme"
"message": "السمة"
},
"themeDesc": {
"message": "Change the application's color theme."
"message": "تغيير سمة لون التطبيق."
},
"dark": {
"message": "داكن",
@@ -659,28 +659,28 @@
"message": "تأكيد تصدير الخزنة"
},
"exportWarningDesc": {
"message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it."
"message": "يحتوي هذا التصدير على بيانات خزنتك بتنسيق غير مشفر. لا يجب عليك تخزين أو إرسال الملف الذي تم تصديره عبر قنوات غير آمنة (مثل البريد الإلكتروني). احذفه مباشرة بعد انتهائك من استخدامه."
},
"encExportKeyWarningDesc": {
"message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file."
"message": "يقوم هذا التصدير بتشفير بياناتك باستخدام مفتاح تشفير حسابك. إذا قمت بتدوير مفتاح تشفير حسابك يجب عليك التصدير مرة أخرى لأنك لن تتمكن من فك تشفير ملف التصدير هذا."
},
"encExportAccountWarningDesc": {
"message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account."
"message": "مفاتيح تشفير الحساب فريدة من نوعها لكل حساب مستخدم Bitwarden، لذلك لا يمكنك استيراد تصدير مشفر إلى حساب آخر."
},
"exportMasterPassword": {
"message": "Enter your master password to export your vault data."
"message": "أدخل كلمة المرور الرئيسية لتصدير بيانات خزنتك."
},
"shared": {
"message": "Shared"
"message": "مشترك"
},
"learnOrg": {
"message": "Learn about organizations"
"message": "تعرف على المؤسسات"
},
"learnOrgConfirmation": {
"message": "Bitwarden allows you to share your vault items with others by using an organization. Would you like to visit the bitwarden.com website to learn more?"
"message": "يسمح لك Bitwarden بمشاركة عناصر خزنتك مع الآخرين باستخدام حساب المؤسسة. هل ترغب في زيارة موقع bitwarden.com لمعرفة المزيد؟"
},
"moveToOrganization": {
"message": "Move to organization"
"message": "الانتقال إلى مؤسسة"
},
"share": {
"message": "مشاركة"
@@ -711,94 +711,94 @@
"message": "رمز التحقق (TOTP)"
},
"copyVerificationCode": {
"message": "Copy verification code"
"message": "نسخ رمز التحقق"
},
"attachments": {
"message": "Attachments"
"message": "المرفقات"
},
"deleteAttachment": {
"message": "Delete attachment"
"message": "حذف المرفق"
},
"deleteAttachmentConfirmation": {
"message": "Are you sure you want to delete this attachment?"
"message": "هل أنت متأكد من أنك تريد حذف هذا المرفق؟"
},
"deletedAttachment": {
"message": "Attachment deleted"
"message": "تم حذف المرفق"
},
"newAttachment": {
"message": "Add new attachment"
"message": "إضافة مرفق جديد"
},
"noAttachments": {
"message": "No attachments."
"message": "لا توجد مرفقات."
},
"attachmentSaved": {
"message": "Attachment saved"
"message": "تم حفظ المرفقات"
},
"file": {
"message": "File"
"message": "الملف"
},
"selectFile": {
"message": "Select a file"
"message": "حدد ملفًا"
},
"maxFileSize": {
"message": "Maximum file size is 500 MB."
"message": "الحجم الأقصى للملف هو 500 ميجابايت."
},
"featureUnavailable": {
"message": "Feature unavailable"
"message": "الميزة غير متوفرة"
},
"updateKey": {
"message": "You cannot use this feature until you update your encryption key."
"message": "لا يمكنك استخدام هذه المِيزة حتى تحديث مفتاح التشفير الخاص بك."
},
"premiumMembership": {
"message": "Premium membership"
"message": "العضوية المميزة"
},
"premiumManage": {
"message": "Manage membership"
"message": "إدارة العضوية"
},
"premiumManageAlert": {
"message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?"
"message": "يمكنك إدارة عضويتك على مقطع ويب bitwarden.com. هل تريد زيارة الموقع الآن؟"
},
"premiumRefresh": {
"message": "Refresh membership"
"message": "تحديث العضوية"
},
"premiumNotCurrentMember": {
"message": "You are not currently a Premium member."
"message": "أنت لست حاليا عضوا مميزا."
},
"premiumSignUpAndGet": {
"message": "Sign up for a Premium membership and get:"
"message": "قم بالتسجيل للحصول على عضوية مميزة واحصل على:"
},
"ppremiumSignUpStorage": {
"message": "1 GB encrypted storage for file attachments."
"message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات."
},
"ppremiumSignUpTwoStep": {
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
"message": "خيارات تسجيل الدخول الإضافية من خطوتين مثل YubiKey و FIDO U2F و Duo."
},
"ppremiumSignUpReports": {
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
"message": "نظافة كلمة المرور، صحة الحساب، وتقارير خرق البيانات للحفاظ على سلامة خزنتك."
},
"ppremiumSignUpTotp": {
"message": "TOTP verification code (2FA) generator for logins in your vault."
"message": "مورد رمز التحقق (2FA) لتسجيل الدخول في خزنتك."
},
"ppremiumSignUpSupport": {
"message": "Priority customer support."
"message": "أولوية دعم العملاء."
},
"ppremiumSignUpFuture": {
"message": "All future Premium features. More coming soon!"
"message": "جميع الميزات المميزة المستقبلية. المزيد قادم قريبا!"
},
"premiumPurchase": {
"message": "Purchase Premium"
"message": "شراء العضوية المميزة"
},
"premiumPurchaseAlert": {
"message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?"
"message": "يمكنك شراء العضوية المتميزة على bitwarden.com على خزانة الويب. هل تريد زيارة الموقع الآن؟"
},
"premiumCurrentMember": {
"message": "You are a Premium member!"
"message": "أنت عضو مميز!"
},
"premiumCurrentMemberThanks": {
"message": "Thank you for supporting Bitwarden."
"message": "شكرا لك على دعم Bitwarden."
},
"premiumPrice": {
"message": "All for just $PRICE$ /year!",
"message": "الكل فقط بـ $PRICE$ /سنة!",
"placeholders": {
"price": {
"content": "$1",
@@ -807,28 +807,28 @@
}
},
"refreshComplete": {
"message": "Refresh complete"
"message": "اكتمل التحديث"
},
"enableAutoTotpCopy": {
"message": "Copy TOTP automatically"
"message": "نسخ TOTP تلقائياً"
},
"disableAutoTotpCopyDesc": {
"message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login."
"message": "إذا كان تسجيل الدخول يحتوي على مفتاح مصادقة، انسخ رمز التحقق TOTP إلى الحافظة الخاصة بك عند ملء تسجيل الدخول تلقائيا."
},
"enableAutoBiometricsPrompt": {
"message": "Ask for biometrics on launch"
"message": "اسأل عن القياسات الحيوية عند الإطلاق"
},
"premiumRequired": {
"message": "Premium required"
"message": "حساب البريميوم مطلوب"
},
"premiumRequiredDesc": {
"message": "A Premium membership is required to use this feature."
"message": "هذه المِيزة متاحة فقط للعضوية المميزة."
},
"enterVerificationCodeApp": {
"message": "Enter the 6 digit verification code from your authenticator app."
"message": "أدخل رمز التحقق من 6 أرقام من تطبيق المصادقة الخاص بك."
},
"enterVerificationCodeEmail": {
"message": "Enter the 6 digit verification code that was emailed to $EMAIL$.",
"message": "أدخل رمز التحقق المكون من 6 أرقام الذي تم إرساله إلى $EMAIL$.",
"placeholders": {
"email": {
"content": "$1",
@@ -837,7 +837,7 @@
}
},
"verificationCodeEmailSent": {
"message": "Verification email sent to $EMAIL$.",
"message": "تم إرسال رسالة التحقق إلى $EMAIL$.",
"placeholders": {
"email": {
"content": "$1",
@@ -846,115 +846,115 @@
}
},
"rememberMe": {
"message": "Remember me"
"message": "تذكرني"
},
"sendVerificationCodeEmailAgain": {
"message": "Send verification code email again"
"message": "إرسال رمز التحقق إلى البريد الإلكتروني مرة أخرى"
},
"useAnotherTwoStepMethod": {
"message": "Use another two-step login method"
"message": "استخدام طريقة أخرى لتسجيل الدخول بخطوتين"
},
"insertYubiKey": {
"message": "Insert your YubiKey into your computer's USB port, then touch its button."
"message": "أدخل YubiKey الخاص بك في منفذ USB في كمبيوترك، ثم المس الزر."
},
"insertU2f": {
"message": "Insert your security key into your computer's USB port. If it has a button, touch it."
"message": "أدخل مفتاح الأمان الخاص بك في منفذ USB كمبيوترك، إذا كان يحتوي على زر، إلمسه."
},
"webAuthnNewTab": {
"message": "To start the WebAuthn 2FA verification. Click the button below to open a new tab and follow the instructions provided in the new tab."
"message": "لبدء التحقق من WebAuthn 2FA. انقر على الزر أدناه لفتح علامة تبويب جديدة واتبع التعليمات المقدمة في علامة التبويب الجديدة."
},
"webAuthnNewTabOpen": {
"message": "Open new tab"
"message": "فتح علامة تبويب جديدة"
},
"webAuthnAuthenticate": {
"message": "Authenticate WebAuthn"
"message": "مصادقة WebAuthn"
},
"loginUnavailable": {
"message": "Login unavailable"
"message": "تسجيل الدخول غير متاح"
},
"noTwoStepProviders": {
"message": "This account has two-step login set up, however, none of the configured two-step providers are supported by this web browser."
"message": "تم إعداد تسجيل الدخول من خطوتين لهذا الحساب، ومع ذلك، لا يدعم هذا المتصفح أيًا من موفري تسجيل الدخول من خطوتين."
},
"noTwoStepProviders2": {
"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)."
"message": "الرجاء استخدام متصفح ويب مدعوم (مثل Chrome) و/أو إضافة موفري إضافيين مدعومين بشكل أفضل عبر متصفحات الويب (مثل تطبيق المصادقة)."
},
"twoStepOptions": {
"message": "Two-step login options"
"message": "خيارات تسجيل الدخول بخطوتين"
},
"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."
"message": "هل تفقد الوصول إلى جميع مزودي التحقق بعاملين؟ استخدم رمز الاسترداد الخاص بك لتعطيل جميع مزودي التحقق بعاملين من حسابك."
},
"recoveryCodeTitle": {
"message": "Recovery code"
"message": "رمز الاسترداد"
},
"authenticatorAppTitle": {
"message": "Authenticator app"
"message": "تطبيق المصادقة"
},
"authenticatorAppDesc": {
"message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.",
"message": "استخدام تطبيق مصادقة (مثل Authy أو Google Authenticator) لإنشاء رموز تحقق مستندة إلى الوقت.",
"description": "'Authy' and 'Google Authenticator' are product names and should not be translated."
},
"yubiKeyTitle": {
"message": "YubiKey OTP Security Key"
"message": "مفتاح أمان YubiKey OTP"
},
"yubiKeyDesc": {
"message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices."
"message": "استخدم YubiKey للوصول إلى حسابك. يعمل مع YubiKey 4 ،4 Nano ،4C، وأجهزة NEO."
},
"duoDesc": {
"message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.",
"message": "التحقق باستخدام نظام الحماية الثنائي باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.",
"description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
},
"duoOrganizationDesc": {
"message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.",
"message": "تحقق من خلال نظام الحماية الثنائي لمؤسستك باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.",
"description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
},
"webAuthnTitle": {
"message": "FIDO2 WebAuthn"
},
"webAuthnDesc": {
"message": "Use any WebAuthn compatible security key to access your account."
"message": "استخدم أي مفتاح أمان متوافق مع WebAuthn للوصول إلى حسابك."
},
"emailTitle": {
"message": "Email"
"message": "البريد الإلكتروني"
},
"emailDesc": {
"message": "Verification codes will be emailed to you."
"message": "سيتم إرسال رمز التحقق إليك بالبريد الإلكتروني."
},
"selfHostedEnvironment": {
"message": "Self-hosted environment"
"message": "البيئة المستضافة ذاتيا"
},
"selfHostedEnvironmentFooter": {
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
"message": "حدد عنوان URL الأساسي لتثبيت Bitwarden المستضاف محليًا."
},
"customEnvironment": {
"message": "Custom environment"
"message": "بيئة مخصصة"
},
"customEnvironmentFooter": {
"message": "For advanced users. You can specify the base URL of each service independently."
"message": "للمستخدمين المتقدمين. يمكنك تحديد عنوان URL الأساسي لكل خدمة بشكل مستقل."
},
"baseUrl": {
"message": "Server URL"
"message": "رابط الخادم"
},
"apiUrl": {
"message": "API Server URL"
"message": "رابط خادم API"
},
"webVaultUrl": {
"message": "Web vault server URL"
"message": "رابط خادم مخزن الويب"
},
"identityUrl": {
"message": "Identity server URL"
"message": "رابط خادم الهوية"
},
"notificationsUrl": {
"message": "Notifications server URL"
"message": "رابط خادم الإشعارات"
},
"iconsUrl": {
"message": "Icons server URL"
"message": "رابط خادم الأيقونات"
},
"environmentSaved": {
"message": "Environment URLs saved"
"message": "روابط البيئة المحفوظة"
},
"enableAutoFillOnPageLoad": {
"message": "Auto-fill on page load"
"message": "ملء تلقائي عند تحميل الصفحة"
},
"enableAutoFillOnPageLoadDesc": {
"message": "If a login form is detected, auto-fill when the web page loads."
@@ -972,7 +972,7 @@
"message": "Auto-fill on page load (if set up in Options)"
},
"autoFillOnPageLoadUseDefault": {
"message": "Use default setting"
"message": "إستخدم الإعداد الإفتراضي"
},
"autoFillOnPageLoadYes": {
"message": "Auto-fill on page load"
@@ -1020,108 +1020,108 @@
"message": "Hidden"
},
"cfTypeBoolean": {
"message": "Boolean"
"message": "قيمة منطقية"
},
"cfTypeLinked": {
"message": "Linked",
"message": "مرتبط",
"description": "This describes a field that is 'linked' (tied) to another field."
},
"linkedValue": {
"message": "Linked value",
"message": "القيمة المرتبطة",
"description": "This describes a value that is 'linked' (tied) to another value."
},
"popup2faCloseMessage": {
"message": "Clicking outside the popup window to check your email for your verification code will cause this popup to close. Do you want to open this popup in a new window so that it does not close?"
"message": "النقر خارج النافذة المنبثقة للتحقق من بريدك الإلكتروني للحصول على رمز التحقق الخاص بك سيؤدي إلى إغلاق هذا المنبثق. هل تريد فتح هذا المنبثق في نافذة جديدة حتى لا يغلق؟"
},
"popupU2fCloseMessage": {
"message": "This browser cannot process U2F requests in this popup window. Do you want to open this popup in a new window so that you can log in using U2F?"
"message": "لا يمكن لهذا المتصفح معالجة طلبات U2F في هذه النافذة المنبثقة. هل تريد فتح هذا المنبثق في نافذة جديدة بحيث يمكنك تسجيل الدخول باستخدام U2F؟"
},
"enableFavicon": {
"message": "Show website icons"
"message": "إظهار أيقونات الموقع"
},
"faviconDesc": {
"message": "Show a recognizable image next to each login."
"message": "إظهار صورة قابلة للتعرف بجانب كل تسجيل دخول."
},
"enableBadgeCounter": {
"message": "Show badge counter"
"message": "إظهار عداد الشارات"
},
"badgeCounterDesc": {
"message": "Indicate how many logins you have for the current web page."
"message": "حدد عدد تسجيلات الدخول الخاصة بك لصفحة الويب الحالية."
},
"cardholderName": {
"message": "Cardholder name"
"message": "اسم حامل البطاقة"
},
"number": {
"message": "Number"
"message": "الرقم"
},
"brand": {
"message": "Brand"
"message": "العلامة"
},
"expirationMonth": {
"message": "Expiration month"
"message": "شهر الانتهاء"
},
"expirationYear": {
"message": "Expiration year"
"message": "سنة الإنتهاء"
},
"expiration": {
"message": "Expiration"
"message": "تاريخ الانتهاء"
},
"january": {
"message": "January"
"message": "جانفي"
},
"february": {
"message": "February"
"message": "فبراير"
},
"march": {
"message": "March"
"message": "مارس"
},
"april": {
"message": "April"
"message": "أبريل"
},
"may": {
"message": "May"
"message": "مايو"
},
"june": {
"message": "June"
"message": "جوان"
},
"july": {
"message": "July"
"message": "جويلية"
},
"august": {
"message": "August"
"message": "أغسطس"
},
"september": {
"message": "September"
"message": "سبتمبر"
},
"october": {
"message": "October"
"message": "أكتوبر"
},
"november": {
"message": "November"
"message": "نوفمبر"
},
"december": {
"message": "December"
"message": "ديسمبر"
},
"securityCode": {
"message": "Security code"
"message": "رمز الأمان"
},
"ex": {
"message": "ex."
"message": "مثال."
},
"title": {
"message": "Title"
"message": "اللقب"
},
"mr": {
"message": "Mr"
"message": "السيد"
},
"mrs": {
"message": "Mrs"
"message": "السيدة"
},
"ms": {
"message": "Ms"
"message": "الآنسة"
},
"dr": {
"message": "Dr"
"message": "الدكتور"
},
"firstName": {
"message": "الاسم الأول"
@@ -1136,107 +1136,107 @@
"message": "الاسم الكامل"
},
"identityName": {
"message": "Identity name"
"message": "اسم الهوية"
},
"company": {
"message": "Company"
"message": "الشركة"
},
"ssn": {
"message": "Social Security number"
"message": "رقم الضمان الاجتماعي"
},
"passportNumber": {
"message": "Passport number"
"message": "رقم جواز السفر"
},
"licenseNumber": {
"message": "License number"
"message": "رقم الرخصة"
},
"email": {
"message": "Email"
"message": "البريد الإلكتروني"
},
"phone": {
"message": "Phone"
"message": "الهاتف"
},
"address": {
"message": "Address"
"message": "العنوان"
},
"address1": {
"message": "Address 1"
"message": "العنوان 1"
},
"address2": {
"message": "Address 2"
"message": "العنوان 2"
},
"address3": {
"message": "Address 3"
"message": "العنوان 3"
},
"cityTown": {
"message": "City / Town"
"message": "المدينة / البلدة"
},
"stateProvince": {
"message": "State / Province"
"message": "الولاية / المقاطعة"
},
"zipPostalCode": {
"message": "Zip / Postal code"
"message": "الرمز البريدي"
},
"country": {
"message": "Country"
"message": "البلد"
},
"type": {
"message": "Type"
"message": "النوع"
},
"typeLogin": {
"message": "Login"
"message": "تسجيل الدخول"
},
"typeLogins": {
"message": "Logins"
"message": "تسجيلات الدخول"
},
"typeSecureNote": {
"message": "Secure note"
"message": "ملاحظة آمنة"
},
"typeCard": {
"message": "Card"
"message": "بطاقة"
},
"typeIdentity": {
"message": "Identity"
"message": "الهوية"
},
"passwordHistory": {
"message": "Password history"
"message": "سجل كلمة المرور"
},
"back": {
"message": "Back"
"message": "رجوع"
},
"collections": {
"message": "Collections"
"message": "المجموعات"
},
"favorites": {
"message": "Favorites"
"message": "المفضلات"
},
"popOutNewWindow": {
"message": "Pop out to a new window"
"message": "انبثق إلى نافذة جديدة"
},
"refresh": {
"message": "Refresh"
"message": "تحديث"
},
"cards": {
"message": "Cards"
"message": "البطاقات"
},
"identities": {
"message": "Identities"
"message": "الهويات"
},
"logins": {
"message": "Logins"
"message": "تسجيلات الدخول"
},
"secureNotes": {
"message": "Secure notes"
"message": "ملاحظات آمنة"
},
"clear": {
"message": "Clear",
"message": "مسح",
"description": "To clear something out. example: To clear browser history."
},
"checkPassword": {
"message": "Check if password has been exposed."
"message": "تحقق مما إذا تم الكشف عن كلمة المرور."
},
"passwordExposed": {
"message": "This password has been exposed $VALUE$ time(s) in data breaches. You should change it.",
"message": "تم الكشف عن كلمة المرور هذه $VALUE$ مرّة (ات) في خروقات البيانات. يجب عليك تغييرها.",
"placeholders": {
"value": {
"content": "$1",
@@ -1245,47 +1245,47 @@
}
},
"passwordSafe": {
"message": "This password was not found in any known data breaches. It should be safe to use."
"message": "لم يتم العثور على كلمة المرور هذه في أي عمليات اختراق معروفة للبيانات. من المفترض أن تكون آمنة للاستخدام."
},
"baseDomain": {
"message": "Base domain",
"message": "النطاق الأساسي",
"description": "Domain name. Ex. website.com"
},
"domainName": {
"message": "Domain name",
"message": "إسم النطاق",
"description": "Domain name. Ex. website.com"
},
"host": {
"message": "Host",
"message": "المضيف",
"description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'."
},
"exact": {
"message": "Exact"
"message": "بالضبط"
},
"startsWith": {
"message": "Starts with"
"message": "يبدأ بـ"
},
"regEx": {
"message": "Regular expression",
"message": "التعبير الاعتيادي",
"description": "A programming term, also known as 'RegEx'."
},
"matchDetection": {
"message": "Match detection",
"message": "كشف المطابقة",
"description": "URI match detection for auto-fill."
},
"defaultMatchDetection": {
"message": "Default match detection",
"message": "الكشف الافتراضي عن المطابقة",
"description": "Default URI match detection for auto-fill."
},
"toggleOptions": {
"message": "Toggle options"
"message": "خيارات التبديل"
},
"toggleCurrentUris": {
"message": "Toggle current URIs",
"message": "تبديل URIs الحالية",
"description": "Toggle the display of the URIs of the currently open tabs in the browser."
},
"currentUri": {
"message": "Current URI",
"message": "URI الحالي",
"description": "The URI of one of the current open tabs in the browser."
},
"organization": {
@@ -1482,7 +1482,7 @@
}
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
"message": "كلمة المرور الرئيسية الجديدة لا تفي بمتطلبات السياسة العامة."
},
"acceptPolicies": {
"message": "By checking this box you agree to the following:"
@@ -1494,7 +1494,7 @@
"message": "Terms of Service"
},
"privacyPolicy": {
"message": "Privacy Policy"
"message": "سياسة الخصوصية"
},
"hintEqualsPassword": {
"message": "Your password hint cannot be the same as your password."
@@ -1695,7 +1695,7 @@
}
},
"custom": {
"message": "Custom"
"message": "مُخصّص"
},
"maximumAccessCount": {
"message": "Maximum Access Count"
@@ -2030,13 +2030,13 @@
}
},
"loginWithMasterPassword": {
"message": "Log in with master password"
"message": "تسجيل الدخول باستخدام كلمة المرور الرئيسية"
},
"loggingInAs": {
"message": "Logging in as"
"message": "تسجيل الدخول كـ"
},
"notYou": {
"message": "Not you?"
"message": "ليس حسابك؟"
},
"newAroundHere": {
"message": "New around here?"

View File

@@ -196,7 +196,7 @@
"message": "Даведка і зваротная сувязь"
},
"sync": {
"message": "Сінхранізавана"
"message": "Сінхранізаваць"
},
"syncVaultNow": {
"message": "Сінхранізаваць сховішча зараз"
@@ -642,7 +642,7 @@
"description": "Light color"
},
"solarizedDark": {
"message": "Цёмная Solarized",
"message": "Solarized dark",
"description": "'Solarized' is a noun and the name of a color scheme. It should not be translated."
},
"exportVault": {

View File

@@ -86,7 +86,7 @@
"message": "Копиране на номера"
},
"copySecurityCode": {
"message": "Копиране на кода да сигурност"
"message": "Копиране на кода за сигурност"
},
"autoFill": {
"message": "Автоматично дописване"
@@ -291,7 +291,7 @@
"message": "Парола"
},
"passphrase": {
"message": "Парола за преминаване"
"message": "Парола-фраза"
},
"favorite": {
"message": "Любими"
@@ -1923,17 +1923,17 @@
"message": "Тип потребителско име"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
"message": "Адрес на е-поща с плюс",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
},
"plusAddressedEmailDesc": {
"message": "Използвайте възможностите за под-адресиране на е-поща на своя доставчик."
},
"catchallEmail": {
"message": "Catch-all email"
"message": "Хващаща всичко е-поща"
},
"catchallEmailDesc": {
"message": "Use your domain's configured catch-all inbox."
"message": "Използвайте конфигурираната входяща кутия за събиране на всичко."
},
"random": {
"message": "Произволно"
@@ -1954,10 +1954,10 @@
"message": "Услуга"
},
"forwardedEmail": {
"message": "Forwarded email alias"
"message": "Псевдоним на препратена е-поща"
},
"forwardedEmailDesc": {
"message": "Generate an email alias with an external forwarding service."
"message": "Създайте псевдоним на е-поща с външна услуга за препращане."
},
"hostname": {
"message": "Име на сървъра",

View File

@@ -53,19 +53,19 @@
"message": "Tab"
},
"vault": {
"message": "Vault"
"message": "Trezor"
},
"myVault": {
"message": "My vault"
"message": "Moj trezor"
},
"allVaults": {
"message": "All vaults"
},
"tools": {
"message": "Tools"
"message": "Alati"
},
"settings": {
"message": "Settings"
"message": "Postavke"
},
"currentTab": {
"message": "Current tab"
@@ -116,7 +116,7 @@
"message": "Add item"
},
"passwordHint": {
"message": "Password hint"
"message": "Nagovještaj lozinke"
},
"enterEmailToGetHint": {
"message": "Enter your account email address to receive your master password hint."
@@ -125,7 +125,7 @@
"message": "Get master password hint"
},
"continue": {
"message": "Continue"
"message": "Nastavi"
},
"sendVerificationCode": {
"message": "Send a verification code to your email"

View File

@@ -1393,7 +1393,7 @@
"message": "Una o més polítiques dorganització afecten la configuració del generador."
},
"vaultTimeoutAction": {
"message": "Acció del temps d'espera de la caixa forta"
"message": "Acció quan acabe el temps d'espera de la caixa forta"
},
"lock": {
"message": "Bloqueja",
@@ -2030,7 +2030,7 @@
}
},
"loginWithMasterPassword": {
"message": "Inicia sessió amb la contrasenya mestra"
"message": "Inici de sessió amb contrasenya mestra"
},
"loggingInAs": {
"message": "Has iniciat sessió com"

View File

@@ -149,7 +149,7 @@
"message": "Změnit hlavní heslo"
},
"fingerprintPhrase": {
"message": "Fráze otisku účtu",
"message": "Unikátní přístupová fráze",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"yourAccountsFingerprint": {
@@ -606,7 +606,7 @@
"message": "Zeptat se na aktualizaci existujícího přihlášení"
},
"changedPasswordNotificationDesc": {
"message": "Ask to update a login's password when a change is detected on a website."
"message": "Dotázat se na aktualizaci hesla pro přihlášení, pokud je na webové stránce zjištěno použití jiného hesla."
},
"notificationChangeDesc": {
"message": "Chcete aktualizovat toto heslo v Bitwarden?"
@@ -618,7 +618,7 @@
"message": "Zobrazit v kontextovém menu"
},
"contextMenuItemDesc": {
"message": "Use a secondary click to access password generation and matching logins for the website. "
"message": "Použijte pravé tlačítko pro přístup k vytvoření hesla a odpovídajícímu přihlášení pro tuto stránku. "
},
"defaultUriMatchDetection": {
"message": "Výchozí zjišťování shody URI",
@@ -1043,10 +1043,10 @@
"message": "Zobrazit rozeznatelný obrázek vedle každého přihlášení."
},
"enableBadgeCounter": {
"message": "Show badge counter"
"message": "Zobrazovat počet uložených přihlašovacích údajů na stránce"
},
"badgeCounterDesc": {
"message": "Zobrazit počet přihlašovacích údajů pro aktuální webovou stránku."
"message": "Zobrazit počet přihlašovacích údajů pro aktuální webovou stránku na ikoně rozšíření prohlížeče."
},
"cardholderName": {
"message": "Jméno držitele karty"
@@ -1632,7 +1632,7 @@
"message": "Odstraněné heslo"
},
"deletedSend": {
"message": "Smazaný Send",
"message": "Send odstraněn",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLink": {
@@ -1899,10 +1899,10 @@
"message": "Vypršel časový limit relace. Vraťte se prosím zpět a zkuste se znovu přihlásit."
},
"exportingPersonalVaultTitle": {
"message": "Exporting individual vault"
"message": "Export mého trezoru"
},
"exportingPersonalVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included.",
"message": "Budou exportovány pouze položky trezoru spojené s účtem $EMAIL$. Nebudou zahrnuty položky trezoru v organizaci.",
"placeholders": {
"email": {
"content": "$1",
@@ -1933,7 +1933,7 @@
"message": "E-mail pro doménový koš"
},
"catchallEmailDesc": {
"message": "Use your domain's configured catch-all inbox."
"message": "Použijte nakonfigurovanou univerzální schránku své domény."
},
"random": {
"message": "Náhodný"
@@ -1970,16 +1970,16 @@
"message": "API klíč"
},
"ssoKeyConnectorError": {
"message": "Key connector error: make sure key connector is available and working correctly."
"message": "Chyba Key Connector: ujistěte se, že je Key Connector k dispozici a funguje správně."
},
"premiumSubcriptionRequired": {
"message": "Vyžadováno prémiové předplatné"
},
"organizationIsDisabled": {
"message": "Organization suspended."
"message": "Organizace je deaktivována."
},
"disabledOrganizationFilterError": {
"message": "Items in suspended Organizations cannot be accessed. Contact your Organization owner for assistance."
"message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace."
},
"cardBrandMir": {
"message": "Mir"
@@ -2000,19 +2000,19 @@
"message": "Klikněte zde"
},
"environmentEditedReset": {
"message": "to reset to pre-configured settings"
"message": "pro obnovení do přednastavených nastavení"
},
"serverVersion": {
"message": "Verze serveru"
},
"selfHosted": {
"message": "Self-hosted"
"message": "Vlastní hosting"
},
"thirdParty": {
"message": "Third-party"
"message": "Tretí strana"
},
"thirdPartyServerMessage": {
"message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.",
"message": "Připojeno k serveru třetí strany $SERVERNAME$. Ověřte chyby připojením na oficiální server nebo nahlaste problém správci serveru.",
"placeholders": {
"servername": {
"content": "$1",

View File

@@ -2030,18 +2030,18 @@
}
},
"loginWithMasterPassword": {
"message": "Log in with master password"
"message": "Log ind med hovedadgangskode"
},
"loggingInAs": {
"message": "Logging in as"
"message": "Logger ind som"
},
"notYou": {
"message": "Not you?"
"message": "Ikke dig?"
},
"newAroundHere": {
"message": "New around here?"
"message": "Ny her?"
},
"rememberEmail": {
"message": "Remember email"
"message": "Husk e-mail"
}
}

View File

@@ -98,7 +98,7 @@
"message": "Benutzerdefinierten Feldnamen kopieren"
},
"noMatchingLogins": {
"message": "Keine passenden Zugangsdaten."
"message": "Keine passenden Zugangsdaten"
},
"unlockVaultMenu": {
"message": "Entsperre deinen Tresor"
@@ -199,7 +199,7 @@
"message": "Synchronisierung"
},
"syncVaultNow": {
"message": "Datenspeicher jetzt synchronisieren"
"message": "Tresor jetzt synchronisieren"
},
"lastSync": {
"message": "Zuletzt synchronisiert:"
@@ -491,7 +491,7 @@
"message": "Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?"
},
"twoStepLoginConfirmation": {
"message": "Mit der Zwei-Faktor-Authentifizierung wird Ihr Account zusätzlich abgesichert, da jede Anmeldung durch einen Sicherheitscode, eine Authentifizierungs-App, eine SMS, einen Anruf oder eine E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchten Sie die Seite jetzt öffnen?"
"message": "Mit der Zwei-Faktor-Authentifizierung wird dein Account zusätzlich abgesichert, da jede Anmeldung durch einen Sicherheitscode, eine Authentifizierungs-App, eine SMS, einen Anruf oder eine E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Seite jetzt öffnen?"
},
"editedFolder": {
"message": "Ordner bearbeitet"
@@ -574,7 +574,7 @@
"message": "Danach fragen Zugangsdaten hinzuzufügen"
},
"addLoginNotificationDesc": {
"message": "Die \"Login hinzufügen\" Benachrichtigung fragt Sie automatisch, ob Sie neue Zugangsdaten in Ihrem Tresor speichern möchten, wenn Sie sich zum ersten Mal mit ihnen anmelden."
"message": "Die \"Login hinzufügen\" Benachrichtigung fragt dich automatisch, ob du neue Zugangsdaten im Tresor speichern möchtest, wenn du dich zum ersten Mal mit ihnen anmeldest."
},
"showCardsCurrentTab": {
"message": "Karten auf Tab Seite anzeigen"
@@ -738,7 +738,7 @@
"message": "Datei"
},
"selectFile": {
"message": "Wähle eine Datei."
"message": "Wähle eine Datei"
},
"maxFileSize": {
"message": "Die maximale Dateigröße beträgt 500 MB."
@@ -762,7 +762,7 @@
"message": "Mitgliedschaft erneuern"
},
"premiumNotCurrentMember": {
"message": "Sie haben derzeit keine Premium-Mitgliedschaft."
"message": "Du bist derzeit kein Premium-Mitglied."
},
"premiumSignUpAndGet": {
"message": "Werde Premium-Mitglied und erhalte dafür:"
@@ -783,7 +783,7 @@
"message": "Vorrangiger Kunden-Support."
},
"ppremiumSignUpFuture": {
"message": "Alle zukünftigen Premium-Features. Mehr in Kürze!"
"message": "Alle zukünftigen Premium-Funktionen. Mehr in Kürze!"
},
"premiumPurchase": {
"message": "Premium-Mitgliedschaft kaufen"
@@ -792,7 +792,7 @@
"message": "Du kannst deine Premium-Mitgliedschaft im Bitwarden.com Web-Tresor kaufen. Möchtest du die Webseite jetzt besuchen?"
},
"premiumCurrentMember": {
"message": "Sie sind jetzt Premium-Mitglied!"
"message": "Du bist jetzt Premium-Mitglied!"
},
"premiumCurrentMemberThanks": {
"message": "Vielen Dank, dass du Bitwarden unterstützt."
@@ -870,7 +870,7 @@
"message": "Authentifiziere WebAuthn"
},
"loginUnavailable": {
"message": "Login nicht verfügbar"
"message": "Anmeldung nicht verfügbar"
},
"noTwoStepProviders": {
"message": "Dieses Konto hat eine aktive Zwei-Faktor Authentifizierung, allerdings wird keiner der konfigurierten Zwei-Faktor Anbieter von diesem Browser unterstützt."

View File

@@ -1312,7 +1312,7 @@
"description": "ex. Date this item was updated"
},
"dateCreated": {
"message": "Created",
"message": "Δημιουργήθηκε",
"description": "ex. Date this item was created"
},
"datePasswordUpdated": {
@@ -2006,13 +2006,13 @@
"message": "Έκδοση διακομιστή"
},
"selfHosted": {
"message": "Self-hosted"
"message": "Αυτο-φιλοξενείται"
},
"thirdParty": {
"message": "Third-party"
},
"thirdPartyServerMessage": {
"message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.",
"message": "Συνδέθηκε με υλοποίηση διακομιστή τρίτων, $SERVERNAME$. Παρακαλώ επαληθεύστε τα σφάλματα χρησιμοποιώντας τον επίσημο διακομιστή, ή αναφέρετε τα στον διακομιστή τρίτων.",
"placeholders": {
"servername": {
"content": "$1",
@@ -2021,7 +2021,7 @@
}
},
"lastSeenOn": {
"message": "last seen on: $DATE$",
"message": "ημερομηνία τελευταίας προβολής: $DATE$",
"placeholders": {
"date": {
"content": "$1",
@@ -2030,18 +2030,18 @@
}
},
"loginWithMasterPassword": {
"message": "Log in with master password"
"message": "Συνδεθείτε με τον κύριο κωδικό πρόσβασης"
},
"loggingInAs": {
"message": "Logging in as"
"message": "Σύνδεση ως"
},
"notYou": {
"message": "Not you?"
"message": "Δεν είστε εσείς;"
},
"newAroundHere": {
"message": "New around here?"
"message": "Νέος/α στα μέρη μας;"
},
"rememberEmail": {
"message": "Remember email"
"message": "Απομνημόνευση email"
}
}

View File

@@ -1123,6 +1123,9 @@
"dr": {
"message": "Dr"
},
"mx": {
"message": "Mx"
},
"firstName": {
"message": "First name"
},
@@ -2058,5 +2061,35 @@
},
"origin": {
"message": "Origin"
},
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
"exposedMasterPasswordDesc": {
"message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?"
},
"weakAndExposedMasterPassword": {
"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?"
},
"checkForBreaches": {
"message": "Check known data breaches for this password"
},
"important": {
"message": "Important:"
},
"masterPasswordHint": {
"message": "Your master password cannot be recovered if you forget it!"
},
"characterMinimum": {
"message": "$LENGTH$ character minimum",
"placeholders": {
"length": {
"content": "$1",
"example": "14"
}
}
}
}

View File

@@ -59,7 +59,7 @@
"message": "My vault"
},
"allVaults": {
"message": "All Vaults"
"message": "All vaults"
},
"tools": {
"message": "Tools"
@@ -92,13 +92,13 @@
"message": "Auto-fill"
},
"generatePasswordCopied": {
"message": "Generate Password (and Copy)"
"message": "Generate password (copied)"
},
"copyElementIdentifier": {
"message": "Copy Custom Field Name"
"message": "Copy custom field name"
},
"noMatchingLogins": {
"message": "No matching logins."
"message": "No matching logins"
},
"unlockVaultMenu": {
"message": "Unlock your vault"
@@ -131,10 +131,10 @@
"message": "Send a verification code to your email"
},
"sendCode": {
"message": "Send Code"
"message": "Send code"
},
"codeSent": {
"message": "Code Sent"
"message": "Code sent"
},
"verificationCode": {
"message": "Verification code"
@@ -245,7 +245,7 @@
"message": "Numbers (0-9)"
},
"specialCharacters": {
"message": "Special Characters (!@#$%^&*)"
"message": "Special characters (!@#$%^&*)"
},
"numWords": {
"message": "Number of words"
@@ -339,7 +339,7 @@
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead."
},
"verifyIdentity": {
"message": "Verify Identity"
"message": "Verify identity"
},
"yourVaultIsLocked": {
"message": "Your vault is locked. Verify your identity to continue."
@@ -482,7 +482,7 @@
"message": "Name is required."
},
"addedFolder": {
"message": "Added folder"
"message": "Folder added"
},
"changeMasterPass": {
"message": "Change master password"
@@ -491,16 +491,16 @@
"message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?"
},
"twoStepLoginConfirmation": {
"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 enabled on the bitwarden.com web vault. Do you want to visit the website now?"
"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": "Edited folder"
"message": "Folder saved"
},
"deleteFolderConfirmation": {
"message": "Are you sure you want to delete this folder?"
},
"deletedFolder": {
"message": "Deleted folder"
"message": "Folder deleted"
},
"gettingStartedTutorial": {
"message": "Getting started tutorial"
@@ -534,16 +534,16 @@
"message": "New URI"
},
"addedItem": {
"message": "Added item"
"message": "Item added"
},
"editedItem": {
"message": "Edited item"
"message": "Item saved"
},
"deleteItemConfirmation": {
"message": "Do you really want to send to the bin?"
},
"deletedItem": {
"message": "Deleted item"
"message": "Item sent to bin"
},
"overwritePassword": {
"message": "Overwrite password"
@@ -552,7 +552,7 @@
"message": "Are you sure you want to overwrite the current password?"
},
"overwriteUsername": {
"message": "Overwrite Username"
"message": "Overwrite username"
},
"overwriteUsernameConfirmation": {
"message": "Are you sure you want to overwrite the current username?"
@@ -656,7 +656,7 @@
"description": "WARNING (should stay in capitalized letters if the language permits)"
},
"confirmVaultExport": {
"message": "Confirm Vault Export"
"message": "Confirm vault export"
},
"exportWarningDesc": {
"message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over insecure channels (such as email). Delete it immediately after you are done using it."
@@ -680,7 +680,7 @@
"message": "Bitwarden allows you to share your vault items with others by using an organisation. Would you like to visit the bitwarden.com website to learn more?"
},
"moveToOrganization": {
"message": "Move to Organisation"
"message": "Move to organisation"
},
"share": {
"message": "Share"
@@ -723,7 +723,7 @@
"message": "Are you sure you want to delete this attachment?"
},
"deletedAttachment": {
"message": "Deleted attachment"
"message": "Attachment deleted"
},
"newAttachment": {
"message": "Add new attachment"
@@ -732,13 +732,13 @@
"message": "No attachments."
},
"attachmentSaved": {
"message": "The attachment has been saved."
"message": "Attachment saved"
},
"file": {
"message": "File"
},
"selectFile": {
"message": "Select a file."
"message": "Select a file"
},
"maxFileSize": {
"message": "Maximum file size is 500 MB."
@@ -762,10 +762,10 @@
"message": "Refresh membership"
},
"premiumNotCurrentMember": {
"message": "You are not currently a premium member."
"message": "You are not currently a Premium member."
},
"premiumSignUpAndGet": {
"message": "Sign up for a premium membership and get:"
"message": "Sign up for a Premium membership and get:"
},
"ppremiumSignUpStorage": {
"message": "1 GB encrypted storage for file attachments."
@@ -783,16 +783,16 @@
"message": "Priority customer support."
},
"ppremiumSignUpFuture": {
"message": "All future premium features. More coming soon!"
"message": "All future Premium features. More coming soon!"
},
"premiumPurchase": {
"message": "Purchase premium"
},
"premiumPurchaseAlert": {
"message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?"
"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": "You are a Premium member!"
},
"premiumCurrentMemberThanks": {
"message": "Thank you for supporting Bitwarden."
@@ -822,7 +822,7 @@
"message": "Premium required"
},
"premiumRequiredDesc": {
"message": "A premium membership is required to use this feature."
"message": "A Premium membership is required to use this feature."
},
"enterVerificationCodeApp": {
"message": "Enter the 6 digit verification code from your authenticator app."
@@ -873,7 +873,7 @@
"message": "Login unavailable"
},
"noTwoStepProviders": {
"message": "This account has two-step login enabled. However, none of the configured two-step providers are supported by this web browser."
"message": "This account has two-step login set up; however, none of the configured two-step providers are supported by this web browser."
},
"noTwoStepProviders2": {
"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)."
@@ -882,7 +882,7 @@
"message": "Two-step login options"
},
"recoveryCodeDesc": {
"message": "Lost access to all of your two-factor providers? Use your recovery code to disable all two-factor providers from your account."
"message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers from your account."
},
"recoveryCodeTitle": {
"message": "Recovery code"
@@ -912,7 +912,7 @@
"message": "FIDO2 WebAuthn"
},
"webAuthnDesc": {
"message": "Use any WebAuthn enabled security key to access your account."
"message": "Use any WebAuthn compatible security key to access your account."
},
"emailTitle": {
"message": "Email"
@@ -951,7 +951,7 @@
"message": "Icons server URL"
},
"environmentSaved": {
"message": "The environment URLs have been saved."
"message": "Environment URLs saved"
},
"enableAutoFillOnPageLoad": {
"message": "Auto-fill on page load"
@@ -969,7 +969,7 @@
"message": "You can turn off auto-fill on page load for individual login items from the item's Edit view."
},
"itemAutoFillOnPageLoad": {
"message": "Auto-fill on page load (if enabled in Options)"
"message": "Auto-fill on page load (if set up in Options)"
},
"autoFillOnPageLoadUseDefault": {
"message": "Use default setting"
@@ -1133,7 +1133,7 @@
"message": "Last name"
},
"fullName": {
"message": "Full Name"
"message": "Full name"
},
"identityName": {
"message": "Identity name"
@@ -1375,7 +1375,7 @@
"message": "Awaiting confirmation from desktop"
},
"awaitDesktopDesc": {
"message": "Please confirm using biometrics in the Bitwarden Desktop application to enable biometrics for browser."
"message": "Please confirm using biometrics in the Bitwarden desktop application to set up biometrics for browser."
},
"lockWithMasterPassOnRestart": {
"message": "Lock with master password on browser restart"
@@ -1413,7 +1413,7 @@
"message": "Are you sure you want to permanently delete this item?"
},
"permanentlyDeletedItem": {
"message": "Permanently deleted item"
"message": "Item permanently deleted"
},
"restoreItem": {
"message": "Restore item"
@@ -1422,7 +1422,7 @@
"message": "Are you sure you want to restore this item?"
},
"restoredItem": {
"message": "Restored item"
"message": "Item restored"
},
"vaultTimeoutLogOutConfirmation": {
"message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?"
@@ -1434,10 +1434,10 @@
"message": "Auto-fill and save"
},
"autoFillSuccessAndSavedUri": {
"message": "Auto-filled item and saved URI"
"message": "Item auto-filled and URI saved"
},
"autoFillSuccess": {
"message": "Auto-filled item"
"message": "Item auto-filled "
},
"setMasterPassword": {
"message": "Set master password"
@@ -1509,19 +1509,19 @@
"message": "Please verify that the desktop application shows this fingerprint: "
},
"desktopIntegrationDisabledTitle": {
"message": "Browser integration is not enabled"
"message": "Browser integration is not set up"
},
"desktopIntegrationDisabledDesc": {
"message": "Browser integration is not enabled in the Bitwarden Desktop application. Please enable it in the settings within the desktop application."
"message": "Browser integration is not set up in the Bitwarden desktop application. Please set it up in the settings within the desktop application."
},
"startDesktopTitle": {
"message": "Start the Bitwarden Desktop application"
"message": "Start the Bitwarden desktop application"
},
"startDesktopDesc": {
"message": "The Bitwarden Desktop application needs to be started before unlock with biometrics can be used."
"message": "The Bitwarden desktop application needs to be started before unlock with biometrics can be used."
},
"errorEnableBiometricTitle": {
"message": "Unable to enable biometrics"
"message": "Unable to set up biometrics"
},
"errorEnableBiometricDesc": {
"message": "Action was cancelled by the desktop application"
@@ -1539,10 +1539,10 @@
"message": "Account mismatch"
},
"biometricsNotEnabledTitle": {
"message": "Biometrics not enabled"
"message": "Biometrics not set up"
},
"biometricsNotEnabledDesc": {
"message": "Browser biometrics requires desktop biometric to be enabled in the settings first."
"message": "Browser biometrics requires desktop biometric to be set up in the settings first."
},
"biometricsNotSupportedTitle": {
"message": "Biometrics not supported"
@@ -1563,7 +1563,7 @@
"message": "This action cannot be done in the sidebar, please retry the action in the popup or popout."
},
"personalOwnershipSubmitError": {
"message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organisation and choose from available Collections."
"message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organisation and choose from available collections."
},
"personalOwnershipPolicyInEffect": {
"message": "An organisation policy is affecting your ownership options."
@@ -1629,10 +1629,10 @@
"message": "Delete"
},
"removedPassword": {
"message": "Removed Password"
"message": "Password removed"
},
"deletedSend": {
"message": "Deleted Send",
"message": "Send deleted",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLink": {
@@ -1669,14 +1669,14 @@
"message": "The file you want to send."
},
"deletionDate": {
"message": "Deletion Date"
"message": "Deletion date"
},
"deletionDateDesc": {
"message": "The Send will be permanently deleted on the specified date and time.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"expirationDate": {
"message": "Expiration Date"
"message": "Expiration date"
},
"expirationDateDesc": {
"message": "If set, access to this Send will expire on the specified date and time.",
@@ -1713,7 +1713,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDisableDesc": {
"message": "Disable this Send so that no one can access it.",
"message": "Deactivate this Send so that no one can access it.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendShareDesc": {
@@ -1728,17 +1728,17 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"currentAccessCount": {
"message": "Current Access Count"
"message": "Current access count"
},
"createSend": {
"message": "Create New Send",
"message": "New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"newPassword": {
"message": "New Password"
"message": "New password"
},
"sendDisabled": {
"message": "Send Disabled",
"message": "Send removed",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDisabledWarning": {
@@ -1746,11 +1746,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Created Send",
"message": "Send created",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Edited Send",
"message": "Send saved",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLinuxChromiumFileWarning": {
@@ -1808,22 +1808,22 @@
"message": "This action is protected. To continue, please re-enter your master password to verify your identity."
},
"emailVerificationRequired": {
"message": "Email Verification Required"
"message": "Email verification required"
},
"emailVerificationRequiredDesc": {
"message": "You must verify your email to use this feature. You can verify your email in the web vault."
},
"updatedMasterPassword": {
"message": "Updated Master Password"
"message": "Updated master password"
},
"updateMasterPassword": {
"message": "Update Master Password"
"message": "Update master password"
},
"updateMasterPasswordWarning": {
"message": "Your Master Password was recently changed by an administrator in your organisation. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
"message": "Your master password was recently changed by an administrator in your organisation. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic Enrolment"
"message": "Automatic enrolment"
},
"resetPasswordAutoEnrollInviteWarning": {
"message": "This organisation has an enterprise policy that will automatically enrol you in password reset. Enrolment will allow organisation administrators to change your master password."
@@ -1857,10 +1857,10 @@
"message": "Your vault timeout exceeds the restrictions set by your organisation."
},
"vaultExportDisabled": {
"message": "Vault Export Disabled"
"message": "Vault export unavailable"
},
"personalVaultExportPolicyInEffect": {
"message": "One or more organisation policies prevents you from exporting your personal vault."
"message": "One or more organisation policies prevents you from exporting your individual vault."
},
"copyCustomFieldNameInvalidElement": {
"message": "Unable to identify a valid form element. Try inspecting the HTML instead."
@@ -1878,13 +1878,13 @@
}
},
"leaveOrganization": {
"message": "Leave Organisation"
"message": "Leave organisation"
},
"removeMasterPassword": {
"message": "Remove Master Password"
"message": "Remove master password"
},
"removedMasterPassword": {
"message": "Master password removed."
"message": "Master password removed"
},
"leaveOrganizationConfirmation": {
"message": "Are you sure you want to leave this organisation?"
@@ -1899,10 +1899,10 @@
"message": "Your session has timed out. Please go back and try logging in again."
},
"exportingPersonalVaultTitle": {
"message": "Exporting Personal Vault"
"message": "Exporting individual vault"
},
"exportingPersonalVaultDescription": {
"message": "Only the personal vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included.",
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included.",
"placeholders": {
"email": {
"content": "$1",
@@ -1914,23 +1914,23 @@
"message": "Error"
},
"regenerateUsername": {
"message": "Regenerate Username"
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate Username"
"message": "Generate username"
},
"usernameType": {
"message": "Username Type"
"message": "Username type"
},
"plusAddressedEmail": {
"message": "Plus Addressed Email",
"message": "Plus addressed email",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
},
"plusAddressedEmailDesc": {
"message": "Use your email provider's sub-addressing capabilities."
},
"catchallEmail": {
"message": "Catch-all Email"
"message": "Catch-all email"
},
"catchallEmailDesc": {
"message": "Use your domain's configured catch-all inbox."
@@ -1939,22 +1939,22 @@
"message": "Random"
},
"randomWord": {
"message": "Random Word"
"message": "Random word"
},
"websiteName": {
"message": "Website Name"
"message": "Website name"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password Type"
"message": "Password type"
},
"service": {
"message": "Service"
},
"forwardedEmail": {
"message": "Forwarded Email Alias"
"message": "Forwarded email alias"
},
"forwardedEmailDesc": {
"message": "Generate an email alias with an external forwarding service."
@@ -1970,16 +1970,16 @@
"message": "API Key"
},
"ssoKeyConnectorError": {
"message": "Key Connector error: make sure Key Connector is available and working correctly."
"message": "Key connector error: make sure key connector is available and working correctly."
},
"premiumSubcriptionRequired": {
"message": "Premium subscription required"
},
"organizationIsDisabled": {
"message": "Organisation is disabled."
"message": "Organisation suspended."
},
"disabledOrganizationFilterError": {
"message": "Items in disabled Organisations cannot be accessed. Contact your Organisation owner for assistance."
"message": "Items in suspended Organisations cannot be accessed. Contact your Organisation owner for assistance."
},
"cardBrandMir": {
"message": "Mir"
@@ -2003,13 +2003,13 @@
"message": "to reset to pre-configured settings"
},
"serverVersion": {
"message": "Server Version"
"message": "Server version"
},
"selfHosted": {
"message": "Self-Hosted"
"message": "Self-hosted"
},
"thirdParty": {
"message": "Third-Party"
"message": "Third-party"
},
"thirdPartyServerMessage": {
"message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.",
@@ -2021,7 +2021,7 @@
}
},
"lastSeenOn": {
"message": "last seen on $DATE$",
"message": "last seen on: $DATE$",
"placeholders": {
"date": {
"content": "$1",

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden - Free Password Manager",
"message": "Bitwarden - Libreng Password Manager",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
@@ -20,7 +20,7 @@
"message": "Mag-login"
},
"enterpriseSingleSignOn": {
"message": "Enterprise single sign-on"
"message": "Enterprise Single Sign-On sa Filipino ay Isang Sign-On na Enterprise"
},
"cancel": {
"message": "Kanselahin"
@@ -47,19 +47,19 @@
"message": "Muling i-type ang Master Password"
},
"masterPassHint": {
"message": "Master password hint (optional)"
"message": "Mungkahi sa Master Password (opsyonal)"
},
"tab": {
"message": "Tab"
},
"vault": {
"message": "Vault"
"message": "Ayos"
},
"myVault": {
"message": "Aking Kahadeyero"
},
"allVaults": {
"message": "All vaults"
"message": "Lahat ng Vault"
},
"tools": {
"message": "Mga Kagamitan"
@@ -80,34 +80,34 @@
"message": "Kopyahin ang URI"
},
"copyUsername": {
"message": "Copy username"
"message": "Kopyahin ang pangalan ng gumagamit"
},
"copyNumber": {
"message": "Copy number"
"message": "Pamamagitan ng Kopya ng Bilang"
},
"copySecurityCode": {
"message": "Copy security code"
"message": "Kopyahin ang code ng seguridad"
},
"autoFill": {
"message": "Auto-fill"
"message": "Auto-fill sa Filipino ay Awtomatikong Pagpuno"
},
"generatePasswordCopied": {
"message": "Generate password (copied)"
"message": "Maglagay ng Password"
},
"copyElementIdentifier": {
"message": "Copy custom field name"
"message": "Salin ang Pangalan ng Pasadyang Field"
},
"noMatchingLogins": {
"message": "No matching logins"
"message": "Walang tumutugmang mga login"
},
"unlockVaultMenu": {
"message": "Buksan ang iyong kahadeyero"
},
"loginToVaultMenu": {
"message": "Log in to your vault"
"message": "Ag-log in sa iyong bangko"
},
"autoFillInfo": {
"message": "There are no logins available to auto-fill for the current browser tab."
"message": "Walang mga login na magagamit para i-auto-fill para sa kasalukuyang tab ng browser."
},
"addLogin": {
"message": "Magdagdag ng Login"
@@ -116,28 +116,28 @@
"message": "Magdagdag ng Item"
},
"passwordHint": {
"message": "Password hint"
"message": "Mungkahi sa Password"
},
"enterEmailToGetHint": {
"message": "Enter your account email address to receive your master password hint."
"message": "Ipasok ang iyong email address ng account para makatanggap ng hint ng iyong master password."
},
"getMasterPasswordHint": {
"message": "Get master password hint"
"message": "Makuha ang Punong Password na Hint"
},
"continue": {
"message": "Magpatuloy"
},
"sendVerificationCode": {
"message": "Send a verification code to your email"
"message": "Magpadala ng isang verification code sa iyong email"
},
"sendCode": {
"message": "Send code"
"message": "Magpadala ng Code"
},
"codeSent": {
"message": "Ipinadala ang Code"
},
"verificationCode": {
"message": "Verification code"
"message": "VerificationCode sa Filipino ay Pagsasagot sa Tanong"
},
"confirmIdentity": {
"message": "Kumpirmahin ang iyong identididad upang magpatuloy."
@@ -146,112 +146,112 @@
"message": "Account"
},
"changeMasterPassword": {
"message": "Change master password"
"message": "Baguhin ang Master Password"
},
"fingerprintPhrase": {
"message": "Fingerprint phrase",
"message": "Hulmabig ng Hilik ng Dako",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"yourAccountsFingerprint": {
"message": "Your account's fingerprint phrase",
"message": "Ang fingerprint pala ng iyong account",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"twoStepLogin": {
"message": "Two-step login"
"message": "Dalawahang-hakbang na Pag-login"
},
"logOut": {
"message": "Log out"
"message": "Mag-Log Out"
},
"about": {
"message": "About"
"message": "Tungkol"
},
"version": {
"message": "Version"
"message": "Bersyon"
},
"save": {
"message": "Save"
"message": "I-save"
},
"move": {
"message": "Move"
"message": "Lumipat"
},
"addFolder": {
"message": "Add folder"
"message": "Magdagdag ng folder"
},
"name": {
"message": "Name"
"message": "Pangalan"
},
"editFolder": {
"message": "Edit folder"
"message": "I-edit ang folder"
},
"deleteFolder": {
"message": "Delete folder"
"message": "Burahin ang folder"
},
"folders": {
"message": "Folders"
"message": "Mga Folder"
},
"noFolders": {
"message": "There are no folders to list."
"message": "Walang mga folder na listahan."
},
"helpFeedback": {
"message": "Help & feedback"
"message": "Tulong at Mga Feedback"
},
"sync": {
"message": "Sync"
"message": "Ikintal"
},
"syncVaultNow": {
"message": "Sync vault now"
"message": "Isingit ang Vault ngayon"
},
"lastSync": {
"message": "Last sync:"
"message": "Huling sinkronisasyon:"
},
"passGen": {
"message": "Password generator"
"message": "Tagapaglikha ng Password"
},
"generator": {
"message": "Generator",
"message": "Magmamana",
"description": "Short for 'Password Generator'."
},
"passGenInfo": {
"message": "Automatically generate strong, unique passwords for your logins."
"message": "Automatiko na gumawa ng mga malakas at natatanging mga password para sa iyong mga logins."
},
"bitWebVault": {
"message": "Bitwarden web vault"
},
"importItems": {
"message": "Import items"
"message": "Isingit ang Vault ngayon"
},
"select": {
"message": "Select"
"message": "Piliin"
},
"generatePassword": {
"message": "Generate password"
"message": "Magtatag ng Password"
},
"regeneratePassword": {
"message": "Regenerate password"
"message": "Muling I-generate ang Password"
},
"options": {
"message": "Options"
"message": "Mga Pagpipilian"
},
"length": {
"message": "Length"
"message": "Kahabaan"
},
"uppercase": {
"message": "Uppercase (A-Z)"
"message": "2. Mga letra na malalaki (A-Z)"
},
"lowercase": {
"message": "Lowercase (a-z)"
"message": "Lowercase (a-t) sa Filipino ay mababang-letra (a-t)"
},
"numbers": {
"message": "Numbers (0-9)"
},
"specialCharacters": {
"message": "Special characters (!@#$%^&*)"
"message": "Mga espesyal na character (!@#$%^&*) sa Filipino ay tinatawag na mga simbolong pambihira"
},
"numWords": {
"message": "Number of words"
"message": "Ang bilang ng mga salita\n\nNumero ng mga salita"
},
"wordSeparator": {
"message": "Word separator"
"message": "Separador ng salita"
},
"capitalize": {
"message": "Capitalize",
@@ -267,16 +267,16 @@
"message": "Minimum special"
},
"avoidAmbChar": {
"message": "Avoid ambiguous characters"
"message": "Iwasan ang mga hindi malinaw na character"
},
"searchVault": {
"message": "Search vault"
},
"edit": {
"message": "Edit"
"message": "I-edit"
},
"view": {
"message": "View"
"message": "Tanaw"
},
"noItemsInList": {
"message": "There are no items to list."
@@ -285,16 +285,16 @@
"message": "Item information"
},
"username": {
"message": "Username"
"message": "Ang pangalan ng tagagamit"
},
"password": {
"message": "Password"
"message": "Ang Password"
},
"passphrase": {
"message": "Passphrase"
"message": "Pasa salita"
},
"favorite": {
"message": "Favorite"
"message": "Ang Paborito"
},
"notes": {
"message": "Notes"
@@ -303,19 +303,19 @@
"message": "Note"
},
"editItem": {
"message": "Edit item"
"message": "Baguhin ang Item"
},
"folder": {
"message": "Folder"
"message": "Folder/Direktoryo"
},
"deleteItem": {
"message": "Delete item"
"message": "Tanggalin ang Item"
},
"viewItem": {
"message": "View item"
"message": "Tingnan ang Item"
},
"launch": {
"message": "Launch"
"message": "Paglulunsad"
},
"website": {
"message": "Website"
@@ -339,13 +339,13 @@
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead."
},
"verifyIdentity": {
"message": "Verify identity"
"message": "I-verify ang pagkakakilanlan"
},
"yourVaultIsLocked": {
"message": "Your vault is locked. Verify your identity to continue."
},
"unlock": {
"message": "Unlock"
"message": "Buksan"
},
"loggedInAsOn": {
"message": "Logged in as $EMAIL$ on $HOSTNAME$.",
@@ -367,7 +367,7 @@
"message": "Vault timeout"
},
"lockNow": {
"message": "Lock now"
"message": "Mag-kandado Na"
},
"immediately": {
"message": "Immediately"
@@ -467,7 +467,7 @@
"message": "Your login session has expired."
},
"logOutConfirmation": {
"message": "Are you sure you want to log out?"
"message": "Sigurado ka bang gusto mong mag-log out?"
},
"yes": {
"message": "Yes"

View File

@@ -32,22 +32,22 @@
"message": "Soumettre"
},
"emailAddress": {
"message": "Adresse e-mail"
"message": "Adresse électronique"
},
"masterPass": {
"message": "Mot de passe maître"
"message": "Mot de passe principal"
},
"masterPassDesc": {
"message": "Le mot de passe maître est le mot de passe que vous utilisez pour accéder à votre coffre. Il est très important de ne pas l'oublier. Il n'existe aucun moyen de le récupérer si vous le perdez."
"message": "Le mot de passe principal est le mot de passe que vous utilisez pour accéder à votre coffre. Il est très important de ne pas oublier votre mot de passe principal. Il n'existe aucun moyen de récupérer le mot de passe si vous l'oubliez."
},
"masterPassHintDesc": {
"message": "Un indice de mot de passe maître peut vous aider à vous rappeler de votre mot de passe en cas d'oubli."
"message": "Un indice de mot de passe principal peut vous aider à vous souvenir de votre mot de passe si vous l'oubliez."
},
"reTypeMasterPass": {
"message": "Saisir à nouveau le mot de passe maître"
"message": "Ressaisir le mot de passe principal"
},
"masterPassHint": {
"message": "Indice du mot de passe maître (facultatif)"
"message": "Indice du mot de passe principal (facultatif)"
},
"tab": {
"message": "Onglet"
@@ -107,7 +107,7 @@
"message": "Connectez-vous à votre coffre"
},
"autoFillInfo": {
"message": "Il n'existe aucun site disponible pour le remplissage automatique pour l'onglet actuel du navigateur."
"message": "Il n'y a pas d'identifiants disponibles à saisir automatiquement pour l'onglet actuel du navigateur."
},
"addLogin": {
"message": "Ajouter un site"
@@ -119,10 +119,10 @@
"message": "Indice mot de passe"
},
"enterEmailToGetHint": {
"message": "Saisissez l'adresse e-mail de votre compte pour recevoir l'indice de votre mot de passe maître."
"message": "Saisissez l'adresse électronique de votre compte pour recevoir l'indice de votre mot de passe principal."
},
"getMasterPasswordHint": {
"message": "Obtenir l'indice du mot de passe maître"
"message": "Obtenir l'indice du mot de passe principal"
},
"continue": {
"message": "Continuer"
@@ -146,7 +146,7 @@
"message": "Compte"
},
"changeMasterPassword": {
"message": "Changer le mot de passe maître"
"message": "Changer le mot de passe principal"
},
"fingerprintPhrase": {
"message": "Phrase d'empreinte",
@@ -157,10 +157,10 @@
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"twoStepLogin": {
"message": "Identification à deux facteurs"
"message": "Authentification à deux facteurs"
},
"logOut": {
"message": "Déconnexion"
"message": "Se déconnecter"
},
"about": {
"message": "À propos"
@@ -212,13 +212,13 @@
"description": "Short for 'Password Generator'."
},
"passGenInfo": {
"message": "Générer automatiquement des mots de passe forts et uniques pour vos identifiants."
"message": "Générer automatiquement des mots de passe robustes et uniques pour vos identifiants."
},
"bitWebVault": {
"message": "Coffre en ligne de bitwarden"
},
"importItems": {
"message": "Importer des identifiants"
"message": "Importer des éléments"
},
"select": {
"message": "Sélectionner"
@@ -261,10 +261,10 @@
"message": "Inclure le numéro"
},
"minNumbers": {
"message": "Nombre minimum de chiffres"
"message": "Minimum de chiffres"
},
"minSpecial": {
"message": "Nombre minimum de caractères spéciaux"
"message": "Minimum de caractères spéciaux"
},
"avoidAmbChar": {
"message": "Éviter les caractères ambigus"
@@ -282,7 +282,7 @@
"message": "Aucun identifiant à afficher."
},
"itemInformation": {
"message": "Information sur l'élément"
"message": "Informations sur l'élément"
},
"username": {
"message": "Nom d'utilisateur"
@@ -303,16 +303,16 @@
"message": "Note"
},
"editItem": {
"message": "Modifier l'identifiant"
"message": "Éditer l'élément"
},
"folder": {
"message": "Dossier"
},
"deleteItem": {
"message": "Supprimer l'identifiant"
"message": "Supprimer l'élément"
},
"viewItem": {
"message": "Voir l'élément"
"message": "Afficher l'élément"
},
"launch": {
"message": "Ouvrir"
@@ -361,7 +361,7 @@
}
},
"invalidMasterPassword": {
"message": "Mot de passe maître invalide"
"message": "Mot de passe principal invalide"
},
"vaultTimeout": {
"message": "Délai d'expiration du coffre"
@@ -424,22 +424,22 @@
"message": "Adresse e-mail invalide."
},
"masterPasswordRequired": {
"message": "Le mot de passe maître est requis."
"message": "Le mot de passe principal est requis."
},
"confirmMasterPasswordRequired": {
"message": "Le mot de passe maître doit être entré de nouveau."
"message": "Une nouvelle saisie du mot de passe principal est nécessaire."
},
"masterPasswordMinlength": {
"message": "Le mot de passe maître doit au moins contenir 8 caractères."
"message": "Le mot de passe principal doit comporter au moins 8 caractères."
},
"masterPassDoesntMatch": {
"message": "La confirmation du mot de passe maître ne correspond pas."
"message": "La confirmation du mot de passe principal ne correspond pas."
},
"newAccountCreated": {
"message": "Votre nouveau compte a été créé ! Vous pouvez maintenant vous authentifier."
},
"masterPassSent": {
"message": "Nous vous avons envoyé un e-mail contenant votre indice de mot de passe maître."
"message": "Nous vous avons envoyé un courriel avec votre indice de mot de passe principal."
},
"verificationCodeRequired": {
"message": "Le code de vérification est requis."
@@ -485,13 +485,13 @@
"message": "Dossier ajouté"
},
"changeMasterPass": {
"message": "Modifier le mot de passe maître"
"message": "Changer le mot de passe principal"
},
"changeMasterPasswordConfirmation": {
"message": "Vous pouvez modifier votre mot de passe maître depuis le coffre web sur bitwarden.com. Souhaitez-vous visiter le site maintenant ?"
"message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?"
},
"twoStepLoginConfirmation": {
"message": "L'identification en deux étapes rend votre compte plus sécurisé en vous demandant de saisir un code de sécurité depuis une application d'authentification à chaque fois que vous vous identifiez. L'identification en deux étapes peut être activée depuis le coffre web sur bitwarden.com. Souhaitez-vous visiter le site maintenant ?"
"message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une c de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?"
},
"editedFolder": {
"message": "Dossier modifié"
@@ -534,16 +534,16 @@
"message": "Nouvel URI"
},
"addedItem": {
"message": "Identifiant ajouté"
"message": "Élément ajouté"
},
"editedItem": {
"message": "Identifiant modifié"
"message": "Élément enregistré"
},
"deleteItemConfirmation": {
"message": "Êtes-vous sûr de vouloir supprimer cet identifiant ?"
},
"deletedItem": {
"message": "L'élément a été envoyé dans la corbeille"
"message": "Élément envoyé à la corbeille"
},
"overwritePassword": {
"message": "Écraser le mot de passe"
@@ -574,19 +574,19 @@
"message": "Demander à ajouter un identifiant"
},
"addLoginNotificationDesc": {
"message": "La \"Notification de demande d'ajout d'identifiant\" apparait automatiquement pour vous demander d'enregistrer dans votre coffre les identifiants que vous utilisez pour la première fois."
"message": "Demander à ajouter un élément si aucun n'est trouvé dans votre coffre."
},
"showCardsCurrentTab": {
"message": "Show cards on Tab page"
"message": "Afficher les cartes sur la page de l'onglet"
},
"showCardsCurrentTabDesc": {
"message": "List card items on the Tab page for easy auto-fill."
"message": "Lister les éléments de la carte sur la page de l'onglet pour faciliter la saisie automatique."
},
"showIdentitiesCurrentTab": {
"message": "Show identities on Tab page"
"message": "Afficher les identités sur la page Onglet courant"
},
"showIdentitiesCurrentTabDesc": {
"message": "List identity items on the Tab page for easy auto-fill."
"message": "Lister les éléments d'identité sur la page de l'onglet pour faciliter la saisie automatique."
},
"clearClipboard": {
"message": "Effacer le presse-papiers",
@@ -618,7 +618,7 @@
"message": "Afficher les options du menu contextuel"
},
"contextMenuItemDesc": {
"message": "Use a secondary click to access password generation and matching logins for the website. "
"message": "Utilisez un clic secondaire pour accéder à la génération de mots de passe et les identifiants correspondants pour le site Web. "
},
"defaultUriMatchDetection": {
"message": "Détection de correspondance URI par défaut",
@@ -668,7 +668,7 @@
"message": "Les clés de chiffrement du compte sont spécifiques à chaque utilisateur Bitwarden. Vous ne pouvez donc pas importer d'export chiffré dans un compte différent."
},
"exportMasterPassword": {
"message": "Saisissez votre mot de passe maître pour exporter les données de votre coffre."
"message": "Saisissez votre mot de passe principal pour exporter les données de votre coffre."
},
"shared": {
"message": "Partagé"
@@ -756,49 +756,49 @@
"message": "Gérer l'adhésion"
},
"premiumManageAlert": {
"message": "Vous pouvez gérer votre adhésion depuis le coffre web sur bitwarden.com. Souhaitez-vous visiter le site web maintenant ?"
"message": "Vous pouvez gérer votre adhésion sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?"
},
"premiumRefresh": {
"message": "Actualiser l'adhésion"
},
"premiumNotCurrentMember": {
"message": "Vous n'êtes actuellement pas un membre premium."
"message": "Vous n'êtes pas actuellement un membre Premium."
},
"premiumSignUpAndGet": {
"message": "Devenez un membre premium et obtenez :"
"message": "Inscrivez-vous pour une adhésion Premium et obtenez :"
},
"ppremiumSignUpStorage": {
"message": "1 Go de stockage de fichiers chiffrés."
"message": "1 Go de stockage chiffré pour les fichiers joints."
},
"ppremiumSignUpTwoStep": {
"message": "Options d'identification en deux étapes additionnelles comme YubiKey, FIDO U2F et Duo."
"message": "Options additionnelles d'identification à deux étapes telles que YubiKey, FIDO U2F et Duo."
},
"ppremiumSignUpReports": {
"message": "Rapports sur l'hygiène des mots de passe, la santé des comptes et les fuites de données pour assurer la sécurité de votre coffre."
"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."
},
"ppremiumSignUpTotp": {
"message": "Génération d'un code de vérification TOTP (2FA) pour les identifiants de votre coffre."
"message": "Générateur de code de vérification TOTP (2FA) pour les identifiants dans votre coffre."
},
"ppremiumSignUpSupport": {
"message": "Support client prioritaire."
"message": "Assistance client prioritaire."
},
"ppremiumSignUpFuture": {
"message": "Toutes les futures options premium. Prochainement !"
"message": "Toutes les futures fonctionnalités Premium. Plus à venir prochainement !"
},
"premiumPurchase": {
"message": "Acheter Premium"
},
"premiumPurchaseAlert": {
"message": "Vous pouvez opter pour une adhésion premium depuis le coffre web sur bitwarden.com. Souhaitez-vous consulter le site web maintenant ?"
"message": "Vous pouvez acheter une adhésion Premium sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?"
},
"premiumCurrentMember": {
"message": "Vous êtes un adhérent premium !"
"message": "Vous êtes un membre Premium !"
},
"premiumCurrentMemberThanks": {
"message": "Merci de supporter Bitwarden."
"message": "Merci de soutenir Bitwarden."
},
"premiumPrice": {
"message": "Tout pour seulement $PRICE$ /an !",
"message": "Tout pour seulement $PRICE$/an !",
"placeholders": {
"price": {
"content": "$1",
@@ -816,13 +816,13 @@
"message": "Si une clé d'authentification est rattachée à votre identifiant, alors le code de vérification TOTP est automatiquement copié dans le presse-papiers lorsque vous renseignez l'identifiant."
},
"enableAutoBiometricsPrompt": {
"message": "Ask for biometrics on launch"
"message": "Demander la biométrie au lancement"
},
"premiumRequired": {
"message": "Version Premium requise"
"message": "Premium requis"
},
"premiumRequiredDesc": {
"message": "Une adhésion premium est requise pour utiliser cette fonctionnalité."
"message": "Une adhésion Premium est requise pour utiliser cette fonctionnalité."
},
"enterVerificationCodeApp": {
"message": "Saisissez le code de vérification à 6 chiffres depuis votre application d'authentification."
@@ -873,13 +873,13 @@
"message": "Identifiant non disponible"
},
"noTwoStepProviders": {
"message": "Ce compte dispose d'une authentification à double facteurs, cependant, aucun service d'authentification à double facteurs n'est supporté par ce navigateur web."
"message": "Ce compte dispose d'une authentification à deux facteurs mise en place, mais aucun des fournisseurs d'authentification à deux facteurs configurés ne sont pris en charge par ce navigateur web."
},
"noTwoStepProviders2": {
"message": "Merci d'utiliser un navigateur web compatible (comme Chrome) et/ou d'ajouter des services additionnels d'identification en deux étapes qui sont mieux supportés par les navigateurs web (comme par exemple une application d'authentification)."
},
"twoStepOptions": {
"message": "Options d'identification à double facteurs"
"message": "Options d'authentification à feux facteurs"
},
"recoveryCodeDesc": {
"message": "Accès perdu à tous vos services d'authentification à double facteurs ? Utilisez votre code de récupération pour désactiver tous les services de double authentifications sur votre compte."
@@ -966,7 +966,7 @@
"message": "Paramètre de saisie automatique par défaut pour les identifiants"
},
"defaultAutoFillOnPageLoadDesc": {
"message": "Après avoir activé le remplissage automatique au chargement de la page, vous pourrez activer ou désactiver la fonctionnalité pour chaque identifiant. Ceci est le paramètre par défaut pour les identifiants qui ne sont pas configurés individuellement."
"message": "Vous pouvez désactiver la saisie automatique au chargement de la page pour les éléments de connexion individuels à partir de la vue Éditer l'élément."
},
"itemAutoFillOnPageLoad": {
"message": "Remplissage automatique au chargement de la page (si activé dans les options)"
@@ -1034,7 +1034,7 @@
"message": "Le fait de cliquer à l'extérieur de la fenêtre pop-up pour vérifier votre e-mail avec votre code de vérification fermera cette pop-up. Voulez-vous ouvrir cette pop-up dans une nouvelle fenêtre pour qu'elle ne soit pas fermée ?"
},
"popupU2fCloseMessage": {
"message": "Ce navigateur ne peut pas traiter les requêtes U2F dans cette popup. Voulez-vous ouvrir cette popup dans une nouvelle fenêtre pour que vous puissiez vous connecter en utilisant U2F ?"
"message": "Ce navigateur ne peut pas traiter les demandes U2F dans cette fenêtre popup. Voulez-vous ouvrir cette popup dans une nouvelle fenêtre afin de pouvoir vous connecter à l'aide de l'U2F ?"
},
"enableFavicon": {
"message": "Afficher les icônes du site web"
@@ -1043,10 +1043,10 @@
"message": "Afficher une image reconnaissable à côté de chaque identifiant."
},
"enableBadgeCounter": {
"message": "Show badge counter"
"message": "Afficher le compteur de badge"
},
"badgeCounterDesc": {
"message": "Indicate how many logins you have for the current web page."
"message": "Indique le nombre d'identifiants que vous avez pour la page web actuelle."
},
"cardholderName": {
"message": "Nom du titulaire de la carte"
@@ -1347,10 +1347,10 @@
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
},
"weakMasterPassword": {
"message": "Mot de passe maître faible"
"message": "Mot de passe principal faible"
},
"weakMasterPasswordDesc": {
"message": "Le mot de passe maître que vous avez choisi est faible. Vous devriez utiliser un mot de passe (ou une phrase de passe) fort(e) pour protéger correctement votre compte Bitwarden. Êtes-vous sûr de vouloir utiliser ce mot de passe maître ?"
"message": "Le mot de passe principal que vous avez choisi est faible. Vous devriez utiliser un mot de passe principal fort (ou une phrase de passe) pour protéger correctement votre compte Bitwarden. Êtes-vous sûr de vouloir utiliser ce mot de passe principal ?"
},
"pin": {
"message": "Code PIN",
@@ -1378,13 +1378,13 @@
"message": "Veuillez confirmer l'utilisation de la biométrie dans l'application Bitwarden de bureau pour activer la biométrie dans le navigateur."
},
"lockWithMasterPassOnRestart": {
"message": "Verrouiller avec le mot de passe maître lors du redémarrage du navigateur."
"message": "Verrouiller avec le mot de passe principal au redémarrage du navigateur"
},
"selectOneCollection": {
"message": "Vous devez sélectionner au moins une collection."
},
"cloneItem": {
"message": "Cloner lélément"
"message": "Cloner l'élément"
},
"clone": {
"message": "Cloner"
@@ -1393,7 +1393,7 @@
"message": "Une ou plusieurs politiques d'organisation affectent les paramètres de votre générateur."
},
"vaultTimeoutAction": {
"message": "Action lors de l'expiration du délai du coffre"
"message": "Action après délai d'expiration du coffre"
},
"lock": {
"message": "Verrouiller",
@@ -1413,7 +1413,7 @@
"message": "Êtes-vous sûr de vouloir supprimer définitivement cet élément ?"
},
"permanentlyDeletedItem": {
"message": "Élément supprimé définitivement"
"message": "Élément définitivement supprimé"
},
"restoreItem": {
"message": "Restaurer l'élément"
@@ -1425,7 +1425,7 @@
"message": "Élément restauré"
},
"vaultTimeoutLogOutConfirmation": {
"message": "La déconnexion supprimera tous les accès à votre coffre et nécessite une authentification en ligne après la période d'expiration. Êtes-vous sûr de vouloir utiliser ce paramètre?"
"message": "La déconnexion supprimera tout accès à votre coffre et nécessitera une authentification en ligne après la période d'expiration. Êtes-vous sûr de vouloir utiliser ce paramètre ?"
},
"vaultTimeoutLogOutConfirmationTitle": {
"message": "Confirmation de l'action lors de l'expiration du délai"
@@ -1434,16 +1434,16 @@
"message": "Remplir automatiquement et enregistrer"
},
"autoFillSuccessAndSavedUri": {
"message": "Élément rempli automatiquement et URI sauvegardée"
"message": "Élément saisi automatiquement et URI sauvegardé"
},
"autoFillSuccess": {
"message": "Élément rempli automatiquement"
"message": "Élément saisi automatiquement"
},
"setMasterPassword": {
"message": "Définir le mot de passe maître"
"message": "Définir le mot de passe principal"
},
"masterPasswordPolicyInEffect": {
"message": "Une ou plusieurs politiques de l'organisation exigent que votre mot de passe maître réponde aux exigences suivantes :"
"message": "Une ou plusieurs politiques de l'organisation exigent que votre mot de passe principal réponde aux exigences suivantes :"
},
"policyInEffectMinComplexity": {
"message": "Score de complexité minimum de $SCORE$",
@@ -1482,7 +1482,7 @@
}
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Votre nouveau mot de passe maître ne répond pas aux exigences de la politique."
"message": "Votre nouveau mot de passe principal ne répond pas aux exigences en matière de politique de sécurité."
},
"acceptPolicies": {
"message": "En cochant cette case, vous acceptez les éléments suivants :"
@@ -1563,7 +1563,7 @@
"message": "Cette action ne peut pas être effectuée dans la barre latérale, veuillez réessayer l'action dans la popup ou la nouvelle fenêtre."
},
"personalOwnershipSubmitError": {
"message": "En raison d'une politique d'entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Sélectionnez une organisation dans l'option Propriété et choisissez parmi les collections disponibles."
"message": "En raison d'une politique d'entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Changez l'option Propriété au profit d'une organisation et choisissez parmi les collections disponibles."
},
"personalOwnershipPolicyInEffect": {
"message": "Une politique d'organisation affecte vos options de propriété."
@@ -1724,7 +1724,7 @@
"message": "Le texte que vous voulez envoyer."
},
"sendHideText": {
"message": "Cacher par défaut le texte de ce Send.",
"message": "Masquer le texte de ce Send par défaut.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"currentAccessCount": {
@@ -1738,7 +1738,7 @@
"message": "Nouveau mot de passe"
},
"sendDisabled": {
"message": "Send désactivé",
"message": "Send supprimé",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDisabledWarning": {
@@ -1793,19 +1793,19 @@
"message": "Une erreur s'est produite lors de l'enregistrement de vos dates de suppression et d'expiration."
},
"hideEmail": {
"message": "Cacher mon adresse e-mail aux destinataires."
"message": "Masquer mon adresse électronique aux destinataires."
},
"sendOptionsPolicyInEffect": {
"message": "Une ou plusieurs politiques d'organisation affectent vos options Send."
},
"passwordPrompt": {
"message": "Ressaisie du mot de passe maître"
"message": "Ressaisir le mot de passe principal"
},
"passwordConfirmation": {
"message": "Confirmation du mot de passe maître"
"message": "Confirmation du mot de passe principal"
},
"passwordConfirmationDesc": {
"message": "Cette action est protégée. Pour continuer, veuillez ressaisir votre mot de passe maître pour vérifier votre identité."
"message": "Cette action est protégée. Pour continuer, veuillez saisir à nouveau votre mot de passe principal pour vérifier votre identité."
},
"emailVerificationRequired": {
"message": "Vérification de l'adresse e-mail nécessaire"
@@ -1814,25 +1814,25 @@
"message": "Vous devez vérifier votre adresse e-mail pour utiliser cette fonctionnalité. Vous pouvez vérifier votre adresse e-mail dans le coffre web."
},
"updatedMasterPassword": {
"message": "Mot de passe maître mis à jour"
"message": "Mot de passe principal mis à jour"
},
"updateMasterPassword": {
"message": "Mettre à jour le mot de passe maître"
"message": "Mettre à jour le mot de passe principal"
},
"updateMasterPasswordWarning": {
"message": "Votre mot de passe maître a récemment été modifié par un administrateur de votre organisation. Pour pouvoir accéder au coffre-fort, vous devez mettre à jour votre mot de passe maître maintenant. Poursuivre vous déconnectera de votre session actuelle, vous obligeant à vous reconnecter. Les sessions actives sur d'autres appareils peuvent rester actives jusqu'à une heure."
"message": "Votre mot de passe principal a été récemment changé par un administrateur de votre organisation. Pour pouvoir accéder au coffre, vous devez le mettre à jour maintenant. En poursuivant, vous serez déconnecté de votre session actuelle et vous devrez vous reconnecter. Les sessions actives sur d'autres appareils peuvent rester actives pendant encore une heure."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Inscription automatique"
},
"resetPasswordAutoEnrollInviteWarning": {
"message": "Cette organisation a une politique d'entreprise qui vous inscrira automatiquement à la réinitialisation du mot de passe. L'inscription permettra aux administrateurs de l'organisation de changer votre mot de passe maître."
"message": "Cette organisation dispose d'une politique d'entreprise qui vous inscrira automatiquement à la réinitialisation du mot de passe. L'inscription permettra aux administrateurs de l'organisation de changer votre mot de passe principal."
},
"selectFolder": {
"message": "Sélectionnez un dossier..."
},
"ssoCompleteRegistration": {
"message": "Afin de terminer la connexion avec SSO, veuillez définir un mot de passe maître pour accéder à votre coffre et le protéger."
"message": "Afin de finaliser la connexion avec SSO, veuillez définir un mot de passe principal pour accéder et protéger votre coffre."
},
"hours": {
"message": "Heures"
@@ -1869,7 +1869,7 @@
"message": "Aucun identifiant unique trouvé."
},
"convertOrganizationEncryptionDesc": {
"message": "$ORGANIZATION$ utilise SSO avec un serveur de clés auto-hébergé. Un mot de passe maître n'est plus nécessaire aux membres de cette organisation pour se connecter.",
"message": "$ORGANIZATION$ utilise le SSO avec un serveur de clés auto-hébergé. Un mot de passe principal n'est plus nécessaire aux membres de cette organisation pour se connecter.",
"placeholders": {
"organization": {
"content": "$1",
@@ -1881,10 +1881,10 @@
"message": "Quitter l'organisation"
},
"removeMasterPassword": {
"message": "Supprimer le mot de passe maître"
"message": "Supprimer le mot de passe principal"
},
"removedMasterPassword": {
"message": "Mot de passe maître supprimé."
"message": "Mot de passe principal supprimé"
},
"leaveOrganizationConfirmation": {
"message": "Êtes-vous sûr·e de vouloir quitter cette organisation ?"
@@ -1902,7 +1902,7 @@
"message": "Export du coffre personnel"
},
"exportingPersonalVaultDescription": {
"message": "Seuls les éléments du coffre personnel associé à l'adresse e-mail $EMAIL$ seront exportés. Les éléments du coffre de l'organisation ne seront pas inclus.",
"message": "Seuls les éléments individuels du coffre associés à $EMAIL$ seront exportés. Les éléments du coffre de l'organisation ne seront pas inclus.",
"placeholders": {
"email": {
"content": "$1",
@@ -1939,10 +1939,10 @@
"message": "Aléatoire"
},
"randomWord": {
"message": "Mots aléatoire"
"message": "Mot aléatoire"
},
"websiteName": {
"message": "Nom du site Web"
"message": "Nom du site web"
},
"whatWouldYouLikeToGenerate": {
"message": "Que souhaitez-vous générer ?"
@@ -1970,16 +1970,16 @@
"message": "Clé d'API"
},
"ssoKeyConnectorError": {
"message": "Erreur du connecteur de clé: veuillez vérifier que le connecteur de clé est disponible et qu'il fonctionne correctement."
"message": "Erreur Key Connector : vérifiez que Key Connector est disponible et fonctionne correctement."
},
"premiumSubcriptionRequired": {
"message": "Un abonnement Premium est requis"
"message": "Abonnement Premium requis"
},
"organizationIsDisabled": {
"message": "L'organisation est désactivée."
},
"disabledOrganizationFilterError": {
"message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide."
"message": "Les éléments des organisations suspendues ne sont pas accessibles. Contactez le propriétaire de votre organisation pour obtenir de l'aide."
},
"cardBrandMir": {
"message": "Mir"
@@ -2000,19 +2000,19 @@
"message": "Cliquez ici"
},
"environmentEditedReset": {
"message": "to reset to pre-configured settings"
"message": "pour réinitialiser aux paramètres par défaut"
},
"serverVersion": {
"message": "Server version"
"message": "Version du serveur"
},
"selfHosted": {
"message": "Auto-hébergé"
},
"thirdParty": {
"message": "Third-party"
"message": "Tierce partie"
},
"thirdPartyServerMessage": {
"message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.",
"message": "Connecté à l'implémentation du serveur tiers, $SERVERNAME$. Veuillez contrôler les bugs en utilisant le serveur officiel, ou rapportez-les au serveur tiers.",
"placeholders": {
"servername": {
"content": "$1",
@@ -2021,7 +2021,7 @@
}
},
"lastSeenOn": {
"message": "last seen on: $DATE$",
"message": "vu pour la dernière fois le : $DATE$",
"placeholders": {
"date": {
"content": "$1",
@@ -2030,18 +2030,18 @@
}
},
"loginWithMasterPassword": {
"message": "Log in with master password"
"message": "Se connecter avec le mot de passe principal"
},
"loggingInAs": {
"message": "Logging in as"
"message": "Connexion en tant que"
},
"notYou": {
"message": "Ce n'est pas vous ?"
},
"newAroundHere": {
"message": "Êtes-vous nouveau ici ?"
"message": "Nouveau par ici ?"
},
"rememberEmail": {
"message": "Remember email"
"message": "Se souvenir du courriel"
}
}

View File

@@ -104,7 +104,7 @@
"message": "Unlock your vault"
},
"loginToVaultMenu": {
"message": "Log in to your vault"
"message": "अपने अकाउंट में लॉगिन करें"
},
"autoFillInfo": {
"message": "इस ब्राउज़र टैब के लिए स्वत: भरण लॉगिन उपलब्ध नहीं है।"
@@ -131,10 +131,10 @@
"message": "Send a verification code to your email"
},
"sendCode": {
"message": "Send code"
"message": "कोड भेजें"
},
"codeSent": {
"message": "Code sent"
"message": "कोड भेजा गया है"
},
"verificationCode": {
"message": "Verification Code"
@@ -339,7 +339,7 @@
"message": "आपका वेब ब्राउज़र आसान क्लिपबोर्ड कॉपीिंग का समर्थन नहीं करता है। इसके बजाय इसे मैन्युअल रूप से कॉपी करें।"
},
"verifyIdentity": {
"message": "Verify identity"
"message": "पहचान सत्यापित करें"
},
"yourVaultIsLocked": {
"message": "आपकी वॉल्ट लॉक हो गई है। जारी रखने के लिए अपने मास्टर पासवर्ड को सत्यापित करें।"
@@ -1133,10 +1133,10 @@
"message": "Last Name"
},
"fullName": {
"message": "Full name"
"message": "पूरा नाम"
},
"identityName": {
"message": "Identity Name"
"message": "पहचान का नाम"
},
"company": {
"message": "कंपनी"
@@ -1887,7 +1887,7 @@
"message": "Master password removed"
},
"leaveOrganizationConfirmation": {
"message": "Are you sure you want to leave this organization?"
"message": "क्या आप सुनिश्चित हैं कि आप इस संगठन को छोड़ना चाहते हैं?"
},
"leftOrganization": {
"message": "You have left the organization."
@@ -1911,13 +1911,13 @@
}
},
"error": {
"message": "Error"
"message": "एरर"
},
"regenerateUsername": {
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate username"
"message": "उपयोगकर्ता नाम बनाएँ"
},
"usernameType": {
"message": "Username type"
@@ -1942,7 +1942,7 @@
"message": "Random word"
},
"websiteName": {
"message": "Website name"
"message": "वेबसाइट का नाम"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
@@ -1997,7 +1997,7 @@
"message": "Settings have been edited"
},
"environmentEditedClick": {
"message": "Click here"
"message": "यहाँ क्लिक करें"
},
"environmentEditedReset": {
"message": "to reset to pre-configured settings"
@@ -2042,6 +2042,6 @@
"message": "New around here?"
},
"rememberEmail": {
"message": "Remember email"
"message": "ईमेल याद रखें"
}
}

View File

@@ -2039,7 +2039,7 @@
"message": "Nisi ti?"
},
"newAroundHere": {
"message": "New around here?"
"message": "Novi korisnik?"
},
"rememberEmail": {
"message": "Zapamti adresu e-pošte"

View File

@@ -47,19 +47,19 @@
"message": "Ketik ulang Kata Sandi Utama"
},
"masterPassHint": {
"message": "Petunjuk Kata Sandi Utama (pilihan)"
"message": "Petunjuk Kata Sandi Utama (opsional)"
},
"tab": {
"message": "Tab"
},
"vault": {
"message": "Vault"
"message": "Brankas"
},
"myVault": {
"message": "Brankas Saya"
},
"allVaults": {
"message": "All vaults"
"message": "Semua brankas"
},
"tools": {
"message": "Alat"
@@ -134,7 +134,7 @@
"message": "Kirim Kode"
},
"codeSent": {
"message": "Kode Terkirim!"
"message": "Kode sudah dikirim"
},
"verificationCode": {
"message": "Kode Verifikasi"
@@ -424,13 +424,13 @@
"message": "Alamat email tidak valid."
},
"masterPasswordRequired": {
"message": "Master password is required."
"message": "Kata sandi utama diperlukan."
},
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
},
"masterPasswordMinlength": {
"message": "Master password must be at least 8 characters long."
"message": "Kata sandi utama harus memiliki panjang setidaknya 8 karakter."
},
"masterPassDoesntMatch": {
"message": "Konfirmasi sandi utama tidak cocok."
@@ -810,7 +810,7 @@
"message": "Penyegaran selesai"
},
"enableAutoTotpCopy": {
"message": "Copy TOTP automatically"
"message": "Salin TOTP secara otomatis"
},
"disableAutoTotpCopyDesc": {
"message": "Jika info masuk Anda memiliki kunci autentikasi yang menyertainya, kode verifikasi TOTP akan disalin secara otomatis ke clipboard Anda setiap kali Anda mengisi info masuk secara otomatis."
@@ -1312,7 +1312,7 @@
"description": "ex. Date this item was updated"
},
"dateCreated": {
"message": "Created",
"message": "Dibuat",
"description": "ex. Date this item was created"
},
"datePasswordUpdated": {
@@ -1488,7 +1488,7 @@
"message": "Dengan mencentang kotak ini, Anda menyetujui yang berikut:"
},
"acceptPoliciesRequired": {
"message": "Terms of Service and Privacy Policy have not been acknowledged."
"message": "Persyaratan Layanan dan Kebijakan Privasi belum disetujui."
},
"termsOfService": {
"message": "Persyaratan Layanan"
@@ -1911,16 +1911,16 @@
}
},
"error": {
"message": "Error"
"message": "Galat"
},
"regenerateUsername": {
"message": "Regenerate username"
"message": "Buat nama pengguna baru"
},
"generateUsername": {
"message": "Generate username"
"message": "Buat nama pengguna baru"
},
"usernameType": {
"message": "Username type"
"message": "Jenis nama pengguna"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
@@ -1936,19 +1936,19 @@
"message": "Use your domain's configured catch-all inbox."
},
"random": {
"message": "Random"
"message": "Acak"
},
"randomWord": {
"message": "Random word"
"message": "Kata acak"
},
"websiteName": {
"message": "Website name"
"message": "Nama situs web"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
"message": "Apa yang ingin Anda buat?"
},
"passwordType": {
"message": "Password type"
"message": "Jenis kata sandi"
},
"service": {
"message": "Service"
@@ -2042,6 +2042,6 @@
"message": "New around here?"
},
"rememberEmail": {
"message": "Remember email"
"message": "Ingat email"
}
}

View File

@@ -403,7 +403,7 @@
"message": "4 ore"
},
"onLocked": {
"message": "Al Blocco Computer"
"message": "Al blocco del computer"
},
"onRestart": {
"message": "Al riavvio del browser"

File diff suppressed because it is too large Load Diff

View File

@@ -193,7 +193,7 @@
"message": "Er zijn geen mappen om weer te geven."
},
"helpFeedback": {
"message": "Hulp en reacties"
"message": "Help & reacties"
},
"sync": {
"message": "Synchroniseren"

View File

@@ -98,7 +98,7 @@
"message": "Kopiuj nazwę pola niestandardowego"
},
"noMatchingLogins": {
"message": "Brak pasujących danych logowania."
"message": "Brak pasujących danych logowania"
},
"unlockVaultMenu": {
"message": "Odblokuj sejf"
@@ -936,7 +936,7 @@
"message": "Adres URL serwera"
},
"apiUrl": {
"message": "Adres URL serwera interfejsu API"
"message": "Adres URL serwera API"
},
"webVaultUrl": {
"message": "Adres URL serwera sejfu internetowego"
@@ -1040,7 +1040,7 @@
"message": "Pokaż ikony witryn"
},
"faviconDesc": {
"message": "Pokaż rozpoznawalny obraz obok każdych danych logowania."
"message": "Pokaż rozpoznawalny obraz obok danych logowania."
},
"enableBadgeCounter": {
"message": "Pokaż licznik na ikonie"
@@ -1316,7 +1316,7 @@
"description": "ex. Date this item was created"
},
"datePasswordUpdated": {
"message": "Aktualizacja hasła",
"message": "Hasło zostało zaktualizowane",
"description": "ex. Date this password was updated"
},
"neverLockWarning": {
@@ -1488,7 +1488,7 @@
"message": "Zaznaczając tę opcję, akceptujesz:"
},
"acceptPoliciesRequired": {
"message": "Warunki użytkowania i polityka prywatności nie zostały zaakceptowane."
"message": "Regulamin i polityka prywatności nie zostały zaakceptowane."
},
"termsOfService": {
"message": "Regulamin"
@@ -1820,7 +1820,7 @@
"message": "Zaktualizuj hasło główne"
},
"updateMasterPasswordWarning": {
"message": "Twoje hasło główne zostało ostatnio zmienione przez administratora Twojej organizacji. Musisz je teraz zaktualizować, aby uzyskać dostęp do sejfu. W przypadku kontynuacji nastąpi wylogowanie z bieżącej sesji, przez co konieczne będzie ponowne zalogowanie się. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie jedną godzinę."
"message": "Hasło główne zostało zmienione przez administratora Twojej organizacji. Musisz je zaktualizować, aby uzyskać dostęp do sejfu. Ta czynność spowoduje wylogowanie z bieżącej sesji, przez co konieczne będzie ponowne zalogowanie się. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatyczne rejestrowanie użytkowników"
@@ -1857,10 +1857,10 @@
"message": "Czas blokowania sejfu przekracza limit określony przez organizację."
},
"vaultExportDisabled": {
"message": "Eksport sejfu wyłączony"
"message": "Eksportowanie sejfu jest niedostępne"
},
"personalVaultExportPolicyInEffect": {
"message": "Co najmniej jedna zasada organizacji uniemożliwia wyeksportowanie Twojego sejfu."
"message": "Co najmniej jedna zasada organizacji uniemożliwia wyeksportowanie osobistego sejfu."
},
"copyCustomFieldNameInvalidElement": {
"message": "Nie można zidentyfikować poprawnego elementu formularza. Spróbuj sprawdzić kod HTML."
@@ -1942,7 +1942,7 @@
"message": "Losowe słowo"
},
"websiteName": {
"message": "Nazwa witryny"
"message": "Nazwa strony"
},
"whatWouldYouLikeToGenerate": {
"message": "Co chcesz wygenerować?"
@@ -1954,10 +1954,10 @@
"message": "Usługa"
},
"forwardedEmail": {
"message": "Alias przekazywanego e-maila"
"message": "Alias przekierowania"
},
"forwardedEmailDesc": {
"message": "Wygeneruj alias adresu e-mail z zewnętrznej usługi przekazywania."
"message": "Wygeneruj alias adresu e-mail z zewnętrznej usługi przekierowania."
},
"hostname": {
"message": "Nazwa hosta",
@@ -1967,7 +1967,7 @@
"message": "Token dostępu API"
},
"apiKey": {
"message": "Klucz interfejsu API"
"message": "Klucz API"
},
"ssoKeyConnectorError": {
"message": "Błąd serwera Key Connector: upewnij się, że serwer Key Connector jest dostępny i działa poprawnie."
@@ -1976,10 +1976,10 @@
"message": "Wymagana jest subskrypcja Premium"
},
"organizationIsDisabled": {
"message": "Organizacja jest wyłączona."
"message": "Organizacja została zawieszona."
},
"disabledOrganizationFilterError": {
"message": "Nie można uzyskać dostępu do elementów w wyłączonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc."
"message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc."
},
"cardBrandMir": {
"message": "Mir"
@@ -1994,7 +1994,7 @@
}
},
"settingsEdited": {
"message": "Ustawienia zostały edytowane"
"message": "Ustawienia zostały zmienione"
},
"environmentEditedClick": {
"message": "Kliknij tutaj"
@@ -2009,7 +2009,7 @@
"message": "Samodzielnie hostowany"
},
"thirdParty": {
"message": "Innego dostawcy"
"message": "Inny dostawca"
},
"thirdPartyServerMessage": {
"message": "Połączono z implementacją serwera innego dostawcy, $SERVERNAME$. Zweryfikuj błędy za pomocą oficjalnego serwera lub zgłoś je serwerowi innego dostawcy.",
@@ -2021,7 +2021,7 @@
}
},
"lastSeenOn": {
"message": "ostatnio widziano $DATE$",
"message": "ostatnio widziany $DATE$",
"placeholders": {
"date": {
"content": "$1",
@@ -2039,9 +2039,9 @@
"message": "To nie Ty?"
},
"newAroundHere": {
"message": "Jesteś tu nowy(a)?"
"message": "Nowy użytkownik?"
},
"rememberEmail": {
"message": "Zapamiętaj email"
"message": "Zapamiętaj adres e-mail"
}
}

View File

@@ -642,7 +642,7 @@
"description": "Light color"
},
"solarizedDark": {
"message": "Солнечная темная",
"message": "Solarized dark",
"description": "'Solarized' is a noun and the name of a color scheme. It should not be translated."
},
"exportVault": {
@@ -921,7 +921,7 @@
"message": "Коды подтверждения будут отправлены вам по электронной почте."
},
"selfHostedEnvironment": {
"message": "Окружение собственного хостинга"
"message": "Окружение пользовательского хостинга"
},
"selfHostedEnvironmentFooter": {
"message": "Укажите URL Bitwarden на вашем сервере."
@@ -1396,7 +1396,7 @@
"message": "Действие по тайм-ауту хранилища"
},
"lock": {
"message": "Заблокировать",
"message": "Блокировка",
"description": "Verb form: to make secure or inaccesible by"
},
"trash": {

View File

@@ -642,7 +642,7 @@
"description": "Light color"
},
"solarizedDark": {
"message": "Tmavá Solarized",
"message": "Solarized tmavý",
"description": "'Solarized' is a noun and the name of a color scheme. It should not be translated."
},
"exportVault": {

View File

@@ -53,13 +53,13 @@
"message": "Zavihek"
},
"vault": {
"message": "Sef"
"message": "Trezor"
},
"myVault": {
"message": "Moj trezor"
},
"allVaults": {
"message": "All vaults"
"message": "Vsi trezorji"
},
"tools": {
"message": "Orodja"
@@ -101,7 +101,7 @@
"message": "Nobenih ujemajočih prijav."
},
"unlockVaultMenu": {
"message": "Unlock your vault"
"message": "Odkleni svoj trezor"
},
"loginToVaultMenu": {
"message": "Log in to your vault"
@@ -131,10 +131,10 @@
"message": "Send a verification code to your email"
},
"sendCode": {
"message": "Send code"
"message": "Pošlji kodo"
},
"codeSent": {
"message": "Code sent"
"message": "Koda poslana"
},
"verificationCode": {
"message": "Verifikacijska koda"
@@ -242,10 +242,10 @@
"message": "Lowercase (a-z)"
},
"numbers": {
"message": "Numbers (0-9)"
"message": "Številke (0-9)"
},
"specialCharacters": {
"message": "Special characters (!@#$%^&*)"
"message": "Posebni znaki (!@#$%^&*)"
},
"numWords": {
"message": "Število besed"
@@ -864,7 +864,7 @@
"message": "To start the WebAuthn 2FA verification. Click the button below to open a new tab and follow the instructions provided in the new tab."
},
"webAuthnNewTabOpen": {
"message": "Open new tab"
"message": "Odpri nov zavihek"
},
"webAuthnAuthenticate": {
"message": "Authenticate WebAuthn"
@@ -879,10 +879,10 @@
"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": "Možnosti dvostopenjske prijave"
},
"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."
"message": "Ste izgubili dostop do vseh vaših ponudnikov dvostopenjske prijave? Uporabite svojo kodo za obnovitev in tako onemogočite dvostopenjsko prijavo v svoj račun."
},
"recoveryCodeTitle": {
"message": "Koda za obnovitev"
@@ -927,7 +927,7 @@
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
},
"customEnvironment": {
"message": "Custom environment"
"message": "Okolje po meri"
},
"customEnvironmentFooter": {
"message": "For advanced users. You can specify the base URL of each service independently."
@@ -993,7 +993,7 @@
"message": "Generate and copy a new random password to the clipboard"
},
"commandLockVaultDesc": {
"message": "Lock the vault"
"message": "Zakleni trezor"
},
"privateModeWarning": {
"message": "Private mode support is experimental and some features are limited."
@@ -1002,10 +1002,10 @@
"message": "Custom fields"
},
"copyValue": {
"message": "Copy value"
"message": "Kopiraj vrednost"
},
"value": {
"message": "Value"
"message": "Vrednost"
},
"newCustomField": {
"message": "New custom field"
@@ -1020,7 +1020,7 @@
"message": "Skrito"
},
"cfTypeBoolean": {
"message": "Boolean"
"message": "Logična vrednost"
},
"cfTypeLinked": {
"message": "Linked",
@@ -1133,7 +1133,7 @@
"message": "Priimek"
},
"fullName": {
"message": "Full name"
"message": "Polno ime"
},
"identityName": {
"message": "Ime identitete"
@@ -1190,22 +1190,22 @@
"message": "Prijave"
},
"typeSecureNote": {
"message": "Secure note"
"message": "Varni zapisek"
},
"typeCard": {
"message": "Card"
"message": "Kartica"
},
"typeIdentity": {
"message": "Identity"
"message": "Identiteta"
},
"passwordHistory": {
"message": "Password history"
"message": "Zgodovina gesel"
},
"back": {
"message": "Back"
"message": "Nazaj"
},
"collections": {
"message": "Collections"
"message": "Zbirke"
},
"favorites": {
"message": "Priljubljeno"
@@ -1226,7 +1226,7 @@
"message": "Prijave"
},
"secureNotes": {
"message": "Secure notes"
"message": "Varni zapiski"
},
"clear": {
"message": "Počisti",
@@ -1285,15 +1285,15 @@
"description": "Toggle the display of the URIs of the currently open tabs in the browser."
},
"currentUri": {
"message": "Current URI",
"message": "Trenutni URI",
"description": "The URI of one of the current open tabs in the browser."
},
"organization": {
"message": "Organization",
"message": "Organizacija",
"description": "An entity of multiple related people (ex. a team or business organization)."
},
"types": {
"message": "Types"
"message": "Tipi"
},
"allItems": {
"message": "All items"
@@ -1302,13 +1302,13 @@
"message": "There are no passwords to list."
},
"remove": {
"message": "Remove"
"message": "Odstrani"
},
"default": {
"message": "Default"
"message": "Privzeto"
},
"dateUpdated": {
"message": "Updated",
"message": "Posodobljeno",
"description": "ex. Date this item was updated"
},
"dateCreated": {
@@ -1329,25 +1329,25 @@
"message": "There are no collections to list."
},
"ownership": {
"message": "Ownership"
"message": "Lastništvo"
},
"whoOwnsThisItem": {
"message": "Who owns this item?"
},
"strong": {
"message": "Strong",
"message": "Močno",
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
},
"good": {
"message": "Good",
"message": "Dobro",
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
},
"weak": {
"message": "Weak",
"message": "Šibko",
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
},
"weakMasterPassword": {
"message": "Weak master password"
"message": "Šibko glavno geslo"
},
"weakMasterPasswordDesc": {
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
@@ -1357,19 +1357,19 @@
"description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device."
},
"unlockWithPin": {
"message": "Unlock with PIN"
"message": "Odkleni s PIN kodo"
},
"setYourPinCode": {
"message": "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application."
},
"pinRequired": {
"message": "PIN code is required."
"message": "Potrebna je PIN koda."
},
"invalidPin": {
"message": "Invalid PIN code."
"message": "Nepravilna PIN koda."
},
"unlockWithBiometrics": {
"message": "Unlock with biometrics"
"message": "Prijava z biometriko"
},
"awaitDesktop": {
"message": "Awaiting confirmation from desktop"
@@ -1387,7 +1387,7 @@
"message": "Clone item"
},
"clone": {
"message": "Clone"
"message": "Kloniraj"
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
@@ -1396,15 +1396,15 @@
"message": "Vault timeout action"
},
"lock": {
"message": "Lock",
"message": "Zakleni",
"description": "Verb form: to make secure or inaccesible by"
},
"trash": {
"message": "Trash",
"message": "Koš",
"description": "Noun: a special folder to hold deleted items"
},
"searchTrash": {
"message": "Search trash"
"message": "Preišči koš"
},
"permanentlyDeleteItem": {
"message": "Permanently delete item"
@@ -1440,7 +1440,7 @@
"message": "Item auto-filled "
},
"setMasterPassword": {
"message": "Set master password"
"message": "Nastavi glavno geslo"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
@@ -1494,13 +1494,13 @@
"message": "Terms of Service"
},
"privacyPolicy": {
"message": "Privacy Policy"
"message": "Pravilnik o zasebnosti"
},
"hintEqualsPassword": {
"message": "Your password hint cannot be the same as your password."
},
"ok": {
"message": "Ok"
"message": "V redu"
},
"desktopSyncVerificationTitle": {
"message": "Desktop sync verification"
@@ -1584,7 +1584,7 @@
}
},
"send": {
"message": "Send",
"message": "Pošlji",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"searchSends": {
@@ -1596,10 +1596,10 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
"message": "Text"
"message": "Besedilo"
},
"sendTypeFile": {
"message": "File"
"message": "Datoteka"
},
"allSends": {
"message": "All Sends",
@@ -1610,7 +1610,7 @@
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
},
"expired": {
"message": "Expired"
"message": "Poteklo"
},
"pendingDeletion": {
"message": "Pending deletion"
@@ -1626,7 +1626,7 @@
"message": "Remove Password"
},
"delete": {
"message": "Delete"
"message": "Izbriši"
},
"removedPassword": {
"message": "Password removed"
@@ -1636,11 +1636,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLink": {
"message": "Send link",
"message": "Pošlji povezavo",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disabled": {
"message": "Disabled"
"message": "Onemogočeno"
},
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
@@ -1683,10 +1683,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 dan"
},
"days": {
"message": "$DAYS$ days",
"message": "$DAYS$ dni",
"placeholders": {
"days": {
"content": "$1",
@@ -1835,10 +1835,10 @@
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"hours": {
"message": "Hours"
"message": "Ur"
},
"minutes": {
"message": "Minutes"
"message": "Minut"
},
"vaultTimeoutPolicyInEffect": {
"message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)",
@@ -1911,7 +1911,7 @@
}
},
"error": {
"message": "Error"
"message": "Napaka"
},
"regenerateUsername": {
"message": "Regenerate username"
@@ -1936,13 +1936,13 @@
"message": "Use your domain's configured catch-all inbox."
},
"random": {
"message": "Random"
"message": "Naključno"
},
"randomWord": {
"message": "Random word"
"message": "Naključna beseda"
},
"websiteName": {
"message": "Website name"
"message": "Ime spletne strani"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
@@ -1997,13 +1997,13 @@
"message": "Settings have been edited"
},
"environmentEditedClick": {
"message": "Click here"
"message": "Kliknite tukaj"
},
"environmentEditedReset": {
"message": "to reset to pre-configured settings"
},
"serverVersion": {
"message": "Server version"
"message": "Verzija strežnika"
},
"selfHosted": {
"message": "Self-hosted"
@@ -2036,12 +2036,12 @@
"message": "Logging in as"
},
"notYou": {
"message": "Not you?"
"message": "Niste vi?"
},
"newAroundHere": {
"message": "New around here?"
},
"rememberEmail": {
"message": "Remember email"
"message": "Zapomni si e-pošto"
}
}

View File

@@ -2012,7 +2012,7 @@
"message": "Трећа страна"
},
"thirdPartyServerMessage": {
"message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.",
"message": "Повезан са имплементацијом сервера треће стране, $SERVERNAME$. Проверите грешке користећи званични сервер или их пријавите серверу треће стране.",
"placeholders": {
"servername": {
"content": "$1",

View File

@@ -131,7 +131,7 @@
"message": "Skicka en verifieringskod till din e-postadress"
},
"sendCode": {
"message": "Send-kod"
"message": "Skicka kod"
},
"codeSent": {
"message": "Kod har skickats"
@@ -318,7 +318,7 @@
"message": "Öppna"
},
"website": {
"message": "Webbsida"
"message": "Webbplats"
},
"toggleVisibility": {
"message": "Växla synlighet"
@@ -436,7 +436,7 @@
"message": "Bekräftelsen för huvudlösenordet stämde ej."
},
"newAccountCreated": {
"message": "Ditt nya konto har blivit skapat! Du kan nu logga in."
"message": "Ditt nya konto har skapats! Du kan logga in nu."
},
"masterPassSent": {
"message": "Vi har skickat ett e-postmeddelande till dig med din huvudlösenordsledtråd."
@@ -448,7 +448,7 @@
"message": "Ogiltig verifieringskod"
},
"valueCopied": {
"message": "$VALUE$ kopierat",
"message": "$VALUE$ har kopierats",
"description": "Value has been copied to the clipboard.",
"placeholders": {
"value": {
@@ -461,10 +461,10 @@
"message": "Kunde inte automatiskt fylla i det valda objektet på den här webbsidan. Klipp/klistra informationen istället."
},
"loggedOut": {
"message": "Loggade ut"
"message": "Utloggad"
},
"loginExpired": {
"message": "Din inloggningssession har utgått."
"message": "Din inloggningssession har upphört."
},
"logOutConfirmation": {
"message": "Är du säker på att du vill logga ut?"
@@ -497,7 +497,7 @@
"message": "Mapp sparad"
},
"deleteFolderConfirmation": {
"message": "Är du säker på att du vill ta bort den här mappen?"
"message": "Är du säker på att du vill radera denna mapp?"
},
"deletedFolder": {
"message": "Mapp raderad"
@@ -717,10 +717,10 @@
"message": "Bifogade filer"
},
"deleteAttachment": {
"message": "Ta bort bilaga"
"message": "Radera bilaga"
},
"deleteAttachmentConfirmation": {
"message": "Är du säker på att du vill ta bort bilagan?"
"message": "Är du säker på att du vill radera denna bilaga?"
},
"deletedAttachment": {
"message": "Raderade bilaga"
@@ -1400,7 +1400,7 @@
"description": "Verb form: to make secure or inaccesible by"
},
"trash": {
"message": "Papperskorgen",
"message": "Papperskorg",
"description": "Noun: a special folder to hold deleted items"
},
"searchTrash": {
@@ -1731,7 +1731,7 @@
"message": "Nuvarande antal åtkomster"
},
"createSend": {
"message": "Skapa ny Send",
"message": "Ny Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"newPassword": {

File diff suppressed because it is too large Load Diff

View File

@@ -53,13 +53,13 @@
"message": "แท็บ"
},
"vault": {
"message": "Vault"
"message": "ตู้นิรภัย"
},
"myVault": {
"message": "My Vault"
},
"allVaults": {
"message": "All vaults"
"message": "ตู้นิรภัยทั้งหมด"
},
"tools": {
"message": "เครื่องมือ"
@@ -95,16 +95,16 @@
"message": "Generate Password (copied)"
},
"copyElementIdentifier": {
"message": "Copy custom field name"
"message": "คัดลอกชื่อของช่องที่กำหนดเอง"
},
"noMatchingLogins": {
"message": "ไม่พบข้อมูลล็อกอินที่ตรงกัน"
},
"unlockVaultMenu": {
"message": "Unlock your vault"
"message": "ปลดล็อกกตู้นิรภัยของคุณ"
},
"loginToVaultMenu": {
"message": "Log in to your vault"
"message": "ลงชื่อเข้าใช้ตู้นิรภัยของคุณ"
},
"autoFillInfo": {
"message": "ไม่พบข้อมูลล็อกอินเพื่อใช้กรอกข้อมูลอัตโนมัติ สำหรับแท็บปัจจุบันของเบราว์เซอร์"
@@ -153,7 +153,7 @@
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"yourAccountsFingerprint": {
"message": "Your account's fingerprint phrase",
"message": "ข้อความลายนิ้วมือของบัญชีของคุณ",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"twoStepLogin": {
@@ -212,7 +212,7 @@
"description": "Short for 'Password Generator'."
},
"passGenInfo": {
"message": "Automatically generate strong, unique passwords for your logins."
"message": "สร้างรหัสผ่านที่รัดกุมและไม่ซ้ำใครโดยอัตโนมัติสำหรับการเข้าสู่ระบบของคุณ"
},
"bitWebVault": {
"message": "bitwarden Web Vault"
@@ -291,7 +291,7 @@
"message": "รหัสผ่าน"
},
"passphrase": {
"message": "Passphrase"
"message": "ข้อความรหัสผ่าน"
},
"favorite": {
"message": "รายการโปรด"
@@ -333,10 +333,10 @@
"message": "Rate the Extension"
},
"rateExtensionDesc": {
"message": "Please consider helping us out with a good review!"
"message": "โปรดพิจารณา ช่วยเราด้วยการตรวจสอบที่ดี!"
},
"browserNotSupportClipboard": {
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead."
"message": "เว็บเบราว์เซอร์ของคุณไม่รองรับการคัดลอกคลิปบอร์ดอย่างง่าย คัดลอกด้วยตนเองแทน"
},
"verifyIdentity": {
"message": "ยืนยันตัวตน"
@@ -424,22 +424,22 @@
"message": "ที่อยู่อีเมลไม่ถูกต้อง"
},
"masterPasswordRequired": {
"message": "Master password is required."
"message": "ต้องใช้รหัสผ่านหลัก"
},
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
"message": "ต้องพิมพ์รหัสผ่านหลักอีกครั้ง"
},
"masterPasswordMinlength": {
"message": "Master password must be at least 8 characters long."
"message": "รหัสผ่านหลักต้องมีความยาวอย่างน้อย 8 ตัวอักษร"
},
"masterPassDoesntMatch": {
"message": "Master password confirmation does not match."
"message": "การยืนยันรหัสผ่านหลักไม่ตรงกัน"
},
"newAccountCreated": {
"message": "Your new account has been created! You may now log in."
"message": "บัญชีใหม่ของคุณถูกสร้างขึ้นแล้ว! ตอนนี้คุณสามารถเข้าสู่ระบบ"
},
"masterPassSent": {
"message": "We've sent you an email with your master password hint."
"message": "เราได้ส่งอีเมลพร้อมคำใบ้รหัสผ่านหลักของคุณออกไปแล้ว"
},
"verificationCodeRequired": {
"message": "ต้องระบุโค้ดยืนยัน"
@@ -497,7 +497,7 @@
"message": "Edited Folder"
},
"deleteFolderConfirmation": {
"message": "Are you sure you want to delete this folder?"
"message": "คุณแน่ใจหรือไม่ว่าต้องการลบโฟลเดอร์นี้"
},
"deletedFolder": {
"message": "ลบโฟลเดอร์แล้ว"
@@ -506,7 +506,7 @@
"message": "Getting Started Tutorial"
},
"gettingStartedTutorialVideo": {
"message": "Watch our getting started tutorial to learn how to get the most out of the browser extension."
"message": "ดูบทช่วยสอนการเริ่มต้นของเราเพื่อเรียนรู้วิธีใช้ประโยชน์สูงสุดจากส่วนขยายเบราว์เซอร์"
},
"syncingComplete": {
"message": "การซิงก์เสร็จสมบูรณ์"
@@ -552,48 +552,48 @@
"message": "คุณต้องการเขียนทับรหัสผ่านปัจจุบันใช่หรือไม่?"
},
"overwriteUsername": {
"message": "Overwrite username"
"message": "เขียนทับชื่อผู้ใช้"
},
"overwriteUsernameConfirmation": {
"message": "Are you sure you want to overwrite the current username?"
"message": "คุณแน่ใจหรือไม่ว่าต้องการเขียนทับชื่อผู้ใช้ปัจจุบัน"
},
"searchFolder": {
"message": "ค้นหาในโพลเดอร์"
},
"searchCollection": {
"message": "Search collection"
"message": "คอลเลกชันการค้นหา"
},
"searchType": {
"message": "Search type"
"message": "ประเภทการค้นหา"
},
"noneFolder": {
"message": "No Folder",
"description": "This is the folder for uncategorized items"
},
"enableAddLoginNotification": {
"message": "Ask to add login"
"message": "ถามเพื่อให้เพิ่มการเข้าสู่ระบบ"
},
"addLoginNotificationDesc": {
"message": "The \"Add Login Notification\" automatically prompts you to save new logins to your vault whenever you log into them for the first time."
},
"showCardsCurrentTab": {
"message": "Show cards on Tab page"
"message": "แสดงการ์ดบนหน้าแท็บ"
},
"showCardsCurrentTabDesc": {
"message": "List card items on the Tab page for easy auto-fill."
"message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย"
},
"showIdentitiesCurrentTab": {
"message": "Show identities on Tab page"
"message": "แสดงตัวตนบนหน้าแท็บ"
},
"showIdentitiesCurrentTabDesc": {
"message": "List identity items on the Tab page for easy auto-fill."
"message": "แสดงรายการข้อมูลประจำตัวในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย"
},
"clearClipboard": {
"message": "ล้างคลิปบอร์ด",
"description": "Clipboard is the operating system thing where you copy/paste data to on your device."
},
"clearClipboardDesc": {
"message": "Automatically clear copied values from your clipboard.",
"message": "ล้างค่าที่คัดลอกโดยอัตโนมัติจากคลิปบอร์ดของคุณ",
"description": "Clipboard is the operating system thing where you copy/paste data to on your device."
},
"notificationAddDesc": {
@@ -603,7 +603,7 @@
"message": "Yes, Save Now"
},
"enableChangedPasswordNotification": {
"message": "Ask to update existing login"
"message": "ขอให้ปรับปรุงการเข้าสู่ระบบที่มีอยู่"
},
"changedPasswordNotificationDesc": {
"message": "Ask to update a login's password when a change is detected on a website."
@@ -615,17 +615,17 @@
"message": "Yes, Update Now"
},
"enableContextMenuItem": {
"message": "Show context menu options"
"message": "แสดงตัวเลือกเมนูบริบท"
},
"contextMenuItemDesc": {
"message": "Use a secondary click to access password generation and matching logins for the website. "
"message": "ใช้การคลิกสำรองเพื่อเข้าถึงการสร้างรหัสผ่านและการเข้าสู่ระบบที่ตรงกันสำหรับเว็บไซต์ "
},
"defaultUriMatchDetection": {
"message": "Default URI match detection",
"message": "การตรวจจับการจับคู่ URI เริ่มต้น",
"description": "Default URI match detection for auto-fill."
},
"defaultUriMatchDetectionDesc": {
"message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill."
"message": "เลือกวิธีเริ่มต้นในการจัดการการตรวจหาการจับคู่ URI สำหรับการเข้าสู่ระบบเมื่อดำเนินการต่างๆ เช่น การป้อนอัตโนมัติ"
},
"theme": {
"message": "ธีม"
@@ -656,31 +656,31 @@
"description": "WARNING (should stay in capitalized letters if the language permits)"
},
"confirmVaultExport": {
"message": "Confirm vault export"
"message": "ยืนยันการส่งออกตู้นิรภัย"
},
"exportWarningDesc": {
"message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it."
},
"encExportKeyWarningDesc": {
"message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file."
"message": "การส่งออกนี้เข้ารหัสข้อมูลของคุณโดยใช้คีย์เข้ารหัสของบัญชีของคุณ หากคุณเคยหมุนเวียนคีย์เข้ารหัสของบัญชี คุณควรส่งออกอีกครั้ง เนื่องจากคุณจะไม่สามารถถอดรหัสไฟล์ส่งออกนี้ได้"
},
"encExportAccountWarningDesc": {
"message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account."
"message": "คีย์การเข้ารหัสบัญชีจะไม่ซ้ำกันสำหรับบัญชีผู้ใช้ Bitwarden แต่ละบัญชี ดังนั้นคุณจึงไม่สามารถนำเข้าการส่งออกที่เข้ารหัสไปยังบัญชีอื่นได้"
},
"exportMasterPassword": {
"message": "Enter your master password to export your vault data."
"message": "ป้อนรหัสผ่านหลักของคุณเพื่อส่งออกข้อมูลตู้นิรภัยของคุณ"
},
"shared": {
"message": "แชร์แล้ว"
},
"learnOrg": {
"message": "Learn about organizations"
"message": "เรียนรู้เกี่ยวกับองค์กร"
},
"learnOrgConfirmation": {
"message": "Bitwarden allows you to share your vault items with others by using an organization. Would you like to visit the bitwarden.com website to learn more?"
"message": "Bitwarden อนุญาตให้คุณแชร์รายการตู้นิรภัยของคุณกับผู้อื่นโดยใช้องค์กร คุณต้องการเยี่ยมชมเว็บไซต์ bitwarden.com เพื่อเรียนรู้เพิ่มเติมหรือไม่?"
},
"moveToOrganization": {
"message": "Move to organization"
"message": "ย้ายไปยังแบบองค์กร"
},
"share": {
"message": "แชร์"
@@ -699,10 +699,10 @@
}
},
"moveToOrgDesc": {
"message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved."
"message": "เลือกองค์กรที่คุณต้องการย้ายรายการนี้ไป การย้ายไปยังองค์กรจะโอนความเป็นเจ้าของรายการไปยังองค์กรนั้น คุณจะไม่ได้เป็นเจ้าของโดยตรงของรายการนี้อีกต่อไปเมื่อมีการย้ายแล้ว"
},
"learnMore": {
"message": "Learn more"
"message": "เรียนรู้เพิ่มเติม"
},
"authenticatorKeyTotp": {
"message": "Authenticator Key (TOTP)"
@@ -747,7 +747,7 @@
"message": "Feature Unavailable"
},
"updateKey": {
"message": "You cannot use this feature until you update your encryption key."
"message": "คุณไม่สามารถใช้คุณลักษณะนี้ได้จนกว่าคุณจะปรับปรุงคีย์การเข้ารหัสลับของคุณ"
},
"premiumMembership": {
"message": "Premium Membership"
@@ -756,28 +756,28 @@
"message": "Manage Membership"
},
"premiumManageAlert": {
"message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?"
"message": "คุณสามารถจัดการการเป็นสมาชิกของคุณได้ที่ bitwarden.com web vault คุณต้องการเข้าชมเว็บไซต์ตอนนี้หรือไม่?"
},
"premiumRefresh": {
"message": "Refresh Membership"
},
"premiumNotCurrentMember": {
"message": "You are not currently a Premium member."
"message": "คุณยังไม่ได้เป็นสมาชิกพรีเมียม"
},
"premiumSignUpAndGet": {
"message": "Sign up for a Premium membership and get:"
"message": "สมัครสมาชิกพรีเมี่ยมและรับ:"
},
"ppremiumSignUpStorage": {
"message": "1 GB of encrypted file storage."
},
"ppremiumSignUpTwoStep": {
"message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo."
"message": "ตัวเลือกการเข้าสู่ระบบแบบสองขั้นตอนเพิ่มเติม เช่น YubiKey, FIDO U2F และ Duo"
},
"ppremiumSignUpReports": {
"message": "Password hygiene, account health, and data breach reports to keep your vault safe."
"message": "สุขอนามัยของรหัสผ่าน ความสมบูรณ์ของบัญชี และรายงานการละเมิดข้อมูลเพื่อให้ตู้นิรภัยของคุณปลอดภัย"
},
"ppremiumSignUpTotp": {
"message": "TOTP verification code (2FA) generator for logins in your vault."
"message": "ตัวสร้างรหัสยืนยัน TOTP (2FA) สำหรับการเข้าสู่ระบบในตู้นิรภัยของคุณ"
},
"ppremiumSignUpSupport": {
"message": "Priority customer support."
@@ -1055,7 +1055,7 @@
"message": "หมายเลข"
},
"brand": {
"message": "Brand"
"message": "แบรนด์"
},
"expirationMonth": {
"message": "Expiration Month"
@@ -1133,7 +1133,7 @@
"message": "Last Name"
},
"fullName": {
"message": "Full name"
"message": "ชื่อเต็ม"
},
"identityName": {
"message": "Identity Name"
@@ -1181,7 +1181,7 @@
"message": "ประเทศ"
},
"type": {
"message": "Type"
"message": "ชนิด"
},
"typeLogin": {
"message": "ล็อกอิน"
@@ -1205,35 +1205,35 @@
"message": "ย้อนกลับ"
},
"collections": {
"message": "Collections"
"message": "คอลเลกชัน"
},
"favorites": {
"message": "รายการโปรด"
},
"popOutNewWindow": {
"message": "Pop out to a new window"
"message": "เปิดหน้าต่างใหม่"
},
"refresh": {
"message": "Refresh"
"message": "รีเฟรช"
},
"cards": {
"message": "Cards"
"message": "บัตร"
},
"identities": {
"message": "Identities"
"message": "ข้อมูลระบุตัวตน"
},
"logins": {
"message": "Logins"
"message": "เข้าสู่ระบบ"
},
"secureNotes": {
"message": "Secure Notes"
},
"clear": {
"message": "Clear",
"message": "ลบทิ้ง",
"description": "To clear something out. example: To clear browser history."
},
"checkPassword": {
"message": "Check if password has been exposed."
"message": "ตรวจสอบว่ารหัสผ่านถูกเปิดเผยหรือไม่"
},
"passwordExposed": {
"message": "This password has been exposed in data breaches. You should change it.",
@@ -1245,28 +1245,28 @@
}
},
"passwordSafe": {
"message": "This password was not found in any known data breaches. It should be safe to use."
"message": "ไม่พบรหัสผ่านนี้ในการละเมิดข้อมูลที่มี ควรใช้อย่างปลอดภัย"
},
"baseDomain": {
"message": "Base domain",
"message": "โดเมนพื้นฐาน",
"description": "Domain name. Ex. website.com"
},
"domainName": {
"message": "Domain name",
"message": "ชื่อโดเมน",
"description": "Domain name. Ex. website.com"
},
"host": {
"message": "Host",
"message": "โฮสต์",
"description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'."
},
"exact": {
"message": "Exact"
"message": "ถูกต้อง"
},
"startsWith": {
"message": "Starts with"
"message": "เริ่มต้นด้วย"
},
"regEx": {
"message": "Regular expression",
"message": "นิพจน์ทั่วไป",
"description": "A programming term, also known as 'RegEx'."
},
"matchDetection": {
@@ -1274,14 +1274,14 @@
"description": "URI match detection for auto-fill."
},
"defaultMatchDetection": {
"message": "Default match detection",
"message": "การตรวจจับการจับคู่เริ่มต้น",
"description": "Default URI match detection for auto-fill."
},
"toggleOptions": {
"message": "Toggle Options"
},
"toggleCurrentUris": {
"message": "Toggle current URIs",
"message": "สลับ URI ปัจจุบัน",
"description": "Toggle the display of the URIs of the currently open tabs in the browser."
},
"currentUri": {
@@ -1293,26 +1293,26 @@
"description": "An entity of multiple related people (ex. a team or business organization)."
},
"types": {
"message": "Types"
"message": "ชนิด"
},
"allItems": {
"message": "รายการทั้งหมด"
},
"noPasswordsInList": {
"message": "There are no passwords to list."
"message": "ไม่มีรหัสผ่านที่จะแสดง"
},
"remove": {
"message": "ลบ"
},
"default": {
"message": "Default"
"message": "ค่าเริ่มต้น"
},
"dateUpdated": {
"message": "อัปเดตแล้ว",
"description": "ex. Date this item was updated"
},
"dateCreated": {
"message": "Created",
"message": "สร้างเมื่อ",
"description": "ex. Date this item was created"
},
"datePasswordUpdated": {
@@ -1320,13 +1320,13 @@
"description": "ex. Date this password was updated"
},
"neverLockWarning": {
"message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected."
"message": "คุณแน่ใจหรือไม่ว่าต้องการใช้ตัวเลือก \"ไม่เคย\" การตั้งค่าตัวเลือกการล็อกเป็น \"ไม่\" จะเก็บคีย์เข้ารหัสของห้องนิรภัยไว้ในอุปกรณ์ของคุณ หากคุณใช้ตัวเลือกนี้ คุณควรตรวจสอบให้แน่ใจว่าคุณปกป้องอุปกรณ์ของคุณอย่างเหมาะสม"
},
"noOrganizationsList": {
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
},
"noCollectionsInList": {
"message": "There are no collections to list."
"message": "ไม่มีคอลเลกชันที่จะแสดง"
},
"ownership": {
"message": "เจ้าของ"
@@ -1350,7 +1350,7 @@
"message": "Weak Master Password"
},
"weakMasterPasswordDesc": {
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
"message": "รหัสผ่านหลักที่คุณเลือกนั้นไม่รัดกุม คุณควรใช้รหัสผ่านหลักที่รัดกุม (หรือวลีรหัสผ่าน) เพื่อปกป้องบัญชี Bitwarden ของคุณอย่างเหมาะสม คุณแน่ใจหรือไม่ว่าต้องการใช้รหัสผ่านหลักนี้"
},
"pin": {
"message": "PIN",
@@ -1369,31 +1369,31 @@
"message": "PIN ไม่ถูกต้อง"
},
"unlockWithBiometrics": {
"message": "Unlock with biometrics"
"message": "ปลดล็อกด้วยไบโอเมตริก"
},
"awaitDesktop": {
"message": "Awaiting confirmation from desktop"
},
"awaitDesktopDesc": {
"message": "Please confirm using biometrics in the Bitwarden desktop application to set up biometrics for browser."
"message": "โปรดยืนยันการใช้ไบโอเมตริกในแอปพลิเคชันเดสก์ท็อป Bitwarden เพื่อตั้งค่าไบโอเมตริกสำหรับเบราว์เซอร์"
},
"lockWithMasterPassOnRestart": {
"message": "Lock with master password on browser restart"
"message": "ล็อคด้วยรหัสผ่านหลักเมื่อรีสตาร์ทเบราว์เซอร์"
},
"selectOneCollection": {
"message": "You must select at least one collection."
"message": "คุณต้องเลือกอย่างน้อยหนึ่งคอลเลกชัน"
},
"cloneItem": {
"message": "Clone item"
"message": "โคลนรายการ"
},
"clone": {
"message": "Clone"
"message": "โคลน"
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
"message": "นโยบายองค์กรอย่างน้อยหนึ่งนโยบายส่งผลต่อการตั้งค่าตัวสร้างของคุณ"
},
"vaultTimeoutAction": {
"message": "Vault timeout action"
"message": "การดำเนินการหลังหมดเวลาล็อคตู้เซฟ"
},
"lock": {
"message": "ล็อก",
@@ -1407,46 +1407,46 @@
"message": "ค้นหาในถังขยะ"
},
"permanentlyDeleteItem": {
"message": "Permanently delete item"
"message": "ลบรายการอย่างถาวร"
},
"permanentlyDeleteItemConfirmation": {
"message": "Are you sure you want to permanently delete this item?"
"message": "คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้อย่างถาวร?"
},
"permanentlyDeletedItem": {
"message": "Item permanently deleted"
"message": "ลบรายการอย่างถาวรแล้ว"
},
"restoreItem": {
"message": "Restore item"
"message": "กู้คืนรายการ"
},
"restoreItemConfirmation": {
"message": "Are you sure you want to restore this item?"
"message": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนรายการนี้"
},
"restoredItem": {
"message": "Item restored"
"message": "คืนค่ารายการแล้ว"
},
"vaultTimeoutLogOutConfirmation": {
"message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?"
"message": "การออกจากระบบจะลบการเข้าถึงตู้นิรภัยของคุณทั้งหมด และต้องมีการตรวจสอบสิทธิ์ออนไลน์หลังจากหมดเวลา คุณแน่ใจหรือไม่ว่าต้องการใช้การตั้งค่านี้"
},
"vaultTimeoutLogOutConfirmationTitle": {
"message": "Timeout action confirmation"
"message": "การยืนยันการดำเนินการหมดเวลา"
},
"autoFillAndSave": {
"message": "Auto-fill and save"
"message": "กรอกอัตโนมัติและบันทึก"
},
"autoFillSuccessAndSavedUri": {
"message": "Item auto-filled and URI saved"
"message": "เติมรายการอัตโนมัติและบันทึก URI แล้ว"
},
"autoFillSuccess": {
"message": "Item auto-filled "
"message": "รายการเติมอัตโนมัติ "
},
"setMasterPassword": {
"message": "ตั้งรหัสผ่านหลัก"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
"message": "นโยบายองค์กรอย่างน้อยหนึ่งนโยบายกำหนดให้รหัสผ่านหลักของคุณเป็นไปตามข้อกำหนดต่อไปนี้:"
},
"policyInEffectMinComplexity": {
"message": "Minimum complexity score of $SCORE$",
"message": "คะแนนความซับซ้อนขั้นต่ำ $SCORE$",
"placeholders": {
"score": {
"content": "$1",
@@ -1482,7 +1482,7 @@
}
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
"message": "รหัสผ่านหลักใหม่ของคุณไม่เป็นไปตามข้อกำหนดของนโยบาย"
},
"acceptPolicies": {
"message": "By checking this box you agree to the following:"

File diff suppressed because it is too large Load Diff

View File

@@ -2039,7 +2039,7 @@
"message": "不是你?"
},
"newAroundHere": {
"message": "新建在这里"
"message": "初来乍到吗"
},
"rememberEmail": {
"message": "记住电子邮件地址"

View File

@@ -2039,7 +2039,7 @@
"message": "不是您嗎?"
},
"newAroundHere": {
"message": "New around here?"
"message": "第一次使用?"
},
"rememberEmail": {
"message": "記住電子郵件地址"

View File

@@ -0,0 +1,66 @@
import { BrowserApi } from "../browser/browserApi";
import { clearClipboardAlarmName } from "../clipboard";
export const alarmKeys = [clearClipboardAlarmName] as const;
export type AlarmKeys = typeof alarmKeys[number];
type AlarmState = { [T in AlarmKeys]: number | undefined };
const alarmState: AlarmState = {
clearClipboard: null,
//TODO once implemented vaultTimeout: null;
//TODO once implemented checkNotifications: null;
//TODO once implemented (if necessary) processReload: null;
};
/**
* Retrieves the set alarm time (planned execution) for a give an commandName {@link AlarmState}
* @param commandName A command that has been previously registered with {@link AlarmState}
* @returns {Promise<number>} null or Unix epoch timestamp when the alarm action is supposed to execute
* @example
* // getAlarmTime(clearClipboard)
*/
export async function getAlarmTime(commandName: AlarmKeys): Promise<number> {
let alarmTime: number;
if (BrowserApi.manifestVersion == 3) {
const fromSessionStore = await chrome.storage.session.get(commandName);
alarmTime = fromSessionStore[commandName];
} else {
alarmTime = alarmState[commandName];
}
return alarmTime;
}
/**
* Registers an action that should execute after the given time has passed
* @param commandName A command that has been previously registered with {@link AlarmState}
* @param delay_ms The number of ms from now in which the command should execute from
* @example
* // setAlarmTime(clearClipboard, 5000) register the clearClipboard action which will execute when at least 5 seconds from now have passed
*/
export async function setAlarmTime(commandName: AlarmKeys, delay_ms: number): Promise<void> {
if (!delay_ms || delay_ms === 0) {
await this.clearAlarmTime(commandName);
return;
}
const time = Date.now() + delay_ms;
await setAlarmTimeInternal(commandName, time);
}
/**
* Clears the time currently set for a given command
* @param commandName A command that has been previously registered with {@link AlarmState}
*/
export async function clearAlarmTime(commandName: AlarmKeys): Promise<void> {
await setAlarmTimeInternal(commandName, null);
}
async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise<void> {
if (BrowserApi.manifestVersion == 3) {
await chrome.storage.session.set({ [commandName]: time });
} else {
alarmState[commandName] = time;
}
}

View File

@@ -0,0 +1,26 @@
import { ClearClipboard, clearClipboardAlarmName } from "../clipboard";
import { alarmKeys, clearAlarmTime, getAlarmTime } from "./alarm-state";
export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => {
alarmKeys.forEach(async (key) => {
const executionTime = await getAlarmTime(key);
if (!executionTime) {
return;
}
const currentDate = Date.now();
if (executionTime > currentDate) {
return;
}
await clearAlarmTime(key);
switch (key) {
case clearClipboardAlarmName:
ClearClipboard.run();
break;
default:
}
});
};

View File

@@ -0,0 +1,29 @@
const NUMBER_OF_ALARMS = 6;
export function registerAlarms() {
alarmsToBeCreated(NUMBER_OF_ALARMS);
}
/**
* Creates staggered alarms that periodically (1min) raise OnAlarm events. The staggering is calculated based on the numnber of alarms passed in.
* @param numberOfAlarms Number of named alarms, that shall be registered
* @example
* // alarmsToBeCreated(2) results in 2 alarms separated by 30 seconds
* @example
* // alarmsToBeCreated(4) results in 4 alarms separated by 15 seconds
* @example
* // alarmsToBeCreated(6) results in 6 alarms separated by 10 seconds
* @example
* // alarmsToBeCreated(60) results in 60 alarms separated by 1 second
*/
function alarmsToBeCreated(numberOfAlarms: number): void {
const oneMinuteInMs = 60 * 1000;
const offset = oneMinuteInMs / numberOfAlarms;
let calculatedWhen: number = Date.now() + offset;
for (let index = 0; index < numberOfAlarms; index++) {
chrome.alarms.create(`bw_alarm${index}`, { periodInMinutes: 1, when: calculatedWhen });
calculatedWhen += offset;
}
}

View File

@@ -1,39 +1,32 @@
import { onAlarmListener } from "./alarms/on-alarm-listener";
import { registerAlarms } from "./alarms/register-alarms";
import MainBackground from "./background/main.background";
import { BrowserApi } from "./browser/browserApi";
import { ClearClipboard } from "./clipboard";
import { onCommandListener } from "./listeners/onCommandListener";
import { onInstallListener } from "./listeners/onInstallListener";
import { UpdateBadge } from "./listeners/update-badge";
const manifestV3MessageListeners: ((
serviceCache: Record<string, unknown>,
message: { command: string }
) => void | Promise<void>)[] = [UpdateBadge.messageListener];
type AlarmAction = (executionTime: Date, serviceCache: Record<string, unknown>) => void;
const AlarmActions: AlarmAction[] = [ClearClipboard.run];
import {
contextMenusClickedListener,
onCommandListener,
onInstallListener,
runtimeMessageListener,
tabsOnActivatedListener,
tabsOnReplacedListener,
tabsOnUpdatedListener,
} from "./listeners";
if (BrowserApi.manifestVersion === 3) {
chrome.commands.onCommand.addListener(onCommandListener);
chrome.runtime.onInstalled.addListener(onInstallListener);
chrome.tabs.onActivated.addListener(UpdateBadge.tabsOnActivatedListener);
chrome.tabs.onReplaced.addListener(UpdateBadge.tabsOnReplacedListener);
chrome.tabs.onUpdated.addListener(UpdateBadge.tabsOnUpdatedListener);
BrowserApi.messageListener("runtime.background", (message) => {
const serviceCache = {};
manifestV3MessageListeners.forEach((listener) => {
listener(serviceCache, message);
});
});
chrome.alarms.onAlarm.addListener((_alarm) => {
const executionTime = new Date();
const serviceCache = {};
for (const alarmAction of AlarmActions) {
alarmAction(executionTime, serviceCache);
chrome.alarms.onAlarm.addListener(onAlarmListener);
registerAlarms();
chrome.tabs.onActivated.addListener(tabsOnActivatedListener);
chrome.tabs.onReplaced.addListener(tabsOnReplacedListener);
chrome.tabs.onUpdated.addListener(tabsOnUpdatedListener);
chrome.contextMenus.onClicked.addListener(contextMenusClickedListener);
BrowserApi.messageListener(
"runtime.background",
(message: { command: string }, sender, sendResponse) => {
runtimeMessageListener(message, sender);
}
});
);
} else {
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().then(() => {

View File

@@ -1,146 +1,38 @@
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { BrowserApi } from "../browser/browserApi";
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
import MainBackground from "./main.background";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class ContextMenusBackground {
private readonly noopCommandSuffix = "noop";
private contextMenus: any;
private contextMenus: typeof chrome.contextMenus;
constructor(
private main: MainBackground,
private cipherService: CipherService,
private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService,
private authService: AuthService,
private eventService: EventService,
private totpService: TotpService
) {
constructor(private contextMenuClickedHandler: ContextMenuClickedHandler) {
this.contextMenus = chrome.contextMenus;
}
async init() {
init() {
if (!this.contextMenus) {
return;
}
this.contextMenus.onClicked.addListener(
async (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => {
if (info.menuItemId === "generate-password") {
await this.generatePasswordToClipboard();
} else if (info.menuItemId === "copy-identifier") {
await this.getClickedElement(tab, info.frameId);
} else if (
info.parentMenuItemId === "autofill" ||
info.parentMenuItemId === "copy-username" ||
info.parentMenuItemId === "copy-password" ||
info.parentMenuItemId === "copy-totp"
) {
await this.cipherAction(tab, info);
}
}
this.contextMenus.onClicked.addListener((info, tab) =>
this.contextMenuClickedHandler.run(info, tab)
);
BrowserApi.messageListener(
"contextmenus.background",
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
async (
msg: { command: string; data: LockedVaultPendingNotificationsItem },
sender: chrome.runtime.MessageSender,
sendResponse: any
) => {
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
await this.cipherAction(
msg.data.commandToRetry.sender.tab,
msg.data.commandToRetry.msg.data
await this.contextMenuClickedHandler.cipherAction(
msg.data.commandToRetry.msg.data,
msg.data.commandToRetry.sender.tab
);
}
}
);
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
}
private async getClickedElement(tab: chrome.tabs.Tab, frameId: number) {
if (tab == null) {
return;
}
BrowserApi.tabSendMessage(tab, { command: "getClickedElement" }, { frameId: frameId });
}
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
if (typeof info.menuItemId !== "string") {
return;
}
const id = info.menuItemId.split("_")[1];
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: this.noopCommandSuffix, data: info },
sender: { tab: tab },
},
target: "contextmenus.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
let cipher: CipherView;
if (id === this.noopCommandSuffix) {
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
} else {
const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find((c) => c.id === id);
}
if (cipher == null) {
return;
}
if (info.parentMenuItemId === "autofill") {
await this.startAutofillPage(tab, cipher);
} else if (info.parentMenuItemId === "copy-username") {
this.platformUtilsService.copyToClipboard(cipher.login.username, { window: window });
} else if (info.parentMenuItemId === "copy-password") {
this.platformUtilsService.copyToClipboard(cipher.login.password, { window: window });
this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (info.parentMenuItemId === "copy-totp") {
const totpValue = await this.totpService.getCode(cipher.login.totp);
this.platformUtilsService.copyToClipboard(totpValue, { window: window });
}
}
private async startAutofillPage(tab: chrome.tabs.Tab, cipher: CipherView) {
this.main.loginToAutoFill = cipher;
if (tab == null) {
return;
}
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
}
}

View File

@@ -1,7 +1,7 @@
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { StateService } from "../services/abstractions/state.service";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
const IdleInterval = 60 * 5; // 5 minutes
@@ -12,7 +12,7 @@ export default class IdleBackground {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private stateService: StateService,
private stateService: BrowserStateService,
private notificationsService: NotificationsService
) {
this.idle = chrome.idle || (browser != null ? browser.idle : null);

View File

@@ -1,3 +1,4 @@
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@@ -7,7 +8,8 @@ import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/co
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { EventService as EventServiceAbstraction } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/abstractions/fido2/fido2-user-interface.service.abstraction";
import { Fido2Service as Fido2ServiceAbstraction } from "@bitwarden/common/abstractions/fido2/fido2.service.abstraction";
@@ -19,7 +21,7 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
@@ -28,7 +30,10 @@ import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
@@ -40,12 +45,10 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
import { UsernameGenerationService as UsernameGenerationServiceAbstraction } from "@bitwarden/common/abstractions/usernameGeneration.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AppIdService } from "@bitwarden/common/services/appId.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -56,7 +59,8 @@ import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service
import { ContainerService } from "@bitwarden/common/services/container.service";
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/services/cryptography/multithread-encrypt.service.implementation";
import { EventService } from "@bitwarden/common/services/event.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { ExportService } from "@bitwarden/common/services/export.service";
import { Fido2Service } from "@bitwarden/common/services/fido2/fido2.service";
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
@@ -64,14 +68,11 @@ import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.s
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/services/provider.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SendService } from "@bitwarden/common/services/send.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { SyncService } from "@bitwarden/common/services/sync/sync.service";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
@@ -86,33 +87,39 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTim
import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service";
import { BrowserApi } from "../browser/browserApi";
import { CipherContextMenuHandler } from "../browser/cipher-context-menu-handler";
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
import { MainContextMenuHandler } from "../browser/main-context-menu-handler";
import { SafariApp } from "../browser/safariApp";
import { AutofillTabCommand } from "../commands/autofill-tab-command";
import { flagEnabled } from "../flags";
import { UpdateBadge } from "../listeners/update-badge";
import { Account } from "../models/account";
import { PopupUtilsService } from "../popup/services/popup-utils.service";
import { AutofillService as AutofillServiceAbstraction } from "../services/abstractions/autofill.service";
import { StateService as StateServiceAbstraction } from "../services/abstractions/state.service";
import { BrowserStateService as StateServiceAbstraction } from "../services/abstractions/browser-state.service";
import AutofillService from "../services/autofill.service";
import { BrowserEnvironmentService } from "../services/browser-environment.service";
import { BrowserFolderService } from "../services/browser-folder.service";
import { BrowserOrganizationService } from "../services/browser-organization.service";
import { BrowserPolicyService } from "../services/browser-policy.service";
import { BrowserSettingsService } from "../services/browser-settings.service";
import { BrowserStateService } from "../services/browser-state.service";
import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserMessagingService from "../services/browserMessaging.service";
import BrowserMessagingPrivateModeBackgroundService from "../services/browserMessagingPrivateModeBackground.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import { BrowserFido2UserInterfaceService } from "../services/fido2/browser-fido2-user-interface.service";
import { FolderService } from "../services/folders/folder.service";
import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
import { VaultFilterService } from "../services/vaultFilter.service";
import VaultTimeoutService from "../services/vaultTimeout/vaultTimeout.service";
import CommandsBackground from "./commands.background";
import ContextMenusBackground from "./contextMenus.background";
import IdleBackground from "./idle.background";
import IconDetails from "./models/iconDetails";
import { NativeMessagingBackground } from "./nativeMessaging.background";
import NotificationBackground from "./notification.background";
import RuntimeBackground from "./runtime.background";
@@ -123,7 +130,7 @@ export default class MainBackground {
messagingService: MessagingServiceAbstraction;
storageService: AbstractStorageService;
secureStorageService: AbstractStorageService;
memoryStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService;
i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction;
@@ -152,12 +159,13 @@ export default class MainBackground {
stateService: StateServiceAbstraction;
stateMigrationService: StateMigrationService;
systemService: SystemServiceAbstraction;
eventService: EventServiceAbstraction;
eventCollectionService: EventCollectionServiceAbstraction;
eventUploadService: EventUploadServiceAbstraction;
policyService: InternalPolicyServiceAbstraction;
popupUtilsService: PopupUtilsService;
sendService: SendServiceAbstraction;
fileUploadService: FileUploadServiceAbstraction;
organizationService: OrganizationServiceAbstraction;
organizationService: InternalOrganizationServiceAbstraction;
providerService: ProviderServiceAbstraction;
keyConnectorService: KeyConnectorServiceAbstraction;
userVerificationService: UserVerificationServiceAbstraction;
@@ -171,6 +179,9 @@ export default class MainBackground {
syncNotifierService: SyncNotifierServiceAbstraction;
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
fido2Service: Fido2ServiceAbstraction;
avatarUpdateService: AvatarUpdateServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler;
cipherContextMenuHandler: CipherContextMenuHandler;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window;
@@ -188,8 +199,6 @@ export default class MainBackground {
private webRequestBackground: WebRequestBackground;
private sidebarAction: any;
private buildingContextMenu: boolean;
private menuOptionsLoaded: any[] = [];
private syncTimeout: any;
private isSafari: boolean;
private nativeMessagingBackground: NativeMessagingBackground;
@@ -233,7 +242,7 @@ export default class MainBackground {
this.secureStorageService,
new StateFactory(GlobalState, Account)
);
this.stateService = new StateService(
this.stateService = new BrowserStateService(
this.storageService,
this.secureStorageService,
this.memoryStorageService,
@@ -288,7 +297,7 @@ export default class MainBackground {
this.appIdService,
(expired: boolean) => this.logout(expired)
);
this.settingsService = new SettingsService(this.stateService);
this.settingsService = new BrowserSettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
@@ -301,7 +310,7 @@ export default class MainBackground {
this.stateService,
this.encryptService
);
this.folderService = new FolderService(
this.folderService = new BrowserFolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
@@ -323,13 +332,12 @@ export default class MainBackground {
this.stateService
);
this.syncNotifierService = new SyncNotifierService();
this.organizationService = new OrganizationService(this.stateService, this.syncNotifierService);
this.policyService = new PolicyService(this.stateService, this.organizationService);
this.organizationService = new BrowserOrganizationService(this.stateService);
this.policyService = new BrowserPolicyService(this.stateService, this.organizationService);
this.policyApiService = new PolicyApiService(
this.policyService,
this.apiService,
this.stateService,
this.organizationService
this.stateService
);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
@@ -415,15 +423,19 @@ export default class MainBackground {
this.stateService,
this.providerService,
this.folderApiService,
this.syncNotifierService,
this.organizationService,
logoutCallback
);
this.eventService = new EventService(
this.eventUploadService = new EventUploadService(
this.apiService,
this.stateService,
this.logService
);
this.eventCollectionService = new EventCollectionService(
this.cipherService,
this.stateService,
this.logService,
this.organizationService
this.organizationService,
this.eventUploadService
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
@@ -435,7 +447,7 @@ export default class MainBackground {
this.cipherService,
this.stateService,
this.totpService,
this.eventService,
this.eventCollectionService,
this.logService
);
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
@@ -535,15 +547,25 @@ export default class MainBackground {
);
this.tabsBackground = new TabsBackground(this, this.notificationBackground);
this.contextMenusBackground = new ContextMenusBackground(
this,
this.cipherService,
this.passwordGenerationService,
this.platformUtilsService,
this.authService,
this.eventService,
this.totpService
);
if (!this.popupOnlyContext) {
const contextMenuClickedHandler = new ContextMenuClickedHandler(
(options) => this.platformUtilsService.copyToClipboard(options.text, { window: self }),
async (_tab) => {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
},
this.authService,
this.cipherService,
new AutofillTabCommand(this.autofillService),
this.totpService,
this.eventCollectionService
);
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
}
this.idleBackground = new IdleBackground(
this.vaultTimeoutService,
this.stateService,
@@ -560,6 +582,18 @@ export default class MainBackground {
this.stateService,
this.apiService
);
this.avatarUpdateService = new AvatarUpdateService(this.apiService, this.stateService);
if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler(this.stateService, this.i18nService);
this.cipherContextMenuHandler = new CipherContextMenuHandler(
this.mainContextMenuHandler,
this.authService,
this.cipherService
);
}
}
async bootstrap() {
@@ -569,7 +603,7 @@ export default class MainBackground {
await (this.vaultTimeoutService as VaultTimeoutService).init(true);
await (this.i18nService as I18nService).init();
await (this.eventService as EventService).init(true);
await (this.eventUploadService as EventUploadService).init(true);
await this.runtimeBackground.init();
await this.notificationBackground.init();
await this.commandsBackground.init();
@@ -577,7 +611,9 @@ export default class MainBackground {
this.twoFactorService.init();
await this.tabsBackground.init();
await this.contextMenusBackground.init();
if (!this.popupOnlyContext) {
this.contextMenusBackground?.init();
}
await this.idleBackground.init();
await this.webRequestBackground.init();
@@ -598,7 +634,9 @@ export default class MainBackground {
return new Promise<void>((resolve) => {
setTimeout(async () => {
await this.environmentService.setUrlsFromStorage();
await this.refreshBadge();
if (!this.isPrivateMode) {
await this.refreshBadge();
}
this.fullSync(true);
setTimeout(() => this.notificationsService.init(), 2500);
resolve();
@@ -615,30 +653,27 @@ export default class MainBackground {
return;
}
const menuDisabled = await this.stateService.getDisableContextMenuItem();
if (!menuDisabled) {
await this.buildContextMenu();
} else {
await this.contextMenusRemoveAll();
}
await MainContextMenuHandler.removeAll();
if (forLocked) {
await this.loadMenuForNoAccessState(!menuDisabled);
await this.mainContextMenuHandler?.noAccess();
this.onUpdatedRan = this.onReplacedRan = false;
return;
}
await this.mainContextMenuHandler?.init();
const tab = await BrowserApi.getTabFromCurrentWindow();
if (tab) {
await this.contextMenuReady(tab, !menuDisabled);
await this.cipherContextMenuHandler?.update(tab.url);
this.onUpdatedRan = this.onReplacedRan = false;
}
}
async logout(expired: boolean, userId?: string) {
await this.eventService.uploadEvents(userId);
await this.eventUploadService.uploadEvents(userId);
await Promise.all([
this.eventService.clearEvents(userId),
this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId),
this.settingsService.clear(userId),
@@ -663,7 +698,7 @@ export default class MainBackground {
BrowserApi.sendMessage("updateBadge");
}
await this.refreshBadge();
await this.refreshMenu(true);
await this.mainContextMenuHandler.noAccess();
await this.reseedStorage();
this.notificationsService.updateConnection(false);
await this.systemService.clearPendingClipboard();
@@ -737,204 +772,6 @@ export default class MainBackground {
}
}
private async buildContextMenu() {
if (!chrome.contextMenus || this.buildingContextMenu) {
return;
}
this.buildingContextMenu = true;
await this.contextMenusRemoveAll();
await this.contextMenusCreate({
type: "normal",
id: "root",
contexts: ["all"],
title: "Bitwarden",
});
await this.contextMenusCreate({
type: "normal",
id: "autofill",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("autoFill"),
});
await this.contextMenusCreate({
type: "normal",
id: "copy-username",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("copyUsername"),
});
await this.contextMenusCreate({
type: "normal",
id: "copy-password",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("copyPassword"),
});
if (await this.stateService.getCanAccessPremium()) {
await this.contextMenusCreate({
type: "normal",
id: "copy-totp",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("copyVerificationCode"),
});
}
await this.contextMenusCreate({
type: "separator",
parentId: "root",
});
await this.contextMenusCreate({
type: "normal",
id: "generate-password",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("generatePasswordCopied"),
});
await this.contextMenusCreate({
type: "normal",
id: "copy-identifier",
parentId: "root",
contexts: ["all"],
title: this.i18nService.t("copyElementIdentifier"),
});
this.buildingContextMenu = false;
}
private async contextMenuReady(tab: any, contextMenuEnabled: boolean) {
await this.loadMenu(tab.url, tab.id, contextMenuEnabled);
this.onUpdatedRan = this.onReplacedRan = false;
}
private async loadMenu(url: string, tabId: number, contextMenuEnabled: boolean) {
if (!url || (!chrome.browserAction && !this.sidebarAction)) {
return;
}
this.menuOptionsLoaded = [];
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Unlocked) {
try {
const ciphers = await this.cipherService.getAllDecryptedForUrl(url);
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
if (contextMenuEnabled) {
ciphers.forEach((cipher) => {
this.loadLoginContextMenuOptions(cipher);
});
}
if (contextMenuEnabled && ciphers.length === 0) {
await this.loadNoLoginsContextMenuOptions(this.i18nService.t("noMatchingLogins"));
}
return;
} catch (e) {
this.logService.error(e);
}
}
await this.loadMenuForNoAccessState(contextMenuEnabled);
}
private async loadMenuForNoAccessState(contextMenuEnabled: boolean) {
if (contextMenuEnabled) {
const authed = await this.stateService.getIsAuthenticated();
await this.loadNoLoginsContextMenuOptions(
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu")
);
}
}
private async loadLoginContextMenuOptions(cipher: any) {
if (
cipher == null ||
cipher.type !== CipherType.Login ||
cipher.reprompt !== CipherRepromptType.None
) {
return;
}
let title = cipher.name;
if (cipher.login.username && cipher.login.username !== "") {
title += " (" + cipher.login.username + ")";
}
await this.loadContextMenuOptions(title, cipher.id, cipher);
}
private async loadNoLoginsContextMenuOptions(noLoginsMessage: string) {
await this.loadContextMenuOptions(noLoginsMessage, "noop", null);
}
private async loadContextMenuOptions(title: string, idSuffix: string, cipher: any) {
if (
!chrome.contextMenus ||
this.menuOptionsLoaded.indexOf(idSuffix) > -1 ||
(cipher != null && cipher.type !== CipherType.Login)
) {
return;
}
this.menuOptionsLoaded.push(idSuffix);
if (cipher == null || (cipher.login.password && cipher.login.password !== "")) {
await this.contextMenusCreate({
type: "normal",
id: "autofill_" + idSuffix,
parentId: "autofill",
contexts: ["all"],
title: this.sanitizeContextMenuTitle(title),
});
}
if (cipher == null || (cipher.login.username && cipher.login.username !== "")) {
await this.contextMenusCreate({
type: "normal",
id: "copy-username_" + idSuffix,
parentId: "copy-username",
contexts: ["all"],
title: this.sanitizeContextMenuTitle(title),
});
}
if (
cipher == null ||
(cipher.login.password && cipher.login.password !== "" && cipher.viewPassword)
) {
await this.contextMenusCreate({
type: "normal",
id: "copy-password_" + idSuffix,
parentId: "copy-password",
contexts: ["all"],
title: this.sanitizeContextMenuTitle(title),
});
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (canAccessPremium && (cipher == null || (cipher.login.totp && cipher.login.totp !== ""))) {
await this.contextMenusCreate({
type: "normal",
id: "copy-totp_" + idSuffix,
parentId: "copy-totp",
contexts: ["all"],
title: this.sanitizeContextMenuTitle(title),
});
}
}
private sanitizeContextMenuTitle(title: string): string {
return title.replace(/&/g, "&&");
}
private async fullSync(override = false) {
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
const lastSync = await this.syncService.getLastSync();
@@ -959,54 +796,4 @@ export default class MainBackground {
this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes
}
// Browser API Helpers
private contextMenusRemoveAll() {
return new Promise<void>((resolve) => {
chrome.contextMenus.removeAll(() => {
resolve();
if (chrome.runtime.lastError) {
return;
}
});
});
}
private contextMenusCreate(options: any) {
return new Promise<void>((resolve) => {
chrome.contextMenus.create(options, () => {
resolve();
if (chrome.runtime.lastError) {
return;
}
});
});
}
private async actionSetIcon(theAction: any, suffix: string, windowId?: number): Promise<any> {
if (!theAction || !theAction.setIcon) {
return;
}
const options: IconDetails = {
path: {
19: "images/icon19" + suffix + ".png",
38: "images/icon38" + suffix + ".png",
},
};
if (this.platformUtilsService.isFirefox()) {
options.windowId = windowId;
await theAction.setIcon(options);
} else if (this.platformUtilsService.isSafari()) {
// Workaround since Safari 14.0.3 returns a pending promise
// which doesn't resolve within a reasonable time.
theAction.setIcon(options);
} else {
return new Promise<void>((resolve) => {
theAction.setIcon(options, () => resolve());
});
}
}
}

View File

@@ -15,7 +15,7 @@ import { LoginView } from "@bitwarden/common/models/view/login.view";
import { BrowserApi } from "../browser/browserApi";
import { AutofillService } from "../services/abstractions/autofill.service";
import { StateService } from "../services/abstractions/state.service";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
import AddChangePasswordQueueMessage from "./models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from "./models/addLoginQueueMessage";
@@ -33,7 +33,7 @@ export default class NotificationBackground {
private authService: AuthService,
private policyService: PolicyService,
private folderService: FolderService,
private stateService: StateService
private stateService: BrowserStateService
) {}
async init() {

View File

@@ -110,6 +110,7 @@ export default class RuntimeBackground {
await this.main.refreshBadge();
await this.main.refreshMenu();
}, 2000);
this.main.avatarUpdateService.loadColorFromState();
}
break;
case "openPopup":

View File

@@ -2,7 +2,10 @@ import { AutofillService as AbstractAutoFillService } from "../../services/abstr
import AutofillService from "../../services/autofill.service";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import { EventServiceInitOptions, eventServiceFactory } from "./event-service.factory";
import {
EventCollectionServiceInitOptions,
eventCollectionServiceFactory,
} from "./event-collection-service.factory";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
@@ -14,7 +17,7 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
TotpServiceInitOptions &
EventServiceInitOptions &
EventCollectionServiceInitOptions &
LogServiceInitOptions;
export function autofillServiceFactory(
@@ -30,7 +33,7 @@ export function autofillServiceFactory(
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await totpServiceFactory(cache, opts),
await eventServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
)
);

View File

@@ -47,7 +47,7 @@ export function cipherServiceFactory(
await fileUploadServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
opts.cipherServiceOptions?.searchServiceFactory === undefined
? () => cache.searchService
? () => cache.searchService as SearchService
: opts.cipherServiceOptions.searchServiceFactory,
await logServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),

View File

@@ -1,7 +1,4 @@
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/services/cryptography/multithread-encrypt.service.implementation";
import { flagEnabled } from "../../flags";
import {
cryptoFunctionServiceFactory,
@@ -24,17 +21,15 @@ export function encryptServiceFactory(
cache: { encryptService?: EncryptServiceImplementation } & CachedServices,
opts: EncryptServiceInitOptions
): Promise<EncryptServiceImplementation> {
return factory(cache, "encryptService", opts, async () =>
flagEnabled("multithreadDecryption")
? new MultithreadEncryptServiceImplementation(
await cryptoFunctionServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
opts.encryptServiceOptions.logMacFailures
)
: new EncryptServiceImplementation(
await cryptoFunctionServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
opts.encryptServiceOptions.logMacFailures
)
return factory(
cache,
"encryptService",
opts,
async () =>
new EncryptServiceImplementation(
await cryptoFunctionServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
opts.encryptServiceOptions.logMacFailures
)
);
}

View File

@@ -0,0 +1,40 @@
import { EventCollectionService as AbstractEventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import {
eventUploadServiceFactory,
EventUploadServiceInitOptions,
} from "./event-upload-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import {
organizationServiceFactory,
OrganizationServiceInitOptions,
} from "./organization-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type EventCollectionServiceOptions = FactoryOptions;
export type EventCollectionServiceInitOptions = EventCollectionServiceOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
OrganizationServiceInitOptions &
EventUploadServiceInitOptions;
export function eventCollectionServiceFactory(
cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices,
opts: EventCollectionServiceInitOptions
): Promise<AbstractEventCollectionService> {
return factory(
cache,
"eventCollectionService",
opts,
async () =>
new EventCollectionService(
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await organizationServiceFactory(cache, opts),
await eventUploadServiceFactory(cache, opts)
)
);
}

View File

@@ -1,40 +0,0 @@
import { EventService as AbstractEventService } from "@bitwarden/common/abstractions/event.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import {
organizationServiceFactory,
OrganizationServiceInitOptions,
} from "./organization-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type EventServiceOptions = FactoryOptions;
export type EventServiceInitOptions = EventServiceOptions &
ApiServiceInitOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
LogServiceInitOptions &
OrganizationServiceInitOptions;
export function eventServiceFactory(
cache: { eventService?: AbstractEventService } & CachedServices,
opts: EventServiceInitOptions
): Promise<AbstractEventService> {
return factory(
cache,
"eventService",
opts,
async () =>
new EventService(
await apiServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await organizationServiceFactory(cache, opts)
)
);
}

View File

@@ -0,0 +1,31 @@
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type EventUploadServiceOptions = FactoryOptions;
export type EventUploadServiceInitOptions = EventUploadServiceOptions &
ApiServiceInitOptions &
StateServiceInitOptions &
LogServiceInitOptions;
export function eventUploadServiceFactory(
cache: { eventUploadService?: AbstractEventUploadService } & CachedServices,
opts: EventUploadServiceInitOptions
): Promise<AbstractEventUploadService> {
return factory(
cache,
"eventUploadService",
opts,
async () =>
new EventUploadService(
await apiServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
)
);
}

View File

@@ -1,4 +1,4 @@
export type CachedServices = Record<string, any>;
export type CachedServices = Record<string, unknown>;
export type FactoryOptions = {
alwaysInitializeNewService?: boolean;

View File

@@ -1,6 +1,6 @@
import { FolderService as AbstractFolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { FolderService } from "../../services/folders/folder.service";
import { BrowserFolderService } from "../../services/browser-folder.service";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import { cryptoServiceFactory, CryptoServiceInitOptions } from "./crypto-service.factory";
@@ -28,7 +28,7 @@ export function folderServiceFactory(
"folderService",
opts,
async () =>
new FolderService(
new BrowserFolderService(
await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts),

View File

@@ -1,17 +1,13 @@
import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { BrowserOrganizationService } from "../../services/browser-organization.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
import {
syncNotifierServiceFactory,
SyncNotifierServiceInitOptions,
} from "./sync-notifier-service.factory";
type OrganizationServiceFactoryOptions = FactoryOptions;
export type OrganizationServiceInitOptions = OrganizationServiceFactoryOptions &
SyncNotifierServiceInitOptions &
StateServiceInitOptions;
export function organizationServiceFactory(
@@ -22,10 +18,6 @@ export function organizationServiceFactory(
cache,
"organizationService",
opts,
async () =>
new OrganizationService(
await stateServiceFactory(cache, opts),
await syncNotifierServiceFactory(cache, opts)
)
async () => new BrowserOrganizationService(await stateServiceFactory(cache, opts))
);
}

View File

@@ -1,5 +1,6 @@
import { PolicyService as AbstractPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
import { BrowserPolicyService } from "../../services/browser-policy.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import {
@@ -26,7 +27,7 @@ export function policyServiceFactory(
"policyService",
opts,
async () =>
new PolicyService(
new BrowserPolicyService(
await stateServiceFactory(cache, opts),
await organizationServiceFactory(cache, opts)
)

View File

@@ -1,5 +1,6 @@
import { SettingsService as AbstractSettingsService } from "@bitwarden/common/abstractions/settings.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { BrowserSettingsService } from "../../services/browser-settings.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
@@ -16,6 +17,6 @@ export function settingsServiceFactory(
cache,
"settingsService",
opts,
async () => new SettingsService(await stateServiceFactory(cache, opts))
async () => new BrowserSettingsService(await stateServiceFactory(cache, opts))
);
}

View File

@@ -2,7 +2,7 @@ import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { Account } from "../../models/account";
import { StateService } from "../../services/state.service";
import { BrowserStateService } from "../../services/browser-state.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
@@ -34,15 +34,15 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
StateMigrationServiceInitOptions;
export async function stateServiceFactory(
cache: { stateService?: StateService } & CachedServices,
cache: { stateService?: BrowserStateService } & CachedServices,
opts: StateServiceInitOptions
): Promise<StateService> {
): Promise<BrowserStateService> {
const service = await factory(
cache,
"stateService",
opts,
async () =>
await new StateService(
await new BrowserStateService(
await diskStorageServiceFactory(cache, opts),
await secureStorageServiceFactory(cache, opts),
await memoryStorageServiceFactory(cache, opts),

View File

@@ -1,4 +1,7 @@
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserApi } from "../../browser/browserApi";
@@ -35,9 +38,9 @@ export function secureStorageServiceFactory(
}
export function memoryStorageServiceFactory(
cache: { memoryStorageService?: AbstractStorageService } & CachedServices,
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
opts: MemoryStorageServiceInitOptions
): Promise<AbstractStorageService> {
): Promise<AbstractMemoryStorageService> {
return factory(cache, "memoryStorageService", opts, async () => {
if (BrowserApi.manifestVersion === 3) {
return new LocalBackedSessionStorageService(

View File

@@ -44,7 +44,7 @@ export class BrowserApi {
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
return new Promise((resolve) => {
chrome.tabs.query(options, (tabs: any[]) => {
chrome.tabs.query(options, (tabs) => {
resolve(tabs);
});
});
@@ -63,7 +63,7 @@ export class BrowserApi {
tab: chrome.tabs.Tab,
command: string,
data: any = null
): Promise<any[]> {
): Promise<void> {
const obj: any = {
command: command,
};
@@ -75,11 +75,11 @@ export class BrowserApi {
return BrowserApi.tabSendMessage(tab, obj);
}
static async tabSendMessage(
static async tabSendMessage<T>(
tab: chrome.tabs.Tab,
obj: any,
obj: T,
options: chrome.tabs.MessageSendOptions = null
): Promise<any> {
): Promise<void> {
if (!tab || !tab.id) {
return;
}
@@ -94,12 +94,13 @@ export class BrowserApi {
});
}
static sendTabsMessage<T = never>(
static sendTabsMessage<T>(
tabId: number,
message: TabMessage,
options?: chrome.tabs.MessageSendOptions,
responseCallback?: (response: T) => void
) {
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, responseCallback);
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
}
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {

View File

@@ -0,0 +1,109 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { CipherContextMenuHandler } from "./cipher-context-menu-handler";
import { MainContextMenuHandler } from "./main-context-menu-handler";
describe("CipherContextMenuHandler", () => {
let mainContextMenuHandler: MockProxy<MainContextMenuHandler>;
let authService: MockProxy<AuthService>;
let cipherService: MockProxy<CipherService>;
let sut: CipherContextMenuHandler;
beforeEach(() => {
mainContextMenuHandler = mock();
authService = mock();
cipherService = mock();
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService);
});
afterEach(() => jest.resetAllMocks());
describe("update", () => {
it("locked, updates for no access", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
await sut.update("https://test.com");
expect(mainContextMenuHandler.noAccess).toHaveBeenCalledTimes(1);
});
it("logged out, updates for no access", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
await sut.update("https://test.com");
expect(mainContextMenuHandler.noAccess).toHaveBeenCalledTimes(1);
});
it("has menu disabled, does not load anything", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
await sut.update("https://test.com");
expect(mainContextMenuHandler.loadOptions).not.toHaveBeenCalled();
expect(mainContextMenuHandler.noAccess).not.toHaveBeenCalled();
expect(mainContextMenuHandler.noLogins).not.toHaveBeenCalled();
});
it("has no ciphers, add no ciphers item", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
mainContextMenuHandler.init.mockResolvedValue(true);
cipherService.getAllDecryptedForUrl.mockResolvedValue([]);
await sut.update("https://test.com");
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
});
it("only adds valid ciphers", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
mainContextMenuHandler.init.mockResolvedValue(true);
const realCipher = {
id: "5",
type: CipherType.Login,
reprompt: CipherRepromptType.None,
name: "Test Cipher",
login: { username: "Test Username" },
};
cipherService.getAllDecryptedForUrl.mockResolvedValue([
null,
undefined,
{ type: CipherType.Card },
{ type: CipherType.Login, reprompt: CipherRepromptType.Password },
realCipher,
] as any[]);
await sut.update("https://test.com");
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(1);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
"Test Cipher (Test Username)",
"5",
"https://test.com",
realCipher
);
});
});
});

View File

@@ -0,0 +1,196 @@
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { Utils } from "@bitwarden/common/misc/utils";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../background/service_factories/auth-service.factory";
import {
cipherServiceFactory,
CipherServiceInitOptions,
} from "../background/service_factories/cipher-service.factory";
import { CachedServices } from "../background/service_factories/factory-options";
import { searchServiceFactory } from "../background/service_factories/search-service.factory";
import { Account } from "../models/account";
import { BrowserApi } from "./browserApi";
import { MainContextMenuHandler } from "./main-context-menu-handler";
const NOT_IMPLEMENTED = (..._args: unknown[]) => Promise.resolve();
const LISTENED_TO_COMMANDS = [
"loggedIn",
"unlocked",
"syncCompleted",
"bgUpdateContextMenu",
"editedCipher",
"addedCipher",
"deletedCipher",
];
export class CipherContextMenuHandler {
constructor(
private mainContextMenuHandler: MainContextMenuHandler,
private authService: AuthService,
private cipherService: CipherService
) {}
static async create(cachedServices: CachedServices) {
const stateFactory = new StateFactory(GlobalState, Account);
let searchService: SearchService | null = null;
const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = {
apiServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
cipherServiceOptions: {
searchServiceFactory: () => searchService,
},
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: false,
},
i18nServiceOptions: {
systemLanguage: chrome.i18n.getUILanguage(),
},
keyConnectorServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
logServiceOptions: {
isDev: false,
},
platformUtilsServiceOptions: {
biometricCallback: () => Promise.resolve(false),
clipboardWriteCallback: NOT_IMPLEMENTED,
win: self,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
};
searchService = await searchServiceFactory(cachedServices, serviceOptions);
return new CipherContextMenuHandler(
await MainContextMenuHandler.mv3Create(cachedServices),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions)
);
}
static async tabsOnActivatedListener(
activeInfo: chrome.tabs.TabActiveInfo,
serviceCache: CachedServices
) {
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
const tab = await BrowserApi.getTab(activeInfo.tabId);
await cipherContextMenuHandler.update(tab.url);
}
static async tabsOnReplacedListener(
addedTabId: number,
removedTabId: number,
serviceCache: CachedServices
) {
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
const tab = await BrowserApi.getTab(addedTabId);
await cipherContextMenuHandler.update(tab.url);
}
static async tabsOnUpdatedListener(
tabId: number,
changeInfo: chrome.tabs.TabChangeInfo,
tab: chrome.tabs.Tab,
serviceCache: CachedServices
) {
if (changeInfo.status !== "complete") {
return;
}
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
await cipherContextMenuHandler.update(tab.url);
}
static async messageListener(
message: { command: string },
sender: chrome.runtime.MessageSender,
cachedServices: CachedServices
) {
if (!CipherContextMenuHandler.shouldListen(message)) {
return;
}
const cipherContextMenuHandler = await CipherContextMenuHandler.create(cachedServices);
await cipherContextMenuHandler.messageListener(message);
}
private static shouldListen(message: { command: string }) {
return LISTENED_TO_COMMANDS.includes(message.command);
}
async messageListener(message: { command: string }, sender?: chrome.runtime.MessageSender) {
if (!CipherContextMenuHandler.shouldListen(message)) {
return;
}
const activeTabs = await BrowserApi.getActiveTabs();
if (!activeTabs || activeTabs.length === 0) {
return;
}
await this.update(activeTabs[0].url);
}
async update(url: string) {
const authStatus = await this.authService.getAuthStatus();
await MainContextMenuHandler.removeAll();
if (authStatus !== AuthenticationStatus.Unlocked) {
// Should I pass in the auth status or even have two seperate methods for this
// on MainContextMenuHandler
await this.mainContextMenuHandler.noAccess();
return;
}
const menuEnabled = await this.mainContextMenuHandler.init();
if (!menuEnabled) {
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(url);
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
if (ciphers.length === 0) {
await this.mainContextMenuHandler.noLogins(url);
return;
}
for (const cipher of ciphers) {
await this.updateForCipher(url, cipher);
}
}
private async updateForCipher(url: string, cipher: CipherView) {
if (
cipher == null ||
cipher.type !== CipherType.Login ||
cipher.reprompt !== CipherRepromptType.None
) {
return;
}
let title = cipher.name;
if (!Utils.isNullOrEmpty(title)) {
title += ` (${cipher.login.username})`;
}
await this.mainContextMenuHandler.loadOptions(title, cipher.id, url, cipher);
}
}

View File

@@ -0,0 +1,193 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { AutofillTabCommand } from "../commands/autofill-tab-command";
import {
CopyToClipboardAction,
ContextMenuClickedHandler,
CopyToClipboardOptions,
GeneratePasswordToClipboardAction,
} from "./context-menu-clicked-handler";
import {
AUTOFILL_ID,
COPY_PASSWORD_ID,
COPY_USERNAME_ID,
COPY_VERIFICATIONCODE_ID,
GENERATE_PASSWORD_ID,
} from "./main-context-menu-handler";
describe("ContextMenuClickedHandler", () => {
const createData = (
menuItemId: chrome.contextMenus.OnClickData["menuItemId"],
parentMenuItemId?: chrome.contextMenus.OnClickData["parentMenuItemId"]
): chrome.contextMenus.OnClickData => {
return {
menuItemId: menuItemId,
parentMenuItemId: parentMenuItemId,
editable: false,
pageUrl: "something",
};
};
const createCipher = (data?: {
id?: CipherView["id"];
username?: CipherView["login"]["username"];
password?: CipherView["login"]["password"];
totp?: CipherView["login"]["totp"];
}): CipherView => {
const { id, username, password, totp } = data || {};
const cipherView = new CipherView(
new Cipher({
id: id ?? "1",
type: CipherType.Login,
} as any)
);
cipherView.login.username = username ?? "USERNAME";
cipherView.login.password = password ?? "PASSWORD";
cipherView.login.totp = totp ?? "TOTP";
return cipherView;
};
let copyToClipboard: CopyToClipboardAction;
let generatePasswordToClipboard: GeneratePasswordToClipboardAction;
let authService: MockProxy<AuthService>;
let cipherService: MockProxy<CipherService>;
let autofillTabCommand: MockProxy<AutofillTabCommand>;
let totpService: MockProxy<TotpService>;
let eventCollectionService: MockProxy<EventCollectionService>;
let sut: ContextMenuClickedHandler;
beforeEach(() => {
copyToClipboard = jest.fn<void, [CopyToClipboardOptions]>();
generatePasswordToClipboard = jest.fn<Promise<void>, [tab: chrome.tabs.Tab]>();
authService = mock();
cipherService = mock();
autofillTabCommand = mock();
totpService = mock();
eventCollectionService = mock();
sut = new ContextMenuClickedHandler(
copyToClipboard,
generatePasswordToClipboard,
authService,
cipherService,
autofillTabCommand,
totpService,
eventCollectionService
);
});
afterEach(() => jest.resetAllMocks());
describe("run", () => {
it("can generate password", async () => {
await sut.run(createData(GENERATE_PASSWORD_ID), { id: 5 } as any);
expect(generatePasswordToClipboard).toBeCalledTimes(1);
expect(generatePasswordToClipboard).toBeCalledWith({
id: 5,
});
});
it("attempts to autofill the correct cipher", async () => {
const cipher = createCipher();
cipherService.getAllDecrypted.mockResolvedValue([cipher]);
await sut.run(createData("T_1", AUTOFILL_ID), { id: 5 } as any);
expect(autofillTabCommand.doAutofillTabWithCipherCommand).toBeCalledTimes(1);
expect(autofillTabCommand.doAutofillTabWithCipherCommand).toBeCalledWith({ id: 5 }, cipher);
});
it("copies username to clipboard", async () => {
cipherService.getAllDecrypted.mockResolvedValue([
createCipher({ username: "TEST_USERNAME" }),
]);
await sut.run(createData("T_1", COPY_USERNAME_ID));
expect(copyToClipboard).toBeCalledTimes(1);
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_USERNAME", options: undefined });
});
it("copies password to clipboard", async () => {
cipherService.getAllDecrypted.mockResolvedValue([
createCipher({ password: "TEST_PASSWORD" }),
]);
await sut.run(createData("T_1", COPY_PASSWORD_ID));
expect(copyToClipboard).toBeCalledTimes(1);
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_PASSWORD", options: undefined });
});
it("copies totp code to clipboard", async () => {
cipherService.getAllDecrypted.mockResolvedValue([createCipher({ totp: "TEST_TOTP_SEED" })]);
totpService.getCode.mockImplementation((seed) => {
if (seed === "TEST_TOTP_SEED") {
return Promise.resolve("123456");
}
return Promise.resolve("654321");
});
await sut.run(createData("T_1", COPY_VERIFICATIONCODE_ID));
expect(totpService.getCode).toHaveBeenCalledTimes(1);
expect(copyToClipboard).toHaveBeenCalledWith({ text: "123456" });
});
it("attempts to find a cipher when noop but unlocked", async () => {
cipherService.getAllDecryptedForUrl.mockResolvedValue([
{
...createCipher({ username: "NOOP_USERNAME" }),
reprompt: CipherRepromptType.None,
} as any,
]);
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
expect(copyToClipboard).toHaveBeenCalledTimes(1);
expect(copyToClipboard).toHaveBeenCalledWith({
text: "NOOP_USERNAME",
tab: { url: "https://test.com" },
});
});
it("attempts to find a cipher when noop but unlocked", async () => {
cipherService.getAllDecryptedForUrl.mockResolvedValue([
{
...createCipher({ username: "NOOP_USERNAME" }),
reprompt: CipherRepromptType.Password,
} as any,
]);
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
});
});
});

View File

@@ -0,0 +1,240 @@
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import LockedVaultPendingNotificationsItem from "../background/models/lockedVaultPendingNotificationsItem";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../background/service_factories/auth-service.factory";
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
import {
cipherServiceFactory,
CipherServiceInitOptions,
} from "../background/service_factories/cipher-service.factory";
import { eventCollectionServiceFactory } from "../background/service_factories/event-collection-service.factory";
import { CachedServices } from "../background/service_factories/factory-options";
import { passwordGenerationServiceFactory } from "../background/service_factories/password-generation-service.factory";
import { searchServiceFactory } from "../background/service_factories/search-service.factory";
import { stateServiceFactory } from "../background/service_factories/state-service.factory";
import { totpServiceFactory } from "../background/service_factories/totp-service.factory";
import { BrowserApi } from "../browser/browserApi";
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
import { AutofillTabCommand } from "../commands/autofill-tab-command";
import { Account } from "../models/account";
import {
AUTOFILL_ID,
COPY_IDENTIFIER_ID,
COPY_PASSWORD_ID,
COPY_USERNAME_ID,
COPY_VERIFICATIONCODE_ID,
GENERATE_PASSWORD_ID,
NOOP_COMMAND_SUFFIX,
} from "./main-context-menu-handler";
export type CopyToClipboardOptions = { text: string; tab: chrome.tabs.Tab };
export type CopyToClipboardAction = (options: CopyToClipboardOptions) => void;
export type GeneratePasswordToClipboardAction = (tab: chrome.tabs.Tab) => Promise<void>;
const NOT_IMPLEMENTED = (..._args: unknown[]) =>
Promise.reject<never>("This action is not implemented inside of a service worker context.");
export class ContextMenuClickedHandler {
constructor(
private copyToClipboard: CopyToClipboardAction,
private generatePasswordToClipboard: GeneratePasswordToClipboardAction,
private authService: AuthService,
private cipherService: CipherService,
private autofillTabCommand: AutofillTabCommand,
private totpService: TotpService,
private eventCollectionService: EventCollectionService
) {}
static async mv3Create(cachedServices: CachedServices) {
const stateFactory = new StateFactory(GlobalState, Account);
let searchService: SearchService | null = null;
const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = {
apiServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
cipherServiceOptions: {
searchServiceFactory: () => searchService,
},
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: false,
},
i18nServiceOptions: {
systemLanguage: chrome.i18n.getUILanguage(),
},
keyConnectorServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
logServiceOptions: {
isDev: false,
},
platformUtilsServiceOptions: {
biometricCallback: NOT_IMPLEMENTED,
clipboardWriteCallback: NOT_IMPLEMENTED,
win: self,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
};
searchService = await searchServiceFactory(cachedServices, serviceOptions);
const generatePasswordToClipboardCommand = new GeneratePasswordToClipboardCommand(
await passwordGenerationServiceFactory(cachedServices, serviceOptions),
await stateServiceFactory(cachedServices, serviceOptions)
);
return new ContextMenuClickedHandler(
(options) => copyToClipboard(options.tab, options.text),
(tab) => generatePasswordToClipboardCommand.generatePasswordToClipboard(tab),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions),
new AutofillTabCommand(await autofillServiceFactory(cachedServices, serviceOptions)),
await totpServiceFactory(cachedServices, serviceOptions),
await eventCollectionServiceFactory(cachedServices, serviceOptions)
);
}
static async onClickedListener(
info: chrome.contextMenus.OnClickData,
tab?: chrome.tabs.Tab,
cachedServices: CachedServices = {}
) {
const contextMenuClickedHandler = await ContextMenuClickedHandler.mv3Create(cachedServices);
await contextMenuClickedHandler.run(info, tab);
}
static async messageListener(
message: { command: string; data: LockedVaultPendingNotificationsItem },
sender: chrome.runtime.MessageSender,
cachedServices: CachedServices
) {
if (
message.command !== "unlockCompleted" ||
message.data.target !== "contextmenus.background"
) {
return;
}
const contextMenuClickedHandler = await ContextMenuClickedHandler.mv3Create(cachedServices);
await contextMenuClickedHandler.run(
message.data.commandToRetry.msg.data,
message.data.commandToRetry.sender.tab
);
}
async run(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
switch (info.menuItemId) {
case GENERATE_PASSWORD_ID:
if (!tab) {
return;
}
await this.generatePasswordToClipboard(tab);
break;
case COPY_IDENTIFIER_ID:
if (!tab) {
return;
}
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
break;
default:
await this.cipherAction(info, tab);
}
}
async cipherAction(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: NOOP_COMMAND_SUFFIX, data: info },
sender: { tab: tab },
},
target: "contextmenus.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
await BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
// NOTE: We don't actually use the first part of this ID, we further switch based on the parentMenuItemId
// I would really love to not add it but that is a departure from how it currently works.
const id = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
let cipher: CipherView | undefined;
if (id === NOOP_COMMAND_SUFFIX) {
// This NOOP item has come through which is generally only for no access state but since we got here
// we are actually unlocked we will do our best to find a good match of an item to autofill this is useful
// in scenarios like unlock on autofill
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
} else {
const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find((c) => c.id === id);
}
if (cipher == null) {
return;
}
switch (info.parentMenuItemId) {
case AUTOFILL_ID:
if (tab == null) {
return;
}
await this.autofillTabCommand.doAutofillTabWithCipherCommand(tab, cipher);
break;
case COPY_USERNAME_ID:
this.copyToClipboard({ text: cipher.login.username, tab: tab });
break;
case COPY_PASSWORD_ID:
this.copyToClipboard({ text: cipher.login.password, tab: tab });
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
break;
case COPY_VERIFICATIONCODE_ID:
this.copyToClipboard({ text: await this.totpService.getCode(cipher.login.totp), tab: tab });
break;
}
}
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
return new Promise<string>((resolve, reject) => {
BrowserApi.sendTabsMessage(
tab.id,
{ command: "getClickedElement" },
{ frameId: info.frameId },
(identifier: string) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(identifier);
}
);
});
}
}

View File

@@ -0,0 +1,137 @@
import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
import { MainContextMenuHandler } from "./main-context-menu-handler";
describe("context-menu", () => {
let stateService: MockProxy<BrowserStateService>;
let i18nService: MockProxy<I18nService>;
let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>;
let createSpy: jest.SpyInstance<
string | number,
[createProperties: chrome.contextMenus.CreateProperties, callback?: () => void]
>;
let sut: MainContextMenuHandler;
beforeEach(() => {
stateService = mock();
i18nService = mock();
removeAllSpy = jest
.spyOn(chrome.contextMenus, "removeAll")
.mockImplementation((callback) => callback());
createSpy = jest.spyOn(chrome.contextMenus, "create").mockImplementation((props, callback) => {
if (callback) {
callback();
}
return props.id;
});
sut = new MainContextMenuHandler(stateService, i18nService);
});
afterEach(() => jest.resetAllMocks());
describe("init", () => {
it("has menu disabled", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(true);
const createdMenu = await sut.init();
expect(createdMenu).toBeFalsy();
expect(removeAllSpy).toHaveBeenCalledTimes(1);
});
it("has menu enabled, but does not have premium", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(false);
stateService.getCanAccessPremium.mockResolvedValue(false);
const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
expect(createSpy).toHaveBeenCalledTimes(7);
});
it("has menu enabled and has premium", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(false);
stateService.getCanAccessPremium.mockResolvedValue(true);
const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
expect(createSpy).toHaveBeenCalledTimes(8);
});
});
describe("loadOptions", () => {
const createCipher = (data?: {
id?: CipherView["id"];
username?: CipherView["login"]["username"];
password?: CipherView["login"]["password"];
totp?: CipherView["login"]["totp"];
viewPassword?: CipherView["viewPassword"];
}): CipherView => {
const { id, username, password, totp, viewPassword } = data || {};
const cipherView = new CipherView(
new Cipher({
id: id ?? "1",
type: CipherType.Login,
viewPassword: viewPassword ?? true,
} as any)
);
cipherView.login.username = username ?? "USERNAME";
cipherView.login.password = password ?? "PASSWORD";
cipherView.login.totp = totp ?? "TOTP";
return cipherView;
};
it("is not a login cipher", async () => {
await sut.loadOptions("TEST_TITLE", "1", "", {
...createCipher(),
type: CipherType.SecureNote,
} as any);
expect(createSpy).not.toHaveBeenCalled();
});
it("creates item for autofill", async () => {
await sut.loadOptions(
"TEST_TITLE",
"1",
"",
createCipher({
username: "",
totp: "",
viewPassword: false,
})
);
expect(createSpy).toHaveBeenCalledTimes(1);
});
it("create entry for each cipher piece", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true);
await sut.loadOptions("TEST_TITLE", "1", "", createCipher());
// One for autofill, copy username, copy password, and copy totp code
expect(createSpy).toHaveBeenCalledTimes(4);
});
it("creates noop item for no cipher", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true);
await sut.loadOptions("TEST_TITLE", "NOOP", "");
expect(createSpy).toHaveBeenCalledTimes(4);
});
});
});

View File

@@ -0,0 +1,241 @@
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { Utils } from "@bitwarden/common/misc/utils";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CachedServices } from "../background/service_factories/factory-options";
import {
i18nServiceFactory,
I18nServiceInitOptions,
} from "../background/service_factories/i18n-service.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../background/service_factories/state-service.factory";
import { Account } from "../models/account";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
export const ROOT_ID = "root";
export const AUTOFILL_ID = "autofill";
export const COPY_USERNAME_ID = "copy-username";
export const COPY_PASSWORD_ID = "copy-password";
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
export const COPY_IDENTIFIER_ID = "copy-identifier";
const SEPARATOR_ID = "separator";
export const GENERATE_PASSWORD_ID = "generate-password";
export const NOOP_COMMAND_SUFFIX = "noop";
export class MainContextMenuHandler {
//
private initRunning = false;
create: (options: chrome.contextMenus.CreateProperties) => Promise<void>;
constructor(private stateService: BrowserStateService, private i18nService: I18nService) {
if (chrome.contextMenus) {
this.create = (options) => {
return new Promise<void>((resolve, reject) => {
chrome.contextMenus.create(options, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve();
});
});
};
} else {
this.create = (_options) => Promise.resolve();
}
}
static async mv3Create(cachedServices: CachedServices) {
const stateFactory = new StateFactory(GlobalState, Account);
const serviceOptions: StateServiceInitOptions & I18nServiceInitOptions = {
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: false,
},
i18nServiceOptions: {
systemLanguage: chrome.i18n.getUILanguage(),
},
logServiceOptions: {
isDev: false,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
};
return new MainContextMenuHandler(
await stateServiceFactory(cachedServices, serviceOptions),
await i18nServiceFactory(cachedServices, serviceOptions)
);
}
/**
*
* @returns a boolean showing whether or not items were created
*/
async init(): Promise<boolean> {
const menuDisabled = await this.stateService.getDisableContextMenuItem();
if (this.initRunning) {
return menuDisabled;
}
try {
if (menuDisabled) {
await MainContextMenuHandler.removeAll();
return false;
}
const create = async (options: Omit<chrome.contextMenus.CreateProperties, "contexts">) => {
await this.create({ ...options, contexts: ["all"] });
};
await create({
id: ROOT_ID,
title: "Bitwarden",
});
await create({
id: AUTOFILL_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFill"),
});
await create({
id: COPY_USERNAME_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyUsername"),
});
await create({
id: COPY_PASSWORD_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyPassword"),
});
if (await this.stateService.getCanAccessPremium()) {
await create({
id: COPY_VERIFICATIONCODE_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyVerificationCode"),
});
}
await create({
id: SEPARATOR_ID,
type: "separator",
parentId: ROOT_ID,
});
await create({
id: GENERATE_PASSWORD_ID,
parentId: ROOT_ID,
title: this.i18nService.t("generatePasswordCopied"),
});
await create({
id: COPY_IDENTIFIER_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyElementIdentifier"),
});
return true;
} finally {
this.initRunning = false;
}
}
static async removeAll() {
return new Promise<void>((resolve, reject) => {
chrome.contextMenus.removeAll(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve();
});
});
}
static remove(menuItemId: string) {
return new Promise<void>((resolve, reject) => {
chrome.contextMenus.remove(menuItemId, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve();
});
});
}
async loadOptions(title: string, id: string, url: string, cipher?: CipherView | undefined) {
if (cipher != null && cipher.type !== CipherType.Login) {
return;
}
const sanitizedTitle = MainContextMenuHandler.sanitizeContextMenuTitle(title);
const createChildItem = async (parent: string) => {
const menuItemId = `${parent}_${id}`;
return await this.create({
type: "normal",
id: menuItemId,
parentId: parent,
title: sanitizedTitle,
contexts: ["all"],
});
};
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.password)) {
await createChildItem(AUTOFILL_ID);
if (cipher?.viewPassword ?? true) {
await createChildItem(COPY_PASSWORD_ID);
}
}
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.username)) {
await createChildItem(COPY_USERNAME_ID);
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (canAccessPremium && (cipher == null || !Utils.isNullOrEmpty(cipher.login.totp))) {
await createChildItem(COPY_VERIFICATIONCODE_ID);
}
}
static sanitizeContextMenuTitle(title: string): string {
return title.replace(/&/g, "&&");
}
async noAccess() {
if (await this.init()) {
const authed = await this.stateService.getIsAuthenticated();
await this.loadOptions(
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
NOOP_COMMAND_SUFFIX,
"<all_urls>"
);
}
}
async noLogins(url: string) {
await this.loadOptions(this.i18nService.t("noMatchingLogins"), NOOP_COMMAND_SUFFIX, url);
}
}

View File

@@ -0,0 +1,39 @@
import { BrowserApi } from "../browser/browserApi";
import { ClearClipboard } from "./clear-clipboard";
describe("clearClipboard", () => {
describe("run", () => {
it("Does not clear clipboard when no active tabs are retrieved", async () => {
jest.spyOn(BrowserApi, "getActiveTabs").mockResolvedValue([] as any);
jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue();
await ClearClipboard.run();
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).not.toHaveBeenCalled();
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).not.toHaveBeenCalledWith(1, {
command: "clearClipboard",
});
});
it("Sends a message to the content script to clear the clipboard", async () => {
jest.spyOn(BrowserApi, "getActiveTabs").mockResolvedValue([
{
id: 1,
},
] as any);
jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue();
await ClearClipboard.run();
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1);
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, {
command: "clearClipboard",
});
});
});
});

View File

@@ -1,79 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BrowserApi } from "../browser/browserApi";
import { StateService } from "../services/abstractions/state.service";
import { ClearClipboard } from "./clear-clipboard";
import { getClearClipboardTime, setClearClipboardTime } from "./clipboard-state";
jest.mock("./clipboard-state", () => {
return {
getClearClipboardTime: jest.fn(),
setClearClipboardTime: jest.fn(),
};
});
const getClearClipboardTimeMock = getClearClipboardTime as jest.Mock;
const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock;
describe("clearClipboard", () => {
describe("run", () => {
let stateService: MockProxy<StateService>;
let serviceCache: Record<string, unknown>;
beforeEach(() => {
stateService = mock<StateService>();
serviceCache = {
stateService: stateService,
};
});
afterEach(() => {
jest.resetAllMocks();
});
it("has a clear time that is past execution time", async () => {
const executionTime = new Date(2022, 1, 1, 12);
const clearTime = new Date(2022, 1, 1, 12, 1);
jest.spyOn(BrowserApi, "getActiveTabs").mockResolvedValue([
{
id: 1,
},
] as any);
jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue();
getClearClipboardTimeMock.mockResolvedValue(clearTime.getTime());
await ClearClipboard.run(executionTime, serviceCache);
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1);
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, {
command: "clearClipboard",
});
});
it("has a clear time before execution time", async () => {
const executionTime = new Date(2022, 1, 1, 12);
const clearTime = new Date(2022, 1, 1, 11);
setClearClipboardTimeMock.mockResolvedValue(clearTime.getTime());
await ClearClipboard.run(executionTime, serviceCache);
expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled();
});
it("has an undefined clearTime", async () => {
const executionTime = new Date(2022, 1, 1);
getClearClipboardTimeMock.mockResolvedValue(undefined);
await ClearClipboard.run(executionTime, serviceCache);
expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,43 +1,15 @@
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { stateServiceFactory } from "../background/service_factories/state-service.factory";
import { BrowserApi } from "../browser/browserApi";
import { Account } from "../models/account";
import { getClearClipboardTime } from "./clipboard-state";
export const clearClipboardAlarmName = "clearClipboard";
export class ClearClipboard {
static async run(executionTime: Date, serviceCache: Record<string, unknown>) {
const stateFactory = new StateFactory(GlobalState, Account);
const stateService = await stateServiceFactory(serviceCache, {
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: false,
},
logServiceOptions: {
isDev: false,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
});
const clearClipboardTime = await getClearClipboardTime(stateService);
if (!clearClipboardTime) {
return;
}
if (clearClipboardTime < executionTime.getTime()) {
return;
}
/**
We currently rely on an active tab with an injected content script (`../content/misc-utils.ts`) to clear the clipboard via `window.navigator.clipboard.writeText(text)`
With https://bugs.chromium.org/p/chromium/issues/detail?id=1160302 it was said that service workers,
would have access to the clipboard api and then we could migrate to a simpler solution
*/
static async run() {
const activeTabs = await BrowserApi.getActiveTabs();
if (!activeTabs || activeTabs.length === 0) {
return;

View File

@@ -1,10 +0,0 @@
import { StateService } from "../services/abstractions/state.service";
const clearClipboardStorageKey = "clearClipboardTime";
export const getClearClipboardTime = async (stateService: StateService) => {
return await stateService.getFromSessionMemory<number>(clearClipboardStorageKey);
};
export const setClearClipboardTime = async (stateService: StateService, time: number) => {
await stateService.setInSessionMemory(clearClipboardStorageKey, time);
};

View File

@@ -2,30 +2,30 @@ import { mock, MockProxy } from "jest-mock-extended";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { setAlarmTime } from "../alarms/alarm-state";
import { BrowserApi } from "../browser/browserApi";
import { StateService } from "../services/abstractions/state.service";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
import { setClearClipboardTime } from "./clipboard-state";
import { clearClipboardAlarmName } from "./clear-clipboard";
import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command";
jest.mock("./clipboard-state", () => {
jest.mock("../alarms/alarm-state", () => {
return {
getClearClipboardTime: jest.fn(),
setClearClipboardTime: jest.fn(),
setAlarmTime: jest.fn(),
};
});
const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock;
const setAlarmTimeMock = setAlarmTime as jest.Mock;
describe("GeneratePasswordToClipboardCommand", () => {
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let stateService: MockProxy<StateService>;
let stateService: MockProxy<BrowserStateService>;
let sut: GeneratePasswordToClipboardCommand;
beforeEach(() => {
passwordGenerationService = mock<PasswordGenerationService>();
stateService = mock<StateService>();
stateService = mock<BrowserStateService>();
passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]);
@@ -53,9 +53,9 @@ describe("GeneratePasswordToClipboardCommand", () => {
text: "PASSWORD",
});
expect(setClearClipboardTimeMock).toHaveBeenCalledTimes(1);
expect(setAlarmTimeMock).toHaveBeenCalledTimes(1);
expect(setClearClipboardTimeMock).toHaveBeenCalledWith(stateService, expect.any(Number));
expect(setAlarmTimeMock).toHaveBeenCalledWith(clearClipboardAlarmName, expect.any(Number));
});
it("does not have clear clipboard value", async () => {
@@ -70,7 +70,7 @@ describe("GeneratePasswordToClipboardCommand", () => {
text: "PASSWORD",
});
expect(setClearClipboardTimeMock).not.toHaveBeenCalled();
expect(setAlarmTimeMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,14 +1,15 @@
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { StateService } from "../services/abstractions/state.service";
import { setAlarmTime } from "../alarms/alarm-state";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
import { setClearClipboardTime } from "./clipboard-state";
import { clearClipboardAlarmName } from "./clear-clipboard";
import { copyToClipboard } from "./copy-to-clipboard-command";
export class GeneratePasswordToClipboardCommand {
constructor(
private passwordGenerationService: PasswordGenerationService,
private stateService: StateService
private stateService: BrowserStateService
) {}
async generatePasswordToClipboard(tab: chrome.tabs.Tab) {
@@ -20,7 +21,7 @@ export class GeneratePasswordToClipboardCommand {
const clearClipboard = await this.stateService.getClearClipboard();
if (clearClipboard != null) {
await setClearClipboardTime(this.stateService, Date.now() + clearClipboard * 1000);
await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000);
}
}
}

View File

@@ -1,10 +1,12 @@
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import AutofillPageDetails from "../models/autofillPageDetails";
import { AutofillService } from "../services/abstractions/autofill.service";
export class AutoFillActiveTabCommand {
export class AutofillTabCommand {
constructor(private autofillService: AutofillService) {}
async doAutoFillActiveTabCommand(tab: chrome.tabs.Tab) {
async doAutofillTabCommand(tab: chrome.tabs.Tab) {
if (!tab.id) {
throw new Error("Tab does not have an id, cannot complete autofill.");
}
@@ -23,6 +25,30 @@ export class AutoFillActiveTabCommand {
);
}
async doAutofillTabWithCipherCommand(tab: chrome.tabs.Tab, cipher: CipherView) {
if (!tab.id) {
throw new Error("Tab does not have an id, cannot complete autofill.");
}
const details = await this.collectPageDetails(tab.id);
await this.autofillService.doAutoFill({
tab: tab,
cipher: cipher,
pageDetails: [
{
frameId: 0,
tab: tab,
details: details,
},
],
skipLastUsed: false,
skipUsernameOnlyFill: false,
onlyEmptyFields: false,
onlyVisibleFields: false,
fillNewPassword: true,
});
}
private async collectPageDetails(tabId: number): Promise<AutofillPageDetails> {
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(

View File

@@ -42,6 +42,8 @@
9. Add new handler, for new command that responds with page details in response callback
10. Handle sandbox iframe and sandbox rule in CSP
11. Work on array of saved urls instead of just one to determine if we should autofill non-https sites
12. Remove setting of attribute com.browser.browser.userEdited on user-inputs
13. Handle null value URLs in urlNotSecure
*/
function collect(document, undefined) {
@@ -50,11 +52,6 @@
// END MODIFICATION
document.elementsByOPID = {};
document.addEventListener('input', function (inputevent) {
inputevent.a !== false &&
inputevent.target.tagName.toLowerCase() === 'input' &&
(inputevent.target.dataset['com.bitwarden.browser.userEdited'] = 'yes');
}, true);
function getPageDetails(theDoc, oneShotId) {
// start helpers
@@ -279,8 +276,6 @@
addProp(field, 'title', getElementAttrValue(el, 'title'));
// START MODIFICATION
addProp(field, 'userEdited', !!el.dataset['com.browser.browser.userEdited']);
var elTagName = el.tagName.toLowerCase();
addProp(field, 'tagName', elTagName);
@@ -638,7 +633,7 @@
return false;
}
return savedURLs.some(url => url.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = document.querySelectorAll('input[type=password]'),
return savedURLs.some(url => url?.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = document.querySelectorAll('input[type=password]'),
0 < passwordInputs.length && (confirmResult = confirm('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.\n\nDo you still wish to fill this login?'),
0 == confirmResult)) ? true : false;
}

View File

@@ -54,9 +54,12 @@ document.addEventListener("contextmenu", (event) => {
});
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
chrome.runtime.onMessage.addListener((event) => {
chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => {
if (event.command === "getClickedElement") {
const identifier = getClickedElementIdentifier();
if (sendResponse) {
sendResponse(identifier);
}
chrome.runtime.sendMessage({
command: "getClickedElementResponse",
sender: "contextMenuHandler",

View File

@@ -1,6 +1,9 @@
import { BehaviorSubject } from "rxjs";
import { StateService } from "../../services/state.service";
import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserStateService } from "../../services/browser-state.service";
import { browserSession } from "./browser-session.decorator";
import { SessionStorable } from "./session-storable";
@@ -11,36 +14,55 @@ import { sessionSync } from "./session-sync.decorator";
jest.mock("./session-syncer");
describe("browserSession decorator", () => {
it("should throw if StateService is not a constructor argument", () => {
it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => {
@browserSession
class TestClass {}
expect(() => {
new TestClass();
}).toThrowError(
"Cannot decorate TestClass with browserSession, Browser's StateService must be injected"
"Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters"
);
});
it("should create if StateService is a constructor argument", () => {
const stateService = Object.create(StateService.prototype, {});
const stateService = Object.create(BrowserStateService.prototype, {
memoryStorageService: {
value: Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
}),
},
});
@browserSession
class TestClass {
constructor(private stateService: StateService) {}
constructor(private stateService: BrowserStateService) {}
}
expect(new TestClass(stateService)).toBeDefined();
});
it("should create if MemoryStorageService is a constructor argument", () => {
const memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
@browserSession
class TestClass {
constructor(private memoryStorageService: AbstractMemoryStorageService) {}
}
expect(new TestClass(memoryStorageService)).toBeDefined();
});
describe("interaction with @sessionSync decorator", () => {
let stateService: StateService;
let memoryStorageService: MemoryStorageService;
@browserSession
class TestClass {
@sessionSync({ initializer: (s: string) => s })
private behaviorSubject = new BehaviorSubject("");
constructor(private stateService: StateService) {}
constructor(private memoryStorageService: MemoryStorageService) {}
fromJSON(json: any) {
this.behaviorSubject.next(json);
@@ -48,16 +70,18 @@ describe("browserSession decorator", () => {
}
beforeEach(() => {
stateService = Object.create(StateService.prototype, {}) as StateService;
memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
});
it("should create a session syncer", () => {
const testClass = new TestClass(stateService) as any as SessionStorable;
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers.length).toEqual(1);
});
it("should initialize the session syncer", () => {
const testClass = new TestClass(stateService) as any as SessionStorable;
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
import { Constructor } from "type-fest";
import { StateService } from "../../services/state.service";
import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SessionStorable } from "./session-storable";
import { SessionSyncer } from "./session-syncer";
@@ -22,26 +22,51 @@ export function browserSession<TCtor extends Constructor<any>>(constructor: TCto
super(...args);
// Require state service to be injected
const stateService = args.find((arg) => arg instanceof StateService);
if (!stateService) {
throw new Error(
`Cannot decorate ${constructor.name} with browserSession, Browser's StateService must be injected`
);
}
const storageService: AbstractMemoryStorageService = this.findStorageService(
[this as any].concat(args)
);
if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) {
return;
}
this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) =>
this.buildSyncer(metadata, stateService)
this.buildSyncer(metadata, storageService)
);
}
buildSyncer(metadata: SyncedItemMetadata, stateService: StateService) {
const syncer = new SessionSyncer((this as any)[metadata.propertyKey], stateService, metadata);
buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) {
const syncer = new SessionSyncer(
(this as any)[metadata.propertyKey],
storageSerice,
metadata
);
syncer.init();
return syncer;
}
findStorageService(args: any[]): AbstractMemoryStorageService {
const storageService = args.find(this.isMemoryStorageService);
if (storageService) {
return storageService;
}
const stateService = args.find(
(arg) =>
arg?.memoryStorageService != null && this.isMemoryStorageService(arg.memoryStorageService)
);
if (stateService) {
return stateService.memoryStorageService;
}
throw new Error(
`Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`
);
}
isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService {
return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE;
}
};
}

View File

@@ -8,9 +8,12 @@ describe("sessionSync decorator", () => {
class TestClass {
@sessionSync({ ctor: ctor, initializer: initializer })
private testProperty = new BehaviorSubject("");
@sessionSync({ ctor: ctor, initializer: initializer, initializeAs: "array" })
private secondTestProperty = new BehaviorSubject("");
complete() {
this.testProperty.complete();
this.secondTestProperty.complete();
}
}
@@ -19,11 +22,40 @@ describe("sessionSync decorator", () => {
expect((testClass as any).__syncedItemMetadata).toEqual([
expect.objectContaining({
propertyKey: "testProperty",
sessionKey: "TestClass_testProperty",
sessionKey: "testProperty_0",
ctor: ctor,
initializer: initializer,
}),
testClass.complete(),
expect.objectContaining({
propertyKey: "secondTestProperty",
sessionKey: "secondTestProperty_1",
ctor: ctor,
initializer: initializer,
initializeAs: "array",
}),
]);
testClass.complete();
});
class TestClass2 {
@sessionSync({ ctor: ctor, initializer: initializer })
private testProperty = new BehaviorSubject("");
complete() {
this.testProperty.complete();
}
}
it("should maintain sessionKey index count for other test classes", () => {
const testClass = new TestClass2();
expect((testClass as any).__syncedItemMetadata).toEqual([
expect.objectContaining({
propertyKey: "testProperty",
sessionKey: "testProperty_2",
ctor: ctor,
initializer: initializer,
}),
]);
testClass.complete();
});
});

View File

@@ -1,13 +1,17 @@
import { Jsonify } from "type-fest";
import { SessionStorable } from "./session-storable";
import { InitializeOptions } from "./sync-item-metadata";
class BuildOptions<T> {
class BuildOptions<T, TJson = Jsonify<T>> {
ctor?: new () => T;
initializer?: (keyValuePair: Jsonify<T>) => T;
initializeAsArray? = false;
initializer?: (keyValuePair: TJson) => T;
initializeAs?: InitializeOptions;
}
// Used to ensure uniqueness for each synced observable
let index = 0;
/**
* A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts.
*
@@ -20,10 +24,10 @@ class BuildOptions<T> {
* @param buildOptions
* Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an
* initializer function that takes a key value pair representation of the BehaviorSubject data
* and returns your instantiated BehaviorSubject value. `initializeAsArray can optionally be used to indicate
* and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate
* the provided initializer function should be used to build an array of values. For example,
* ```ts
* \@sessionSync({ initializer: Foo.fromJSON, initializeAsArray: true })
* \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' })
* ```
* is equivalent to
* ```
@@ -43,10 +47,10 @@ export function sessionSync<T>(buildOptions: BuildOptions<T>) {
p.__syncedItemMetadata.push({
propertyKey,
sessionKey: `${prototype.constructor.name}_${propertyKey}`,
sessionKey: `${propertyKey}_${index++}`,
ctor: buildOptions.ctor,
initializer: buildOptions.initializer,
initializeAsArray: buildOptions.initializeAsArray,
initializeAs: buildOptions.initializeAs ?? "object",
});
};
}

View File

@@ -1,8 +1,10 @@
import { awaitAsync } from "@bitwarden/angular/../test-utils";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, ReplaySubject } from "rxjs";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserApi } from "../../browser/browserApi";
import { StateService } from "../../services/abstractions/state.service";
import { SessionSyncer } from "./session-syncer";
import { SyncedItemMetadata } from "./sync-item-metadata";
@@ -10,8 +12,13 @@ import { SyncedItemMetadata } from "./sync-item-metadata";
describe("session syncer", () => {
const propertyKey = "behaviorSubject";
const sessionKey = "Test__" + propertyKey;
const metaData = { propertyKey, sessionKey, initializer: (s: string) => s };
let stateService: MockProxy<StateService>;
const metaData: SyncedItemMetadata = {
propertyKey,
sessionKey,
initializer: (s: string) => s,
initializeAs: "object",
};
let storageService: MockProxy<MemoryStorageService>;
let sut: SessionSyncer;
let behaviorSubject: BehaviorSubject<string>;
@@ -23,8 +30,9 @@ describe("session syncer", () => {
manifest_version: 3,
});
stateService = mock<StateService>();
sut = new SessionSyncer(behaviorSubject, stateService, metaData);
storageService = mock();
storageService.has.mockResolvedValue(false);
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
});
afterEach(() => {
@@ -34,53 +42,87 @@ describe("session syncer", () => {
});
describe("constructor", () => {
it("should throw if behaviorSubject is not an instance of BehaviorSubject", () => {
it("should throw if subject is not an instance of Subject", () => {
expect(() => {
new SessionSyncer({} as any, stateService, null);
}).toThrowError("behaviorSubject must be an instance of BehaviorSubject");
new SessionSyncer({} as any, storageService, null);
}).toThrowError("subject must inherit from Subject");
});
it("should create if either ctor or initializer is provided", () => {
expect(
new SessionSyncer(behaviorSubject, stateService, { propertyKey, sessionKey, ctor: String })
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
ctor: String,
initializeAs: "object",
})
).toBeDefined();
expect(
new SessionSyncer(behaviorSubject, stateService, {
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
initializer: (s: any) => s,
initializeAs: "object",
})
).toBeDefined();
});
it("should throw if neither ctor or initializer is provided", () => {
expect(() => {
new SessionSyncer(behaviorSubject, stateService, { propertyKey, sessionKey });
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
initializeAs: "object",
});
}).toThrowError("ctor or initializer must be provided");
});
});
describe("manifest v2 init", () => {
let observeSpy: jest.SpyInstance;
let listenForUpdatesSpy: jest.SpyInstance;
beforeEach(() => {
observeSpy = jest.spyOn(behaviorSubject, "subscribe").mockReturnThis();
listenForUpdatesSpy = jest.spyOn(BrowserApi, "messageListener").mockReturnValue();
jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({
name: "bitwarden-test",
version: "0.0.0",
manifest_version: 2,
});
describe("init", () => {
it("should ignore all updates currently in a ReplaySubject's buffer", () => {
const replaySubject = new ReplaySubject<string>(Infinity);
replaySubject.next("1");
replaySubject.next("2");
replaySubject.next("3");
sut = new SessionSyncer(replaySubject, storageService, metaData);
// block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation();
sut.init();
expect(sut["ignoreNUpdates"]).toBe(3);
});
it("should not start observing", () => {
expect(observeSpy).not.toHaveBeenCalled();
it("should ignore BehaviorSubject's initial value", () => {
const behaviorSubject = new BehaviorSubject<string>("initial");
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
// block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation();
sut.init();
expect(sut["ignoreNUpdates"]).toBe(1);
});
it("should not start listening", () => {
expect(listenForUpdatesSpy).not.toHaveBeenCalled();
it("should grab an initial value from storage if it exists", async () => {
storageService.has.mockResolvedValue(true);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
sut.init();
await awaitAsync();
expect(updateSpy).toHaveBeenCalledWith();
});
it("should not grab an initial value from storage if it does not exist", async () => {
storageService.has.mockResolvedValue(false);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
sut.init();
await awaitAsync();
expect(updateSpy).not.toHaveBeenCalled();
});
});
@@ -98,8 +140,8 @@ describe("session syncer", () => {
it("should update the session memory", async () => {
// await finishing of fire-and-forget operation
await new Promise((resolve) => setTimeout(resolve, 100));
expect(stateService.setInSessionMemory).toHaveBeenCalledTimes(1);
expect(stateService.setInSessionMemory).toHaveBeenCalledWith(sessionKey, "test");
expect(storageService.save).toHaveBeenCalledTimes(1);
expect(storageService.save).toHaveBeenCalledWith(sessionKey, "test");
});
it("should update sessionSyncers in other contexts", async () => {
@@ -129,26 +171,29 @@ describe("session syncer", () => {
it("should ignore messages with the wrong command", async () => {
await sut.updateFromMessage({ command: "wrong_command", id: sut.id });
expect(stateService.getFromSessionMemory).not.toHaveBeenCalled();
expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled();
});
it("should ignore messages from itself", async () => {
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id });
expect(stateService.getFromSessionMemory).not.toHaveBeenCalled();
expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled();
});
it("should update from message on emit from another instance", async () => {
const builder = jest.fn();
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
stateService.getFromSessionMemory.mockResolvedValue("test");
storageService.getBypassCache.mockResolvedValue("test");
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" });
await awaitAsync();
expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1);
expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder);
expect(storageService.getBypassCache).toHaveBeenCalledTimes(1);
expect(storageService.getBypassCache).toHaveBeenCalledWith(sessionKey, {
deserializer: builder,
});
expect(nextSpy).toHaveBeenCalledTimes(1);
expect(nextSpy).toHaveBeenCalledWith("test");

View File

@@ -1,9 +1,9 @@
import { BehaviorSubject, concatMap, Subscription } from "rxjs";
import { BehaviorSubject, concatMap, ReplaySubject, Subject, Subscription } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { BrowserApi } from "../../browser/browserApi";
import { StateService } from "../../services/abstractions/state.service";
import { SyncedItemMetadata } from "./sync-item-metadata";
@@ -11,16 +11,16 @@ export class SessionSyncer {
subscription: Subscription;
id = Utils.newGuid();
// everyone gets the same initial values
private ignoreNextUpdate = true;
// ignore initial values
private ignoreNUpdates = 0;
constructor(
private behaviorSubject: BehaviorSubject<any>,
private stateService: StateService,
private subject: Subject<any>,
private memoryStorageService: AbstractMemoryStorageService,
private metaData: SyncedItemMetadata
) {
if (!(behaviorSubject instanceof BehaviorSubject)) {
throw new Error("behaviorSubject must be an instance of BehaviorSubject");
if (!(subject instanceof Subject)) {
throw new Error("subject must inherit from Subject");
}
if (metaData.ctor == null && metaData.initializer == null) {
@@ -29,11 +29,26 @@ export class SessionSyncer {
}
init() {
if (BrowserApi.manifestVersion !== 3) {
return;
switch (this.subject.constructor) {
case ReplaySubject:
// ignore all updates currently in the buffer
this.ignoreNUpdates = (this.subject as any)._buffer.length;
break;
case BehaviorSubject:
this.ignoreNUpdates = 1;
break;
default:
break;
}
this.observe();
// must be synchronous
this.memoryStorageService.has(this.metaData.sessionKey).then((hasInSessionMemory) => {
if (hasInSessionMemory) {
this.update();
}
});
this.listenForUpdates();
}
@@ -41,11 +56,11 @@ export class SessionSyncer {
// This may be a memory leak.
// There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary
// contexts. If so, this is handled by destruction of the context.
this.subscription = this.behaviorSubject
this.subscription = this.subject
.pipe(
concatMap(async (next) => {
if (this.ignoreNextUpdate) {
this.ignoreNextUpdate = false;
if (this.ignoreNUpdates > 0) {
this.ignoreNUpdates -= 1;
return;
}
await this.updateSession(next);
@@ -66,14 +81,20 @@ export class SessionSyncer {
if (message.command != this.updateMessageCommand || message.id === this.id) {
return;
}
this.update();
}
async update() {
const builder = SyncedItemMetadata.builder(this.metaData);
const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder);
this.ignoreNextUpdate = true;
this.behaviorSubject.next(value);
const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey, {
deserializer: builder,
});
this.ignoreNUpdates = 1;
this.subject.next(value);
}
private async updateSession(value: any) {
await this.stateService.setInSessionMemory(this.metaData.sessionKey, value);
await this.memoryStorageService.save(this.metaData.sessionKey, value);
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id });
}

View File

@@ -1,17 +1,27 @@
export type InitializeOptions = "array" | "record" | "object";
export class SyncedItemMetadata {
propertyKey: string;
sessionKey: string;
ctor?: new () => any;
initializer?: (keyValuePair: any) => any;
initializeAsArray?: boolean;
initializeAs: InitializeOptions;
static builder(metadata: SyncedItemMetadata): (o: any) => any {
const itemBuilder =
metadata.initializer != null
? metadata.initializer
: (o: any) => Object.assign(new metadata.ctor(), o);
if (metadata.initializeAsArray) {
if (metadata.initializeAs === "array") {
return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o));
} else if (metadata.initializeAs === "record") {
return (keyValuePair: any) => {
const record: Record<any, any> = {};
for (const key in keyValuePair) {
record[key] = itemBuilder(keyValuePair[key]);
}
return record;
};
} else {
return (keyValuePair: any) => itemBuilder(keyValuePair);
}

View File

@@ -8,32 +8,60 @@ describe("builder", () => {
const ctor = TestClass;
it("should use initializer if provided", () => {
const metadata = { propertyKey, sessionKey: key, initializer };
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer,
initializeAs: "object",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({})).toBe("used initializer");
});
it("should use ctor if initializer is not provided", () => {
const metadata = { propertyKey, sessionKey: key, ctor };
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
ctor,
initializeAs: "object",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({})).toBeInstanceOf(TestClass);
});
it("should prefer initializer over ctor", () => {
const metadata = { propertyKey, sessionKey: key, ctor, initializer };
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
ctor,
initializer,
initializeAs: "object",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({})).toBe("used initializer");
});
it("should honor initialize as array", () => {
const metadata = {
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer: initializer,
initializeAsArray: true,
initializeAs: "array",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder([{}])).toBeInstanceOf(Array);
expect(builder([{}])[0]).toBe("used initializer");
});
it("should honor initialize as record", () => {
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer: initializer,
initializeAs: "record",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({ key: "" })).toBeInstanceOf(Object);
expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" });
});
});

View File

@@ -0,0 +1,27 @@
import { combine } from "./combine";
describe("combine", () => {
it("runs", async () => {
const combined = combine([
(arg: Record<string, unknown>, serviceCache: Record<string, unknown>) => {
arg["one"] = true;
serviceCache["one"] = true;
return Promise.resolve();
},
(arg: Record<string, unknown>, serviceCache: Record<string, unknown>) => {
if (serviceCache["one"] !== true) {
throw new Error("One should have ran.");
}
arg["two"] = true;
return Promise.resolve();
},
]);
const arg: Record<string, unknown> = {};
await combined(arg);
expect(arg["one"]).toBeTruthy();
expect(arg["two"]).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { CachedServices } from "../background/service_factories/factory-options";
type Listener<T extends unknown[]> = (...args: [...T, CachedServices]) => Promise<void>;
export const combine = <T extends unknown[]>(
listeners: Listener<T>[],
startingServices: CachedServices = {}
) => {
return async (...args: T) => {
const cachedServices = { ...startingServices };
for (const listener of listeners) {
await listener(...[...args, cachedServices]);
}
};
};

View File

@@ -0,0 +1,43 @@
import { CipherContextMenuHandler } from "../browser/cipher-context-menu-handler";
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
import { combine } from "./combine";
import { onCommandListener } from "./onCommandListener";
import { onInstallListener } from "./onInstallListener";
import { UpdateBadge } from "./update-badge";
const tabsOnActivatedListener = combine([
UpdateBadge.tabsOnActivatedListener,
CipherContextMenuHandler.tabsOnActivatedListener,
]);
const tabsOnReplacedListener = combine([
UpdateBadge.tabsOnReplacedListener,
CipherContextMenuHandler.tabsOnReplacedListener,
]);
const tabsOnUpdatedListener = combine([
UpdateBadge.tabsOnUpdatedListener,
CipherContextMenuHandler.tabsOnUpdatedListener,
]);
const contextMenusClickedListener = ContextMenuClickedHandler.onClickedListener;
// TODO: All message listeners should be RuntimeMessage in Notifications follow up then this type annotation can be inferred
const runtimeMessageListener = combine<
[message: { command: string }, sender: chrome.runtime.MessageSender]
>([
UpdateBadge.messageListener,
CipherContextMenuHandler.messageListener,
ContextMenuClickedHandler.messageListener,
]);
export {
tabsOnActivatedListener,
tabsOnReplacedListener,
tabsOnUpdatedListener,
contextMenusClickedListener,
runtimeMessageListener,
onCommandListener,
onInstallListener,
};

View File

@@ -14,7 +14,7 @@ import {
import { stateServiceFactory } from "../background/service_factories/state-service.factory";
import { BrowserApi } from "../browser/browserApi";
import { GeneratePasswordToClipboardCommand } from "../clipboard";
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
import { AutofillTabCommand } from "../commands/autofill-tab-command";
import { Account } from "../models/account";
export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => {
@@ -75,8 +75,8 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
return;
}
const command = new AutoFillActiveTabCommand(autofillService);
await command.doAutoFillActiveTabCommand(tab);
const command = new AutofillTabCommand(autofillService);
await command.doAutofillTabCommand(tab);
};
const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise<void> => {

Some files were not shown because too many files have changed in this diff Show More