diff --git a/.eslintrc.json b/.eslintrc.json index 6a5fc5b83f3..6bcea9dd045 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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/**/*"] }] - } } ] } diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index e1c74a70943..af82f6b811a 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -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 @@ -112,12 +108,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 +152,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 +166,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 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 0a5746ce682..e54dc936eb4 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -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: @@ -183,7 +183,7 @@ jobs: name: dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-opera-mv3.zip if-no-files-found: error - + - name: Upload Chrome artifact uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0 with: diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e32dbedb24c..56ad1fac9fc 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -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 diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index be69b22a8a5..f92ae8a71e4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -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 diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index f7073ec3c62..b0bd282de6f 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -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: diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index a57b600d0ec..fb456de1951 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -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 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 80c3b71c15d..a4aceac962b 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -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 }} diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index d60fa700617..363fe134eee 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -54,20 +54,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 +85,7 @@ jobs: echo echo "If you want to pull a staged release because it hasn’t 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 +104,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 +121,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 diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 3e5b5fc3c2b..7dee24e9202 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -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" diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 80d54eef145..f7ba508c6c3 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -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' }} diff --git a/.storybook/main.js b/.storybook/main.js index 7c0d2d97a15..83fe7fef3dd 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -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", diff --git a/README.md b/README.md index c426251905b..c04fbf66062 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Github Workflow web build on master - + gitter chat @@ -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: diff --git a/apps/browser/README.md b/apps/browser/README.md index 3ec5e544547..2cd1615a112 100644 --- a/apps/browser/README.md +++ b/apps/browser/README.md @@ -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. diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6e4208b13f0..248608de651 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -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,13 +571,13 @@ "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." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 1ce19d5f377..655d9e3d58e 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -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": "Fráze otisku prstu vašeho účtu", + "message": "Fráze otisku vašeho účtu", "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": { @@ -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", @@ -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", diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 293c6e0559c..810efccc664 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -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" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 75624fd4f56..bc362924d83 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1485,7 +1485,7 @@ "message": "Uusi pääsalasanasi ei täytä käytännön määrittämiä vaatimuksia." }, "acceptPolicies": { - "message": "Valitsemalla tämän ruudun hyväksyt seuraavat:" + "message": "Valitsemalla tämän hyväksyt seuraavat:" }, "acceptPoliciesRequired": { "message": "Palveluehtoja ja tietosuojakäytäntöä ei ole vahvistettu." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index c2d133c853f..3f273ce1d41 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -577,16 +577,16 @@ "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." }, "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 le remplissage 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 le remplissage 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", @@ -816,7 +816,7 @@ "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" @@ -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" @@ -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,10 +2030,10 @@ } }, "loginWithMasterPassword": { - "message": "Log in with master password" + "message": "Connectez-vous avec le mot de passe maître" }, "loggingInAs": { - "message": "Logging in as" + "message": "Connexion en tant que" }, "notYou": { "message": "Ce n'est pas vous ?" @@ -2042,6 +2042,6 @@ "message": "Êtes-vous nouveau ici ?" }, "rememberEmail": { - "message": "Remember email" + "message": "Se souvenir de l'e-mail" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 5682bb08e91..8b263c097c5 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2042,6 +2042,6 @@ "message": "Jesteś tu nowy(a)?" }, "rememberEmail": { - "message": "Zapamiętaj email" + "message": "Zapamiętaj adres e-mail" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 7b20d2151d3..39c7c700972 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -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": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 746ec5540a9..35a1aa7a8fe 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -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" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 37020aa2a7e..fc595643ba0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -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", diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 52062854b6d..d13d2f63b80 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -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:" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index b05d505d21d..b4e2bd0304f 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -2039,7 +2039,7 @@ "message": "不是你?" }, "newAroundHere": { - "message": "新建在这里?" + "message": "初来乍到吗?" }, "rememberEmail": { "message": "记住电子邮件地址" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index a4ff34345c1..0fcb389a61f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -2039,7 +2039,7 @@ "message": "不是您嗎?" }, "newAroundHere": { - "message": "New around here?" + "message": "第一次使用?" }, "rememberEmail": { "message": "記住電子郵件地址" diff --git a/apps/browser/src/alarms/alarm-state.ts b/apps/browser/src/alarms/alarm-state.ts new file mode 100644 index 00000000000..774bcfe1c03 --- /dev/null +++ b/apps/browser/src/alarms/alarm-state.ts @@ -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} null or Unix epoch timestamp when the alarm action is supposed to execute + * @example + * // getAlarmTime(clearClipboard) + */ +export async function getAlarmTime(commandName: AlarmKeys): Promise { + 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 { + 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 { + await setAlarmTimeInternal(commandName, null); +} + +async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise { + if (BrowserApi.manifestVersion == 3) { + await chrome.storage.session.set({ [commandName]: time }); + } else { + alarmState[commandName] = time; + } +} diff --git a/apps/browser/src/alarms/on-alarm-listener.ts b/apps/browser/src/alarms/on-alarm-listener.ts new file mode 100644 index 00000000000..4b246908778 --- /dev/null +++ b/apps/browser/src/alarms/on-alarm-listener.ts @@ -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: + } + }); +}; diff --git a/apps/browser/src/alarms/register-alarms.ts b/apps/browser/src/alarms/register-alarms.ts new file mode 100644 index 00000000000..c4da2fc27bc --- /dev/null +++ b/apps/browser/src/alarms/register-alarms.ts @@ -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; + } +} diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index 504b5afb5c0..fc020b19aa3 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,6 +1,7 @@ +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"; @@ -9,13 +10,12 @@ const manifestV3MessageListeners: (( serviceCache: Record, message: { command: string } ) => void | Promise)[] = [UpdateBadge.messageListener]; -type AlarmAction = (executionTime: Date, serviceCache: Record) => void; - -const AlarmActions: AlarmAction[] = [ClearClipboard.run]; if (BrowserApi.manifestVersion === 3) { chrome.commands.onCommand.addListener(onCommandListener); chrome.runtime.onInstalled.addListener(onInstallListener); + chrome.alarms.onAlarm.addListener(onAlarmListener); + registerAlarms(); chrome.tabs.onActivated.addListener(UpdateBadge.tabsOnActivatedListener); chrome.tabs.onReplaced.addListener(UpdateBadge.tabsOnReplacedListener); chrome.tabs.onUpdated.addListener(UpdateBadge.tabsOnUpdatedListener); @@ -26,14 +26,6 @@ if (BrowserApi.manifestVersion === 3) { listener(serviceCache, message); }); }); - chrome.alarms.onAlarm.addListener((_alarm) => { - const executionTime = new Date(); - const serviceCache = {}; - - for (const alarmAction of AlarmActions) { - alarmAction(executionTime, serviceCache); - } - }); } else { const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); bitwardenMain.bootstrap().then(() => { diff --git a/apps/browser/src/background/contextMenus.background.ts b/apps/browser/src/background/contextMenus.background.ts index 76fbfd78a97..98223280a23 100644 --- a/apps/browser/src/background/contextMenus.background.ts +++ b/apps/browser/src/background/contextMenus.background.ts @@ -1,6 +1,6 @@ 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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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"; @@ -24,7 +24,7 @@ export default class ContextMenusBackground { private passwordGenerationService: PasswordGenerationService, private platformUtilsService: PlatformUtilsService, private authService: AuthService, - private eventService: EventService, + private eventCollectionService: EventCollectionService, private totpService: TotpService ) { this.contextMenus = chrome.contextMenus; @@ -124,7 +124,7 @@ export default class ContextMenusBackground { 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); + this.eventCollectionService.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 }); diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 56e2e2dd7e1..1a8c5ae5c50 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -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); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index cc973a8c826..bd70ef57c98 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -7,7 +7,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 { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/abstractions/fileUpload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction"; @@ -17,7 +18,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"; @@ -54,21 +55,19 @@ 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 { FileUploadService } from "@bitwarden/common/services/fileUpload.service"; import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.service"; 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"; @@ -89,19 +88,22 @@ 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 { 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"; @@ -148,12 +150,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; @@ -227,7 +230,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, @@ -282,7 +285,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, @@ -295,7 +298,7 @@ export default class MainBackground { this.stateService, this.encryptService ); - this.folderService = new FolderService( + this.folderService = new BrowserFolderService( this.cryptoService, this.i18nService, this.cipherService, @@ -317,8 +320,8 @@ 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, @@ -409,15 +412,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, @@ -429,7 +436,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); @@ -532,7 +539,7 @@ export default class MainBackground { this.passwordGenerationService, this.platformUtilsService, this.authService, - this.eventService, + this.eventCollectionService, this.totpService ); this.idleBackground = new IdleBackground( @@ -560,7 +567,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(); @@ -626,10 +633,9 @@ export default class MainBackground { } 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), diff --git a/apps/browser/src/background/notification.background.ts b/apps/browser/src/background/notification.background.ts index 127fc5203d7..2da4791857d 100644 --- a/apps/browser/src/background/notification.background.ts +++ b/apps/browser/src/background/notification.background.ts @@ -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() { diff --git a/apps/browser/src/background/service_factories/autofill-service.factory.ts b/apps/browser/src/background/service_factories/autofill-service.factory.ts index a14cd1dd8c3..1df2ec9bbdf 100644 --- a/apps/browser/src/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/background/service_factories/autofill-service.factory.ts @@ -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) ) ); diff --git a/apps/browser/src/background/service_factories/event-collection-service.factory.ts b/apps/browser/src/background/service_factories/event-collection-service.factory.ts new file mode 100644 index 00000000000..477797d1b42 --- /dev/null +++ b/apps/browser/src/background/service_factories/event-collection-service.factory.ts @@ -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 { + return factory( + cache, + "eventCollectionService", + opts, + async () => + new EventCollectionService( + await cipherServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), + await organizationServiceFactory(cache, opts), + await eventUploadServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/background/service_factories/event-service.factory.ts b/apps/browser/src/background/service_factories/event-service.factory.ts deleted file mode 100644 index 61a82ebeb19..00000000000 --- a/apps/browser/src/background/service_factories/event-service.factory.ts +++ /dev/null @@ -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 { - 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) - ) - ); -} diff --git a/apps/browser/src/background/service_factories/event-upload-service.factory.ts b/apps/browser/src/background/service_factories/event-upload-service.factory.ts new file mode 100644 index 00000000000..c0ed0e5426d --- /dev/null +++ b/apps/browser/src/background/service_factories/event-upload-service.factory.ts @@ -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 { + return factory( + cache, + "eventUploadService", + opts, + async () => + new EventUploadService( + await apiServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), + await logServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/background/service_factories/folder-service.factory.ts b/apps/browser/src/background/service_factories/folder-service.factory.ts index a7c90d234b1..bb35970325f 100644 --- a/apps/browser/src/background/service_factories/folder-service.factory.ts +++ b/apps/browser/src/background/service_factories/folder-service.factory.ts @@ -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), diff --git a/apps/browser/src/background/service_factories/organization-service.factory.ts b/apps/browser/src/background/service_factories/organization-service.factory.ts index 2e7c31b596e..4f2eaee8058 100644 --- a/apps/browser/src/background/service_factories/organization-service.factory.ts +++ b/apps/browser/src/background/service_factories/organization-service.factory.ts @@ -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)) ); } diff --git a/apps/browser/src/background/service_factories/policy-service.factory.ts b/apps/browser/src/background/service_factories/policy-service.factory.ts index d4940bef259..d20bca3c62e 100644 --- a/apps/browser/src/background/service_factories/policy-service.factory.ts +++ b/apps/browser/src/background/service_factories/policy-service.factory.ts @@ -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) ) diff --git a/apps/browser/src/background/service_factories/settings-service.factory.ts b/apps/browser/src/background/service_factories/settings-service.factory.ts index 745a6d08d60..73e0ae52032 100644 --- a/apps/browser/src/background/service_factories/settings-service.factory.ts +++ b/apps/browser/src/background/service_factories/settings-service.factory.ts @@ -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)) ); } diff --git a/apps/browser/src/background/service_factories/state-service.factory.ts b/apps/browser/src/background/service_factories/state-service.factory.ts index 1b81567ac52..6d2c5cb4fa7 100644 --- a/apps/browser/src/background/service_factories/state-service.factory.ts +++ b/apps/browser/src/background/service_factories/state-service.factory.ts @@ -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 { +): Promise { 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), diff --git a/apps/browser/src/clipboard/clear-clipboard.spec.ts b/apps/browser/src/clipboard/clear-clipboard.spec.ts new file mode 100644 index 00000000000..0166bdb47af --- /dev/null +++ b/apps/browser/src/clipboard/clear-clipboard.spec.ts @@ -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", + }); + }); + }); +}); diff --git a/apps/browser/src/clipboard/clear-clipboard.spec.ts.disabled b/apps/browser/src/clipboard/clear-clipboard.spec.ts.disabled deleted file mode 100644 index 6f7314d91ac..00000000000 --- a/apps/browser/src/clipboard/clear-clipboard.spec.ts.disabled +++ /dev/null @@ -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; - let serviceCache: Record; - - beforeEach(() => { - stateService = mock(); - 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(); - }); - }); -}); diff --git a/apps/browser/src/clipboard/clear-clipboard.ts b/apps/browser/src/clipboard/clear-clipboard.ts index 00bf329f8d3..149992555b9 100644 --- a/apps/browser/src/clipboard/clear-clipboard.ts +++ b/apps/browser/src/clipboard/clear-clipboard.ts @@ -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) { - 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; diff --git a/apps/browser/src/clipboard/clipboard-state.ts b/apps/browser/src/clipboard/clipboard-state.ts index a1c15addc0a..cfa2f9459f8 100644 --- a/apps/browser/src/clipboard/clipboard-state.ts +++ b/apps/browser/src/clipboard/clipboard-state.ts @@ -1,10 +1,10 @@ -import { StateService } from "../services/abstractions/state.service"; +import { BrowserStateService } from "../services/abstractions/browser-state.service"; const clearClipboardStorageKey = "clearClipboardTime"; -export const getClearClipboardTime = async (stateService: StateService) => { +export const getClearClipboardTime = async (stateService: BrowserStateService) => { return await stateService.getFromSessionMemory(clearClipboardStorageKey); }; -export const setClearClipboardTime = async (stateService: StateService, time: number) => { +export const setClearClipboardTime = async (stateService: BrowserStateService, time: number) => { await stateService.setInSessionMemory(clearClipboardStorageKey, time); }; diff --git a/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts index e9c2141211f..5d119513f43 100644 --- a/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts @@ -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; - let stateService: MockProxy; + let stateService: MockProxy; let sut: GeneratePasswordToClipboardCommand; beforeEach(() => { passwordGenerationService = mock(); - stateService = mock(); + stateService = mock(); 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(); }); }); }); diff --git a/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts index ca92d2c686f..0cd8eec2c93 100644 --- a/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts @@ -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); } } } diff --git a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.spec.ts index cc8a5618760..92c5dfb0170 100644 --- a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.spec.ts @@ -1,6 +1,6 @@ import { BehaviorSubject } from "rxjs"; -import { StateService } from "../../services/state.service"; +import { BrowserStateService } from "../../services/browser-state.service"; import { browserSession } from "./browser-session.decorator"; import { SessionStorable } from "./session-storable"; @@ -22,25 +22,25 @@ describe("browserSession decorator", () => { }); it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(StateService.prototype, {}); + const stateService = Object.create(BrowserStateService.prototype, {}); @browserSession class TestClass { - constructor(private stateService: StateService) {} + constructor(private stateService: BrowserStateService) {} } expect(new TestClass(stateService)).toBeDefined(); }); describe("interaction with @sessionSync decorator", () => { - let stateService: StateService; + let stateService: BrowserStateService; @browserSession class TestClass { @sessionSync({ initializer: (s: string) => s }) private behaviorSubject = new BehaviorSubject(""); - constructor(private stateService: StateService) {} + constructor(private stateService: BrowserStateService) {} fromJSON(json: any) { this.behaviorSubject.next(json); @@ -48,7 +48,7 @@ describe("browserSession decorator", () => { } beforeEach(() => { - stateService = Object.create(StateService.prototype, {}) as StateService; + stateService = Object.create(BrowserStateService.prototype, {}) as BrowserStateService; }); it("should create a session syncer", () => { diff --git a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts index 73cdf767357..5d9d56c1d71 100644 --- a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts +++ b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts @@ -1,6 +1,6 @@ import { Constructor } from "type-fest"; -import { StateService } from "../../services/state.service"; +import { BrowserStateService } from "../../services/browser-state.service"; import { SessionStorable } from "./session-storable"; import { SessionSyncer } from "./session-syncer"; @@ -22,7 +22,13 @@ export function browserSession>(constructor: TCto super(...args); // Require state service to be injected - const stateService = args.find((arg) => arg instanceof StateService); + const stateService: BrowserStateService = [this as any] + .concat(args) + .find( + (arg) => + typeof arg.setInSessionMemory === "function" && + typeof arg.getFromSessionMemory === "function" + ); if (!stateService) { throw new Error( `Cannot decorate ${constructor.name} with browserSession, Browser's StateService must be injected` @@ -38,7 +44,7 @@ export function browserSession>(constructor: TCto ); } - buildSyncer(metadata: SyncedItemMetadata, stateService: StateService) { + buildSyncer(metadata: SyncedItemMetadata, stateService: BrowserStateService) { const syncer = new SessionSyncer((this as any)[metadata.propertyKey], stateService, metadata); syncer.init(); return syncer; diff --git a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts index 38ec7aac9a1..c6d46b28fd1 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts @@ -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(); }); }); diff --git a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.ts b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.ts index df0764528f7..6a6b3d33fa1 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.ts @@ -1,13 +1,17 @@ import { Jsonify } from "type-fest"; import { SessionStorable } from "./session-storable"; +import { InitializeOptions } from "./sync-item-metadata"; -class BuildOptions { +class BuildOptions> { ctor?: new () => T; - initializer?: (keyValuePair: Jsonify) => 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 { * @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(buildOptions: BuildOptions) { 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", }); }; } diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts index 5286cece1bb..c37b640f3f9 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts @@ -1,8 +1,9 @@ +import { awaitAsync, awaitAsync as flushAsyncObservables } from "@bitwarden/angular/../test-utils"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, ReplaySubject } from "rxjs"; import { BrowserApi } from "../../browser/browserApi"; -import { StateService } from "../../services/abstractions/state.service"; +import { BrowserStateService } from "../../services/abstractions/browser-state.service"; import { SessionSyncer } from "./session-syncer"; import { SyncedItemMetadata } from "./sync-item-metadata"; @@ -10,8 +11,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; + const metaData: SyncedItemMetadata = { + propertyKey, + sessionKey, + initializer: (s: string) => s, + initializeAs: "object", + }; + let stateService: MockProxy; let sut: SessionSyncer; let behaviorSubject: BehaviorSubject; @@ -23,7 +29,8 @@ describe("session syncer", () => { manifest_version: 3, }); - stateService = mock(); + stateService = mock(); + stateService.hasInSessionMemory.mockResolvedValue(false); sut = new SessionSyncer(behaviorSubject, stateService, metaData); }); @@ -34,53 +41,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"); + }).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, stateService, { + propertyKey, + sessionKey, + ctor: String, + initializeAs: "object", + }) ).toBeDefined(); expect( new SessionSyncer(behaviorSubject, stateService, { 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, stateService, { + 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(Infinity); + replaySubject.next("1"); + replaySubject.next("2"); + replaySubject.next("3"); + sut = new SessionSyncer(replaySubject, stateService, 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("initial"); + sut = new SessionSyncer(behaviorSubject, stateService, 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 () => { + stateService.hasInSessionMemory.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 () => { + stateService.hasInSessionMemory.mockResolvedValue(false); + //Block a call to update + const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); + + sut.init(); + await awaitAsync(); + + expect(updateSpy).not.toHaveBeenCalled(); }); }); @@ -146,6 +187,7 @@ describe("session syncer", () => { stateService.getFromSessionMemory.mockResolvedValue("test"); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" }); + await flushAsyncObservables(); expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1); expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index 2acfed2954f..91b371ef817 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -1,9 +1,9 @@ -import { BehaviorSubject, concatMap, Subscription } from "rxjs"; +import { BehaviorSubject, concatMap, ReplaySubject, Subject, Subscription } from "rxjs"; import { Utils } from "@bitwarden/common/misc/utils"; import { BrowserApi } from "../../browser/browserApi"; -import { StateService } from "../../services/abstractions/state.service"; +import { BrowserStateService } from "../../services/abstractions/browser-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, - private stateService: StateService, + private subject: Subject, + private stateService: BrowserStateService, 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.stateService.hasInSessionMemory(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,10 +81,14 @@ 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); + this.ignoreNUpdates = 1; + this.subject.next(value); } private async updateSession(value: any) { diff --git a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts index 2b3f4715d46..facfda32fd8 100644 --- a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts +++ b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts @@ -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 = {}; + for (const key in keyValuePair) { + record[key] = itemBuilder(keyValuePair[key]); + } + return record; + }; } else { return (keyValuePair: any) => itemBuilder(keyValuePair); } diff --git a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts index 5cd869a5b67..12b0b57d523 100644 --- a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts @@ -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" }); + }); }); diff --git a/apps/browser/src/listeners/update-badge.ts b/apps/browser/src/listeners/update-badge.ts index 9c7c122a45a..8762a15ab2a 100644 --- a/apps/browser/src/listeners/update-badge.ts +++ b/apps/browser/src/listeners/update-badge.ts @@ -15,7 +15,7 @@ import { searchServiceFactory } from "../background/service_factories/search-ser import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { BrowserApi } from "../browser/browserApi"; import { Account } from "../models/account"; -import { StateService } from "../services/abstractions/state.service"; +import { BrowserStateService } from "../services/abstractions/browser-state.service"; import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; export type BadgeOptions = { @@ -25,7 +25,7 @@ export type BadgeOptions = { export class UpdateBadge { private authService: AuthService; - private stateService: StateService; + private stateService: BrowserStateService; private cipherService: CipherService; private badgeAction: typeof chrome.action; private sidebarAction: OperaSidebarAction | FirefoxSidebarAction; diff --git a/apps/browser/src/models/account.ts b/apps/browser/src/models/account.ts index f49c55d2909..cfbcbecf979 100644 --- a/apps/browser/src/models/account.ts +++ b/apps/browser/src/models/account.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Account as BaseAccount, AccountSettings as BaseAccountSettings, @@ -9,6 +11,14 @@ import { BrowserSendComponentState } from "./browserSendComponentState"; export class AccountSettings extends BaseAccountSettings { vaultTimeout = -1; // On Restart + + static fromJSON(json: Jsonify): AccountSettings { + if (json == null) { + return null; + } + + return Object.assign(new AccountSettings(), json, super.fromJSON(json)); + } } export class Account extends BaseAccount { @@ -29,4 +39,18 @@ export class Account extends BaseAccount { this.ciphers = init?.ciphers ?? new BrowserComponentState(); this.sendType = init?.sendType ?? new BrowserComponentState(); } + + static fromJSON(json: Jsonify): Account { + if (json == null) { + return null; + } + + return Object.assign(new Account({}), json, super.fromJSON(json), { + settings: AccountSettings.fromJSON(json.settings), + groupings: BrowserGroupingsComponentState.fromJSON(json.groupings), + send: BrowserSendComponentState.fromJSON(json.send), + ciphers: BrowserComponentState.fromJSON(json.ciphers), + sendType: BrowserComponentState.fromJSON(json.sendType), + }); + } } diff --git a/apps/browser/src/models/browserComponentState.ts b/apps/browser/src/models/browserComponentState.ts index d968726c413..c5540d088ff 100644 --- a/apps/browser/src/models/browserComponentState.ts +++ b/apps/browser/src/models/browserComponentState.ts @@ -1,4 +1,14 @@ +import { Jsonify } from "type-fest"; + export class BrowserComponentState { scrollY: number; searchText: string; + + static fromJSON(json: Jsonify) { + if (json == null) { + return null; + } + + return Object.assign(new BrowserComponentState(), json); + } } diff --git a/apps/browser/src/models/browserGroupingsComponentState.ts b/apps/browser/src/models/browserGroupingsComponentState.ts index 63eb4aaa88f..f406e3d8271 100644 --- a/apps/browser/src/models/browserGroupingsComponentState.ts +++ b/apps/browser/src/models/browserGroupingsComponentState.ts @@ -1,7 +1,9 @@ import { CipherType } from "@bitwarden/common/enums/cipherType"; +import { Utils } from "@bitwarden/common/misc/utils"; import { CipherView } from "@bitwarden/common/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/models/view/collection.view"; import { FolderView } from "@bitwarden/common/models/view/folder.view"; +import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; import { BrowserComponentState } from "./browserComponentState"; @@ -15,4 +17,28 @@ export class BrowserGroupingsComponentState extends BrowserComponentState { folders: FolderView[]; collections: CollectionView[]; deletedCount: number; + + toJSON() { + return Utils.merge(this, { + collectionCounts: Utils.mapToRecord(this.collectionCounts), + folderCounts: Utils.mapToRecord(this.folderCounts), + typeCounts: Utils.mapToRecord(this.typeCounts), + }); + } + + static fromJSON(json: DeepJsonify) { + if (json == null) { + return null; + } + + return Object.assign(new BrowserGroupingsComponentState(), json, { + favoriteCiphers: json.favoriteCiphers?.map((c) => CipherView.fromJSON(c)), + noFolderCiphers: json.noFolderCiphers?.map((c) => CipherView.fromJSON(c)), + ciphers: json.ciphers?.map((c) => CipherView.fromJSON(c)), + collectionCounts: Utils.recordToMap(json.collectionCounts), + folderCounts: Utils.recordToMap(json.folderCounts), + typeCounts: Utils.recordToMap(json.typeCounts), + folders: json.folders?.map((f) => FolderView.fromJSON(f)), + }); + } } diff --git a/apps/browser/src/models/browserSendComponentState.ts b/apps/browser/src/models/browserSendComponentState.ts index e2bf4eaa5d6..99508737ab1 100644 --- a/apps/browser/src/models/browserSendComponentState.ts +++ b/apps/browser/src/models/browserSendComponentState.ts @@ -1,9 +1,28 @@ import { SendType } from "@bitwarden/common/enums/sendType"; +import { Utils } from "@bitwarden/common/misc/utils"; import { SendView } from "@bitwarden/common/models/view/send.view"; +import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; import { BrowserComponentState } from "./browserComponentState"; export class BrowserSendComponentState extends BrowserComponentState { sends: SendView[]; typeCounts: Map; + + toJSON() { + return Utils.merge(this, { + typeCounts: Utils.mapToRecord(this.typeCounts), + }); + } + + static fromJSON(json: DeepJsonify) { + if (json == null) { + return null; + } + + return Object.assign(new BrowserSendComponentState(), json, { + sends: json.sends?.map((s) => SendView.fromJSON(s)), + typeCounts: Utils.recordToMap(json.typeCounts), + }); + } } diff --git a/apps/browser/src/popup/accounts/set-password.component.html b/apps/browser/src/popup/accounts/set-password.component.html index 517374578c2..9eb551bf269 100644 --- a/apps/browser/src/popup/accounts/set-password.component.html +++ b/apps/browser/src/popup/accounts/set-password.component.html @@ -39,13 +39,10 @@
-