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

Merge branch 'feature/SG-771-org-domain-claiming-web' into feature/SG-680-create-domain-verification-comp

This commit is contained in:
Jared Snider
2022-12-05 15:22:02 -05:00
358 changed files with 3788 additions and 2126 deletions

View File

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

View File

@@ -4,14 +4,10 @@
./apps/browser/src/safari/desktop/Base.lproj
./apps/browser/src/services/vaultTimeout
./apps/browser/store/windows/Assets
./apps/desktop/src/models/nativeMessaging
./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads
./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses
./libs/common/spec/misc/logInStrategies
./libs/common/src/abstractions/fileDownload
./libs/common/src/abstractions/userVerification
./libs/common/src/abstractions/vaultTimeout
./libs/common/src/emailForwarders
./libs/common/src/misc/logInStrategies
./libs/common/src/services/userVerification
./libs/common/src/services/vaultTimeout
@@ -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,16 +152,6 @@
./libs/common/src/services/bitwardenFileUpload.service.ts
./libs/common/src/services/webCryptoFunction.service.ts
./libs/common/src/interfaces/IEncrypted.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
@@ -186,42 +166,9 @@
./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/browser/README.md

View File

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

View File

@@ -62,7 +62,7 @@ jobs:
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version package.json)
echo "::set-output name=package_version::$PKG_VERSION"
echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT
cli:

View File

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

View File

@@ -60,7 +60,7 @@ jobs:
- name: Get GitHub sha as version
id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
run: echo "value=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
build-artifacts:
name: Build artifacts
@@ -303,7 +303,7 @@ jobs:
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi
echo "::set-output name=value::$IMAGE_TAG"
echo "value=$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Tag image
env:

View File

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

View File

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

View File

@@ -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 hasnt gone well, you must increment the version \
number higher than your broken release. Because some of your users will be on the broken 1.0.1, \
releasing a new 1.0.1 would result in them staying on a broken version.
releasing a new 1.0.1 would result in them staying on a broken version."
exit 1
fi
@@ -118,10 +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

View File

@@ -36,7 +36,7 @@ jobs:
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "::set-output name=new-version::$NEW_VER"
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump:
name: "Trigger desktop version bump workflow"

View File

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

View File

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

View File

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

View File

@@ -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."

View File

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

View File

@@ -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"

View File

@@ -3,7 +3,7 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Ilmainen salasananhallinta",
"message": "Bitwarden - Free Password Manager",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {

View File

@@ -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"
}
}

View File

@@ -2042,6 +2042,6 @@
"message": "Jesteś tu nowy(a)?"
},
"rememberEmail": {
"message": "Zapamiętaj email"
"message": "Zapamiętaj adres e-mail"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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<string, unknown>,
message: { command: string }
) => void | Promise<void>)[] = [UpdateBadge.messageListener];
type AlarmAction = (executionTime: Date, serviceCache: Record<string, unknown>) => 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(() => {

View File

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

View File

@@ -61,14 +61,11 @@ import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.s
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/services/provider.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SendService } from "@bitwarden/common/services/send.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { SyncService } from "@bitwarden/common/services/sync/sync.service";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
@@ -89,19 +86,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";
@@ -227,7 +227,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 +282,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 +295,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 +317,8 @@ export default class MainBackground {
this.stateService
);
this.syncNotifierService = new SyncNotifierService();
this.organizationService = new OrganizationService(this.stateService);
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,

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
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";
@@ -17,6 +18,6 @@ export function organizationServiceFactory(
cache,
"organizationService",
opts,
async () => new OrganizationService(await stateServiceFactory(cache, opts))
async () => new BrowserOrganizationService(await stateServiceFactory(cache, opts))
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +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<number>(clearClipboardStorageKey);
};
export const setClearClipboardTime = async (stateService: StateService, time: number) => {
export const setClearClipboardTime = async (stateService: BrowserStateService, time: number) => {
await stateService.setInSessionMemory(clearClipboardStorageKey, time);
};

View File

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

View File

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

View File

@@ -1,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", () => {

View File

@@ -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<TCtor extends Constructor<any>>(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<TCtor extends Constructor<any>>(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;

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { 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<StateService>;
const metaData: SyncedItemMetadata = {
propertyKey,
sessionKey,
initializer: (s: string) => s,
initializeAs: "object",
};
let stateService: MockProxy<BrowserStateService>;
let sut: SessionSyncer;
let behaviorSubject: BehaviorSubject<string>;
@@ -23,7 +29,7 @@ describe("session syncer", () => {
manifest_version: 3,
});
stateService = mock<StateService>();
stateService = mock<BrowserStateService>();
sut = new SessionSyncer(behaviorSubject, stateService, metaData);
});
@@ -34,53 +40,85 @@ 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<string>(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<string>("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", () => {
stateService.hasInSessionMemory.mockResolvedValue(true);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
sut.init();
expect(updateSpy).toHaveBeenCalledWith();
});
it("should not grab an initial value from storage if it does not exist", () => {
stateService.hasInSessionMemory.mockResolvedValue(false);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
sut.init();
expect(updateSpy).toHaveBeenCalledWith();
});
});
@@ -146,6 +184,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);

View File

@@ -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<any>,
private stateService: StateService,
private subject: Subject<any>,
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,23 @@ 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();
if (this.stateService.hasInSessionMemory(this.metaData.sessionKey)) {
this.update();
}
this.listenForUpdates();
}
@@ -41,11 +53,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 +78,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) {

View File

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

View File

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

View File

@@ -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;

View File

@@ -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>): 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>): 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),
});
}
}

View File

@@ -1,4 +1,14 @@
import { Jsonify } from "type-fest";
export class BrowserComponentState {
scrollY: number;
searchText: string;
static fromJSON(json: Jsonify<BrowserComponentState>) {
if (json == null) {
return null;
}
return Object.assign(new BrowserComponentState(), json);
}
}

View File

@@ -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<BrowserGroupingsComponentState>) {
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)),
});
}
}

View File

@@ -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<SendType, number>;
toJSON() {
return Utils.merge(this, {
typeCounts: Utils.mapToRecord(this.typeCounts),
});
}
static fromJSON(json: DeepJsonify<BrowserSendComponentState>) {
if (json == null) {
return null;
}
return Object.assign(new BrowserSendComponentState(), json, {
sends: json.sends?.map((s) => SendView.fromJSON(s)),
typeCounts: Utils.recordToMap(json.typeCounts),
});
}
}

View File

@@ -39,13 +39,10 @@
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword"
>{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ passwordStrengthComponent?.masterPasswordScoreColor }}"
*ngIf="passwordStrengthComponent?.masterPasswordScoreText"
>
{{ passwordStrengthComponent?.masterPasswordScoreText }}
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input

View File

@@ -86,7 +86,9 @@
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn && !webAuthnNewTab">
<div id="web-authn-frame"><iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe></div>
<div id="web-authn-frame">
<iframe id="webauthn_iframe" [attr.allow]="webAuthnAllow"></iframe>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>

View File

@@ -19,7 +19,7 @@ import { MessagingService } from "@bitwarden/common/abstractions/messaging.servi
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { BrowserApi } from "../browser/browserApi";
import { StateService } from "../services/abstractions/state.service";
import { BrowserStateService } from "../services/abstractions/browser-state.service";
import { routerTransition } from "./app-routing.animations";
@@ -43,7 +43,7 @@ export class AppComponent implements OnInit, OnDestroy {
private authService: AuthService,
private i18nService: I18nService,
private router: Router,
private stateService: StateService,
private stateService: BrowserStateService,
private messagingService: MessagingService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,

View File

@@ -57,11 +57,11 @@
appStopClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="regenerate()"
[disabled]="form.loading"
[disabled]="$any(form).loading"
>
<i
class="bwi bwi-lg bwi-generate"
[ngClass]="form.loading ? 'bwi-spin' : ''"
[ngClass]="$any(form).loading ? 'bwi-spin' : ''"
aria-hidden="true"
></i>
</button>
@@ -351,7 +351,7 @@
<input
id="duckduckgo-apikey"
type="password"
name="DuckDudkGoApiKey"
name="DuckDuckGoApiKey"
[(ngModel)]="usernameOptions.forwardedDuckDuckGoToken"
(blur)="saveUsernameOptions()"
/>

View File

@@ -432,6 +432,12 @@ app-vault-view .box-footer {
user-select: auto;
}
/* tweak for inconsistent line heights in cipher view */
.box-footer button,
.box-footer a {
line-height: 1;
}
// Workaround for slow performance on external monitors on Chrome + MacOS
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
@keyframes redraw {

View File

@@ -305,8 +305,11 @@
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading"></i>
<i class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw" [hidden]="!deleteBtn.loading"></i>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteSend" | i18n }}</span>
</div>

View File

@@ -12,7 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SendService } from "@bitwarden/common/abstractions/send.service";
import { StateService } from "../../services/abstractions/state.service";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { PopupUtilsService } from "../services/popup-utils.service";
@Component({
@@ -33,7 +33,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
stateService: BrowserStateService,
messagingService: MessagingService,
policyService: PolicyService,
environmentService: EnvironmentService,

View File

@@ -15,7 +15,7 @@ import { SendType } from "@bitwarden/common/enums/sendType";
import { SendView } from "@bitwarden/common/models/view/send.view";
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
import { StateService } from "../../services/abstractions/state.service";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { PopupUtilsService } from "../services/popup-utils.service";
const ComponentId = "SendComponent";
@@ -42,7 +42,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
policyService: PolicyService,
searchService: SearchService,
private popupUtils: PopupUtilsService,
private stateService: StateService,
private stateService: BrowserStateService,
private router: Router,
private syncService: SyncService,
private changeDetectorRef: ChangeDetectorRef,
@@ -165,12 +165,12 @@ export class SendGroupingsComponent extends BaseSendComponent {
}
private async saveState() {
this.state = {
this.state = Object.assign(new BrowserSendComponentState(), {
scrollY: this.popupUtils.getContentScrollY(window),
searchText: this.searchText,
sends: this.sends,
typeCounts: this.typeCounts,
};
});
await this.stateService.setBrowserSendComponentState(this.state);
}

View File

@@ -16,7 +16,7 @@ import { SendType } from "@bitwarden/common/enums/sendType";
import { SendView } from "@bitwarden/common/models/view/send.view";
import { BrowserComponentState } from "../../models/browserComponentState";
import { StateService } from "../../services/abstractions/state.service";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { PopupUtilsService } from "../services/popup-utils.service";
const ComponentId = "SendTypeComponent";
@@ -41,7 +41,7 @@ export class SendTypeComponent extends BaseSendComponent {
policyService: PolicyService,
searchService: SearchService,
private popupUtils: PopupUtilsService,
private stateService: StateService,
private stateService: BrowserStateService,
private route: ActivatedRoute,
private location: Location,
private changeDetectorRef: ChangeDetectorRef,

View File

@@ -5,7 +5,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service";
import { BrowserStateService as StateServiceAbstraction } from "../../services/abstractions/browser-state.service";
import { PopupUtilsService } from "./popup-utils.service";

View File

@@ -38,6 +38,7 @@ import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abs
import { SendService } from "@bitwarden/common/abstractions/send.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
@@ -47,6 +48,8 @@ import { UserVerificationService } from "@bitwarden/common/abstractions/userVeri
import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { LoginService } from "@bitwarden/common/services/login.service";
@@ -54,9 +57,14 @@ import { SearchService } from "@bitwarden/common/services/search.service";
import MainBackground from "../../background/main.background";
import { BrowserApi } from "../../browser/browserApi";
import { Account } from "../../models/account";
import { AutofillService } 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 { BrowserEnvironmentService } from "../../services/browser-environment.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 { BrowserFileDownloadService } from "../../services/browserFileDownloadService";
import BrowserMessagingService from "../../services/browserMessaging.service";
import BrowserMessagingPrivateModePopupService from "../../services/browserMessagingPrivateModePopup.service";
@@ -190,8 +198,13 @@ function getBgService<T>(service: keyof MainBackground) {
{ provide: EventService, useFactory: getBgService<EventService>("eventService"), deps: [] },
{
provide: PolicyService,
useFactory: getBgService<PolicyService>("policyService"),
deps: [],
useFactory: (
stateService: StateServiceAbstraction,
organizationService: OrganizationService
) => {
return new BrowserPolicyService(stateService, organizationService);
},
deps: [StateServiceAbstraction, OrganizationService],
},
{
provide: PolicyApiServiceAbstraction,
@@ -212,8 +225,10 @@ function getBgService<T>(service: keyof MainBackground) {
{ provide: SyncService, useFactory: getBgService<SyncService>("syncService"), deps: [] },
{
provide: SettingsService,
useFactory: getBgService<SettingsService>("settingsService"),
deps: [],
useFactory: (stateService: StateServiceAbstraction) => {
return new BrowserSettingsService(stateService);
},
deps: [StateServiceAbstraction],
},
{
provide: AbstractStorageService,
@@ -261,8 +276,10 @@ function getBgService<T>(service: keyof MainBackground) {
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: OrganizationService,
useFactory: getBgService<OrganizationService>("organizationService"),
deps: [],
useFactory: (stateService: StateServiceAbstraction) => {
return new BrowserOrganizationService(stateService);
},
deps: [StateServiceAbstraction],
},
{
provide: VaultFilterService,
@@ -293,10 +310,36 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
},
{
provide: StateServiceAbstraction,
useFactory: getBgService<StateServiceAbstraction>("stateService"),
provide: StateMigrationService,
useFactory: getBgService<StateMigrationService>("stateMigrationService"),
deps: [],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractStorageService,
logService: LogServiceAbstraction,
stateMigrationService: StateMigrationService
) => {
return new BrowserStateService(
storageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
);
},
deps: [
AbstractStorageService,
SECURE_STORAGE,
MEMORY_STORAGE,
LogServiceAbstraction,
StateMigrationService,
],
},
{
provide: UsernameGenerationService,
useFactory: getBgService<UsernameGenerationService>("usernameGenerationService"),
@@ -317,17 +360,19 @@ function getBgService<T>(service: keyof MainBackground) {
},
{
provide: AbstractThemingService,
useFactory: () => {
useFactory: (
stateService: StateServiceAbstraction,
platformUtilsService: PlatformUtilsService
) => {
return new ThemingService(
getBgService<StateServiceAbstraction>("stateService")(),
stateService,
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
// In Safari we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
getBgService<PlatformUtilsService>("platformUtilsService")().isSafari()
? getBgService<Window>("backgroundWindow")()
: window,
platformUtilsService.isSafari() ? getBgService<Window>("backgroundWindow")() : window,
document
);
},
deps: [StateServiceAbstraction, PlatformUtilsService],
},
],
})

View File

@@ -40,8 +40,11 @@
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading"></i>
<i class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw" [hidden]="!deleteBtn.loading"></i>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteFolder" | i18n }}</span>
</div>

View File

@@ -49,14 +49,14 @@
#refreshBtn
type="button"
(click)="refresh()"
[disabled]="refreshBtn.loading"
[disabled]="$any(refreshBtn).loading"
[appApiAction]="refreshPromise"
class="btn link block"
>
<span [hidden]="refreshBtn.loading">{{ "premiumRefresh" | i18n }}</span>
<span [hidden]="$any(refreshBtn).loading">{{ "premiumRefresh" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!refreshBtn.loading"
[hidden]="!$any(refreshBtn).loading"
aria-hidden="true"
></i>
</button>

View File

@@ -17,11 +17,15 @@
class="btn block primary"
(click)="sync()"
#syncBtn
[disabled]="syncBtn.loading"
[disabled]="$any(syncBtn).loading"
[appApiAction]="syncPromise"
>
<span [hidden]="syncBtn.loading">{{ "syncVaultNow" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!syncBtn.loading" aria-hidden="true"></i>
<span [hidden]="$any(syncBtn).loading">{{ "syncVaultNow" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!$any(syncBtn).loading"
aria-hidden="true"
></i>
</button>
<p class="text-center text-muted small">{{ "lastSync" | i18n }} {{ lastSync }}</p>
</div>

View File

@@ -81,8 +81,8 @@
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean"
appTrueFalseValue
trueValue="true"
falseValue="false"
[trueValue]="true"
[falseValue]="false"
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>

View File

@@ -88,17 +88,17 @@
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="checkPasswordBtn.loading"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="checkPasswordBtn.loading"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!checkPasswordBtn.loading"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
@@ -487,7 +487,7 @@
type="text"
name="Login.Uris[{{ i }}].Uri"
[(ngModel)]="u.uri"
[hidden]="u.showUriOptionsInput === true"
[hidden]="$any(u).showUriOptionsInput === true"
placeholder="{{ 'ex' | i18n }} https://google.com"
inputmode="url"
appInputVerbatim
@@ -500,7 +500,7 @@
id="currentUris{{ i }}"
name="Login.Uris[{{ i }}].CurrentUris"
[(ngModel)]="u.uri"
[hidden]="!u.showCurrentUris"
[hidden]="!$any(u).showCurrentUris"
>
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
@@ -512,7 +512,9 @@
id="loginUriMatch{{ i }}"
name="Login.Uris[{{ i }}].Match"
[(ngModel)]="u.match"
[hidden]="u.showOptions === false || (u.showOptions == null && u.match == null)"
[hidden]="
$any(u).showOptions === false || ($any(u).showOptions == null && u.match == null)
"
(change)="loginUriMatchChanged(u)"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
@@ -526,7 +528,7 @@
appStopClick
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
(click)="toggleUriInput(u)"
[attr.aria-pressed]="u.showCurrentUris === true"
[attr.aria-pressed]="$any(u).showCurrentUris === true"
>
<i aria-hidden="true" class="bwi bwi-lg bwi-list"></i>
</button>
@@ -536,7 +538,7 @@
appStopClick
appA11yTitle="{{ 'toggleOptions' | i18n }}"
(click)="toggleUriOptions(u)"
[attr.aria-pressed]="u.showOptions === true"
[attr.aria-pressed]="$any(u).showOptions === true"
>
<i class="bwi bwi-lg bwi-cog" aria-hidden="true"></i>
</button>
@@ -695,7 +697,7 @@
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>
@@ -713,8 +715,11 @@
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading"></i>
<i class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw" [hidden]="!deleteBtn.loading"></i>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteItem" | i18n }}</span>
</div>

View File

@@ -37,16 +37,16 @@
(click)="delete(a)"
#deleteBtn
[appApiAction]="deletePromises[a.id]"
[disabled]="deleteBtn.loading"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="deleteBtn.loading"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
[hidden]="!$any(deleteBtn).loading"
aria-hidden="true"
></i>
</button>

View File

@@ -33,7 +33,7 @@
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>

View File

@@ -200,7 +200,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
}
}
private async load() {
protected async load() {
this.isLoading = false;
this.tab = await BrowserApi.getTabFromCurrentWindow();
if (this.tab != null) {

View File

@@ -18,7 +18,7 @@ import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { BrowserApi } from "../../browser/browserApi";
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
import { StateService } from "../../services/abstractions/state.service";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { VaultFilterService } from "../../services/vaultFilter.service";
import { PopupUtilsService } from "../services/popup-utils.service";
@@ -83,7 +83,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
private platformUtilsService: PlatformUtilsService,
private searchService: SearchService,
private location: Location,
private browserStateService: StateService,
private browserStateService: BrowserStateService,
private vaultFilterService: VaultFilterService
) {
this.noFolderListSize = 100;
@@ -373,7 +373,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
}
private async saveState() {
this.state = {
this.state = Object.assign(new BrowserGroupingsComponentState(), {
scrollY: this.popupUtils.getContentScrollY(window),
searchText: this.searchText,
favoriteCiphers: this.favoriteCiphers,
@@ -385,7 +385,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
folders: this.folders,
collections: this.collections,
deletedCount: this.deletedCount,
};
});
await this.browserStateService.setBrowserGroupingComponentState(this.state);
}

View File

@@ -21,7 +21,7 @@ import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { BrowserApi } from "../../browser/browserApi";
import { BrowserComponentState } from "../../models/browserComponentState";
import { StateService } from "../../services/abstractions/state.service";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { VaultFilterService } from "../../services/vaultFilter.service";
import { PopupUtilsService } from "../services/popup-utils.service";
@@ -60,7 +60,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService,
private stateService: BrowserStateService,
private popupUtils: PopupUtilsService,
private i18nService: I18nService,
private folderService: FolderService,

View File

@@ -27,11 +27,7 @@
role="dialog"
aria-modal="true"
>
<button
appStopClick
(click)="selectAllVaults()"
[ngClass]="{ active: !myVaultOnly && !selectOrganizationId }"
>
<button appStopClick (click)="selectAllVaults()">
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>
&nbsp;{{ "allVaults" | i18n }}
</button>
@@ -60,7 +56,7 @@
<i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger"
aria-label="{{ 'organizationIsDisabled' | i18n }}"
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i>
</button>

View File

@@ -88,17 +88,17 @@
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="checkPasswordBtn.loading"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="checkPasswordBtn.loading"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!checkPasswordBtn.loading"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
@@ -537,12 +537,12 @@
<small class="row-sub-label">{{ attachment.sizeName }}</small>
<i
class="bwi bwi-download bwi-fw row-sub-icon"
*ngIf="!attachment.downloading"
*ngIf="!$any(attachment).downloading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
*ngIf="attachment.downloading"
*ngIf="$any(attachment).downloading"
aria-hidden="true"
></i>
</button>

View File

@@ -8,7 +8,8 @@ import { BrowserComponentState } from "../../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> {
abstract hasInSessionMemory(key: string): Promise<boolean>;
abstract getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T>;
abstract setInSessionMemory(key: string, value: any): Promise<void>;
getBrowserGroupingComponentState: (

View File

@@ -14,7 +14,6 @@ import { BrowserApi } from "../browser/browserApi";
import AutofillField from "../models/autofillField";
import AutofillPageDetails from "../models/autofillPageDetails";
import AutofillScript from "../models/autofillScript";
import { StateService } from "../services/abstractions/state.service";
import {
AutoFillOptions,
@@ -22,6 +21,7 @@ import {
PageDetail,
FormData,
} from "./abstractions/autofill.service";
import { BrowserStateService } from "./abstractions/browser-state.service";
import {
AutoFillConstants,
CreditCardAutoFillConstants,
@@ -39,7 +39,7 @@ export interface GenerateFillScriptOptions {
export default class AutofillService implements AutofillServiceInterface {
constructor(
private cipherService: CipherService,
private stateService: StateService,
private stateService: BrowserStateService,
private totpService: TotpService,
private eventService: EventService,
private logService: LogService

View File

@@ -4,12 +4,12 @@ import { Folder } from "@bitwarden/common/models/domain/folder";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { FolderService as BaseFolderService } from "@bitwarden/common/services/folder/folder.service";
import { browserSession, sessionSync } from "../../decorators/session-sync-observable";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
@browserSession
export class FolderService extends BaseFolderService {
@sessionSync({ initializer: Folder.fromJSON, initializeAsArray: true })
export class BrowserFolderService extends BaseFolderService {
@sessionSync({ initializer: Folder.fromJSON, initializeAs: "array" })
protected _folders: BehaviorSubject<Folder[]>;
@sessionSync({ initializer: FolderView.fromJSON, initializeAsArray: true })
@sessionSync({ initializer: FolderView.fromJSON, initializeAs: "array" })
protected _folderViews: BehaviorSubject<FolderView[]>;
}

View File

@@ -0,0 +1,12 @@
import { BehaviorSubject } from "rxjs";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
@browserSession
export class BrowserOrganizationService extends OrganizationService {
@sessionSync({ initializer: Organization.fromJSON, initializeAs: "array" })
protected _organizations: BehaviorSubject<Organization[]>;
}

View File

@@ -0,0 +1,12 @@
import { BehaviorSubject } from "rxjs";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
@browserSession
export class BrowserPolicyService extends PolicyService {
@sessionSync({ ctor: Policy, initializeAs: "array" })
protected _policies: BehaviorSubject<Policy[]>;
}

View File

@@ -0,0 +1,11 @@
import { BehaviorSubject } from "rxjs";
import { AccountSettingsSettings } from "@bitwarden/common/models/domain/account";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { sessionSync } from "../decorators/session-sync-observable";
export class BrowserSettingsService extends SettingsService {
@sessionSync({ initializer: (obj: string[][]) => obj })
protected _settings: BehaviorSubject<AccountSettingsSettings>;
}

View File

@@ -1,6 +1,6 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import {
MemoryStorageServiceInterface,
@@ -18,28 +18,29 @@ import { BrowserComponentState } from "../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../models/browserSendComponentState";
import { AbstractKeyGenerationService } from "./abstractions/abstractKeyGeneration.service";
import { BrowserStateService } from "./browser-state.service";
import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service";
import { StateService } from "./state.service";
describe("Browser State Service", () => {
let secureStorageService: SubstituteOf<AbstractStorageService>;
let diskStorageService: SubstituteOf<AbstractStorageService>;
let logService: SubstituteOf<LogService>;
let stateMigrationService: SubstituteOf<StateMigrationService>;
let stateFactory: SubstituteOf<StateFactory<GlobalState, Account>>;
let secureStorageService: MockProxy<AbstractStorageService>;
let diskStorageService: MockProxy<AbstractStorageService>;
let logService: MockProxy<LogService>;
let stateMigrationService: MockProxy<StateMigrationService>;
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean;
let state: State<GlobalState, Account>;
const userId = "userId";
let sut: StateService;
let sut: BrowserStateService;
beforeEach(() => {
secureStorageService = Substitute.for();
diskStorageService = Substitute.for();
logService = Substitute.for();
stateMigrationService = Substitute.for();
stateFactory = Substitute.for();
secureStorageService = mock();
diskStorageService = mock();
logService = mock();
stateMigrationService = mock();
stateFactory = mock();
useAccountCache = true;
state = new State(new GlobalState());
@@ -54,9 +55,12 @@ describe("Browser State Service", () => {
beforeEach(() => {
// We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass.
memoryStorageService = Object.create(LocalBackedSessionStorageService.prototype);
memoryStorageService = new LocalBackedSessionStorageService(
mock<EncryptService>(),
mock<AbstractKeyGenerationService>()
);
sut = new StateService(
sut = new BrowserStateService(
diskStorageService,
secureStorageService,
memoryStorageService,
@@ -80,14 +84,14 @@ describe("Browser State Service", () => {
});
describe("state methods", () => {
let memoryStorageService: SubstituteOf<AbstractStorageService & MemoryStorageServiceInterface>;
let memoryStorageService: MockProxy<AbstractStorageService & MemoryStorageServiceInterface>;
beforeEach(() => {
memoryStorageService = Substitute.for();
const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state)));
memoryStorageService.get("state", Arg.any()).mimicks(stateGetter);
memoryStorageService = mock();
const stateGetter = (key: string) => Promise.resolve(state);
memoryStorageService.get.mockImplementation(stateGetter);
sut = new StateService(
sut = new BrowserStateService(
diskStorageService,
secureStorageService,
memoryStorageService,
@@ -128,6 +132,7 @@ describe("Browser State Service", () => {
[SendType.Text, 5],
]);
state.accounts[userId].send = sendState;
(global as any)["watch"] = state;
const actual = await sut.getBrowserSendComponentState();
expect(actual).toBeInstanceOf(BrowserSendComponentState);

View File

@@ -1,24 +1,40 @@
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
import {
StateService as BaseStateService,
withPrototype,
} from "@bitwarden/common/services/state.service";
import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import { Account } from "../models/account";
import { BrowserComponentState } from "../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../models/browserSendComponentState";
import { StateService as StateServiceAbstraction } from "./abstractions/state.service";
import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service";
export class StateService
@browserSession
export class BrowserStateService
extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction
{
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAs: "record",
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
@sessionSync({ ctor: String })
protected activeAccountSubject: BehaviorSubject<string>;
@sessionSync({ ctor: Boolean })
protected activeAccountUnlockedSubject: BehaviorSubject<boolean>;
protected accountDeserializer = Account.fromJSON;
async hasInSessionMemory(key: string): Promise<boolean> {
return await this.memoryStorageService.has(key);
}
async getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T> {
return this.memoryStorageService instanceof AbstractCachedStorageService
? await this.memoryStorageService.getBypassCache<T>(key, { deserializer: deserializer })
@@ -44,7 +60,6 @@ export class StateService
);
}
@withPrototype(BrowserGroupingsComponentState)
async getBrowserGroupingComponentState(
options?: StorageOptions
): Promise<BrowserGroupingsComponentState> {
@@ -67,7 +82,6 @@ export class StateService
);
}
@withPrototype(BrowserComponentState)
async getBrowserVaultItemsComponentState(
options?: StorageOptions
): Promise<BrowserComponentState> {
@@ -90,7 +104,6 @@ export class StateService
);
}
@withPrototype(BrowserSendComponentState)
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -111,7 +124,6 @@ export class StateService
);
}
@withPrototype(BrowserComponentState)
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@@ -150,6 +150,10 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
return Promise.resolve(BrowserApi.getApplicationVersion());
}
async getApplicationVersionNumber(): Promise<string> {
return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
}
supportsWebAuthn(win: Window): boolean {
return typeof PublicKeyCredential !== "undefined";
}

View File

@@ -139,7 +139,7 @@ Bitwarden offers Teams and Enterprise plans for companies so you can securely sh
Why Choose Bitwarden:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private.
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private.
Built-in Password Generator
Generate strong, unique, and random passwords based on security requirements for every website you frequent.

View File

@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden Ilmainen salasanojen hallinta</value>
<value>Bitwarden Free Password Manager</value>
</data>
<data name="Summary" xml:space="preserve">
<value>Turvallinen ja ilmainen salasanojen hallinta kaikille laitteillesi</value>

View File

@@ -148,7 +148,8 @@ Traductions mondiales
Les traductions de Bitwarden existent dans 40 langues et ne cessent de croître, grâce à notre communauté mondiale.
Applications multiplateformes
Sécurisez et partagez des données sensibles dans votre coffre-fort Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore.</value>
Sécurisez et partagez des données sensibles dans votre coffre-fort Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore.
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils</value>

View File

@@ -124,31 +124,31 @@
<value>Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја</value>
</data>
<data name="Description" xml:space="preserve">
<value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC.
<value>Bitwarden, Inc. је матична компанија фирме 8bit Solutions LLC.
NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE.
Именован као најбољи управљач лозинкама од стране новинских сајтова као што су THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, и других.
Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go.
Управљајте, чувајте, обезбедите, и поделите неограничен број лозинки са неограниченог броја уређаја где год да се налазите. Bitwarden свима доноси решења за управљање лозинкама која су отвореног кода, било да сте код куће, на послу, или на путу.
Generate strong, unique, and random passwords based on security requirements for every website you frequent.
Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете.
Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone.
Bitwarden Send брзо преноси шифроване информације--- датотеке и обичан текст-- директно и свима.
Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues.
Bitwarden нуди планове за компаније и предузећа како бисте могли безбедно да делите лозинке са вашим колегама.
Why Choose Bitwarden:
Зашто изабрати Bitwarden:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private.
Шифровање светске класе
Лозинке су заштићене напредним шифровањем од једног до другог краја (AES-256 bit, salted hashing, и PBKDF2 SHA-256) како би ваши подаци остали безбедни и приватни.
Built-in Password Generator
Generate strong, unique, and random passwords based on security requirements for every website you frequent.
Уграђен генератор лозинки
Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете.
Global Translations
Bitwarden translations exist in 40 languages and are growing, thanks to our global community.
Глобално преведен
Bitwarden преводи постоје за 40 језика и стално се унапређују, захваљујући нашој глобалној заједници.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Вишеплатформне апликације
Обезбедите и поделите осетљиве податке у вашем Bitwarden сефу из било ког претраживача, мобилног уређаја, или desktop оперативног система, и других.
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@@ -15,6 +15,7 @@
}
},
"angularCompilerOptions": {
"strictTemplates": true,
"preserveWhitespaces": true
},
"include": ["src", "../../libs/common/src/services/**/*.worker.ts"]

View File

@@ -11,7 +11,7 @@ The Bitwarden CLI is a powerful, full-featured command-line interface (CLI) tool
## Developer Documentation
Please refer to the [CLI section](https://contributing.bitwarden.com/clients/cli/) 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 [CLI section](https://contributing.bitwarden.com/docs/clients/cli/) 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.
## User Documentation

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/cli",
"version": "2022.10.0",
"version": "2022.11.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/cli",
"version": "2022.10.0",
"version": "2022.11.0",
"license": "GPL-3.0-only",
"dependencies": {
"@koa/multer": "^3.0.0",

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.",
"version": "2022.10.0",
"version": "2022.11.0",
"keywords": [
"bitwarden",
"password",

View File

@@ -1,104 +1,109 @@
import * as fetch from "node-fetch";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response";
const CLIENTS_RELEASE_LIST_ENDPOINT = "https://api.github.com/repos/bitwarden/clients/releases";
const DEFAULT_DOWNLOAD_URL = "https://github.com/bitwarden/clients/releases";
const UPDATE_COMMAND = "npm install -g @bitwarden/cli";
export class UpdateCommand {
inPkg = false;
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private repoName: string,
private executableName: string,
private showExtendedMessage: boolean
) {
constructor(private platformUtilsService: PlatformUtilsService) {
this.inPkg = !!(process as any).pkg;
}
async run(): Promise<Response> {
const currentVersion = await this.platformUtilsService.getApplicationVersion();
const response = await fetch.default(
"https://api.github.com/repos/bitwarden/" + this.repoName + "/releases/latest"
);
if (response.status === 200) {
const responseJson = await response.json();
const res = new MessageResponse(null, null);
const tagName: string = responseJson.tag_name;
if (tagName === "v" + currentVersion) {
res.title = "No update available.";
res.noColor = true;
return Response.success(res);
}
let downloadUrl: string = null;
if (responseJson.assets != null) {
for (const a of responseJson.assets) {
const download: string = a.browser_download_url;
if (download == null) {
continue;
}
if (download.indexOf(".zip") === -1) {
continue;
}
if (
process.platform === "win32" &&
download.indexOf(this.executableName + "-windows") > -1
) {
downloadUrl = download;
break;
} else if (
process.platform === "darwin" &&
download.indexOf(this.executableName + "-macos") > -1
) {
downloadUrl = download;
break;
} else if (
process.platform === "linux" &&
download.indexOf(this.executableName + "-linux") > -1
) {
downloadUrl = download;
break;
}
}
}
res.title = "A new version is available: " + tagName;
if (downloadUrl == null) {
downloadUrl = "https://github.com/bitwarden/" + this.repoName + "/releases";
} else {
res.raw = downloadUrl;
}
res.message = "";
if (responseJson.body != null && responseJson.body !== "") {
res.message = responseJson.body + "\n\n";
}
res.message += "You can download this update at " + downloadUrl;
if (this.showExtendedMessage) {
if (this.inPkg) {
res.message +=
"\n\nIf you installed this CLI through a package manager " +
"you should probably update using its update command instead.";
} else {
res.message +=
"\n\nIf you installed this CLI through NPM " +
"you should update using `npm install -g @bitwarden/" +
this.repoName +
"`";
}
}
return Response.success(res);
} else {
const response = await fetch.default(CLIENTS_RELEASE_LIST_ENDPOINT);
if (response.status !== 200) {
return Response.error("Error contacting update API: " + response.status);
}
const responseJson = await response.json();
const cliRelease = responseJson.find((r: any) => r.tag_name.includes("cli"));
if (cliRelease === undefined || cliRelease === null) {
return Response.error("Could not find latest CLI version.");
}
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (cliRelease.tag_name === "cli-v" + currentVersion) {
const response = new MessageResponse(null, null);
response.title = "No update available.";
response.noColor = true;
return Response.success(response);
}
const res = this.getFoundUpdateResponse(cliRelease);
return Response.success(res);
}
private getFoundUpdateResponse(release: any) {
const downloadUrl = this.getDownloadUrl(release.assets);
const response = new MessageResponse(null, null);
response.title = "A new version is available: " + release.tag_name;
response.raw = downloadUrl;
response.message = this.getMessage(release, downloadUrl);
return response;
}
private getMessage(release: any, downloadUrl: string) {
let message = "";
if (release.body != null && release.body !== "") {
message = release.body + "\n\n";
}
message += "You can download this update at " + downloadUrl;
if (this.inPkg) {
message +=
"\n\nIf you installed this CLI through a package manager " +
"you should probably update using its update command instead.";
} else {
message +=
"\n\nIf you installed this CLI through NPM " +
"you should update using `" +
UPDATE_COMMAND +
"`";
}
return message;
}
private getDownloadUrl(assets: any) {
if (assets == null) {
return DEFAULT_DOWNLOAD_URL;
}
let downloadUrl: string = DEFAULT_DOWNLOAD_URL;
for (const a of assets) {
const download: string = a.browser_download_url;
if (download == null) {
continue;
}
if (download.indexOf(".zip") === -1) {
continue;
}
if (process.platform === "win32" && download.indexOf("bw-windows") > -1) {
downloadUrl = download;
break;
} else if (process.platform === "darwin" && download.indexOf("bw-macos") > -1) {
downloadUrl = download;
break;
} else if (process.platform === "linux" && download.indexOf("bw-linux") > -1) {
downloadUrl = download;
break;
}
}
return downloadUrl;
}
}

View File

@@ -403,13 +403,7 @@ export class Program {
writeLn("", true);
})
.action(async () => {
const command = new UpdateCommand(
this.main.platformUtilsService,
this.main.i18nService,
"cli",
"bw",
true
);
const command = new UpdateCommand(this.main.platformUtilsService);
const response = await command.run();
this.processResponse(response);
});

View File

@@ -88,6 +88,10 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(this.packageJson.version);
}
async getApplicationVersionNumber(): Promise<string> {
return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
}
getApplicationVersionSync(): string {
return this.packageJson.version;
}

View File

@@ -12,4 +12,4 @@ The Bitwarden desktop app is written using Electron and Angular. The application
## Documentation
Please refer to the [Desktop section](https://contributing.bitwarden.com/clients/desktop/) 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 [Desktop section](https://contributing.bitwarden.com/docs/clients/desktop/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.

View File

@@ -5,9 +5,9 @@ import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { CredentialCreatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import { CredentialCreatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("name", {

View File

@@ -5,8 +5,8 @@ import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("uri", {

View File

@@ -5,9 +5,9 @@ import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { CredentialUpdatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import { CredentialUpdatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import * as config from "../variables";
// Command line arguments

View File

@@ -5,8 +5,8 @@ import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("userId", {

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