1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

Merge branch 'main' into desktop/pm-18768/migrate-vault-cipher-list-fix

This commit is contained in:
Leslie Xiong
2026-01-23 03:25:48 -05:00
36 changed files with 314 additions and 103 deletions

View File

@@ -72,7 +72,6 @@ jobs:
permissions:
id-token: write
contents: write
pull-requests: write
steps:
- name: Validate version input format
@@ -111,8 +110,7 @@ jobs:
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write # for creating, committing to, and pushing new branches
permission-pull-requests: write # for generating pull requests
permission-contents: write # for committing and pushing to main (bypasses rulesets)
- name: Check out branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -448,53 +446,15 @@ jobs:
echo "No changes to commit!";
fi
- name: Create version bump branch
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
BRANCH_NAME="version-bump-$(date +%s)"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Commit version bumps with GPG signature
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
git commit -m "Bumped client version(s)" -a
- name: Push version bump branch
- name: Push changes to main
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
git push --set-upstream origin "$BRANCH_NAME"
- name: Create Pull Request for version bump
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
VERSION_BROWSER: ${{ steps.set-final-version-output.outputs.version_browser }}
VERSION_CLI: ${{ steps.set-final-version-output.outputs.version_cli }}
VERSION_DESKTOP: ${{ steps.set-final-version-output.outputs.version_desktop }}
VERSION_WEB: ${{ steps.set-final-version-output.outputs.version_web }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const versions = [];
if (process.env.VERSION_BROWSER) versions.push(`- Browser: ${process.env.VERSION_BROWSER}`);
if (process.env.VERSION_CLI) versions.push(`- CLI: ${process.env.VERSION_CLI}`);
if (process.env.VERSION_DESKTOP) versions.push(`- Desktop: ${process.env.VERSION_DESKTOP}`);
if (process.env.VERSION_WEB) versions.push(`- Web: ${process.env.VERSION_WEB}`);
const body = versions.length > 0
? `Automated version bump:\n\n${versions.join('\n')}`
: 'Automated version bump';
const { data: pr } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Bumped client version(s)',
body: body,
head: process.env.BRANCH_NAME,
base: context.ref.replace('refs/heads/', '')
});
console.log(`Created PR #${pr.number}: ${pr.html_url}`);
git push
cut_branch:
name: Cut branch

View File

@@ -96,7 +96,9 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
*/
async getPageDetails(): Promise<AutofillPageDetails> {
// Set up listeners on top-layer candidates that predate Mutation Observer setup
this.setupInitialTopLayerListeners();
if (this.autofillOverlayContentService) {
this.setupInitialTopLayerListeners();
}
if (!this.mutationObserver) {
this.setupMutationObserver();
@@ -1072,19 +1074,21 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
}
private setupTopLayerCandidateListener = (element: Element) => {
const ownedTags = this.autofillOverlayContentService.getOwnedInlineMenuTagNames() || [];
this.ownedExperienceTagNames = ownedTags;
if (this.autofillOverlayContentService) {
const ownedTags = this.autofillOverlayContentService.getOwnedInlineMenuTagNames() || [];
this.ownedExperienceTagNames = ownedTags;
if (!ownedTags.includes(element.tagName)) {
element.addEventListener("toggle", (event: ToggleEvent) => {
if (event.newState === "open") {
// Add a slight delay (but faster than a user's reaction), to ensure the layer
// positioning happens after any triggered toggle has completed.
setTimeout(this.autofillOverlayContentService.refreshMenuLayerPosition, 100);
}
});
if (!ownedTags.includes(element.tagName)) {
element.addEventListener("toggle", (event: ToggleEvent) => {
if (event.newState === "open") {
// Add a slight delay (but faster than a user's reaction), to ensure the layer
// positioning happens after any triggered toggle has completed.
setTimeout(this.autofillOverlayContentService.refreshMenuLayerPosition, 100);
}
});
this.autofillOverlayContentService.refreshMenuLayerPosition();
this.autofillOverlayContentService.refreshMenuLayerPosition();
}
}
};

View File

@@ -37,7 +37,7 @@ export class PopupSizeService {
/** Begin listening for state changes */
async init() {
this.width$.subscribe((width: PopupWidthOption) => {
PopupSizeService.setStyle(width);
void PopupSizeService.setStyle(width);
localStorage.setItem(PopupSizeService.LocalStorageKey, width);
});
}
@@ -77,8 +77,9 @@ export class PopupSizeService {
}
}
private static setStyle(width: PopupWidthOption) {
if (!BrowserPopupUtils.inPopup(window)) {
private static async setStyle(width: PopupWidthOption) {
const isInTab = await BrowserPopupUtils.isInTab();
if (!BrowserPopupUtils.inPopup(window) || isInTab) {
return;
}
const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default;
@@ -91,6 +92,6 @@ export class PopupSizeService {
**/
static initBodyWidthFromLocalStorage() {
const storedValue = localStorage.getItem(PopupSizeService.LocalStorageKey);
this.setStyle(storedValue as any);
void this.setStyle(storedValue as any);
}
}

View File

@@ -24,12 +24,13 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
const IMPORTERS: &[(&str, &str)] = &[
("arccsv", "Arc"),
("bravecsv", "Brave"),
("chromecsv", "Chrome"),
("chromiumcsv", "Chromium"),
("bravecsv", "Brave"),
("edgecsv", "Microsoft Edge"),
("operacsv", "Opera"),
("vivaldicsv", "Vivaldi"),
("edgecsv", "Microsoft Edge"),
];
let supported: HashSet<&'static str> =
@@ -91,6 +92,7 @@ mod tests {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let expected: HashSet<String> = HashSet::from([
"arccsv".to_string(),
"chromecsv".to_string(),
"chromiumcsv".to_string(),
"bravecsv".to_string(),
@@ -113,6 +115,7 @@ mod tests {
fn macos_specific_loaders_match_const_array() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let ids = [
"arccsv",
"chromecsv",
"chromiumcsv",
"bravecsv",

View File

@@ -12,9 +12,13 @@ if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]; then
export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3"
fi
# A bug in Electron 39 (which now enables Wayland by default) causes a crash on
# systems using Wayland with hardware acceleration. Platform decided to
# configure Electron to use X11 (with an opt-out) until the upstream bug is
# fixed. The follow-up task is https://bitwarden.atlassian.net/browse/PM-31080.
PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto"
if [ "$USE_X11" = "true" ]; then
PARAMS=""
if [ "$USE_X11" != "false" ]; then
PARAMS="--ozone-platform=x11"
fi
$APP_PATH/bitwarden-app $PARAMS "$@"

View File

@@ -32,8 +32,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherType } from "@bitwarden/common/vault/enums";
import { DialogService, ToastService } from "@bitwarden/components";
import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { ApproveSshRequestComponent } from "../components/approve-ssh-request";
import { SshAgentPromptType } from "../models/ssh-agent-setting";
@Injectable({

View File

@@ -17,16 +17,16 @@
[text]="organization.node.name"
[appA11yTitle]="organization.node.name"
(click)="applyFilter($event, organization)"
/>
@if (!organization.node.enabled) {
<span class="tw-ml-auto">
>
@if (!organization.node.enabled) {
<i
slot="end"
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
[attr.aria-label]="'organizationIsDisabled' | i18n"
[appA11yTitle]="'organizationIsDisabled' | i18n"
></i>
</span>
}
}
</bit-nav-item>
}
</bit-nav-group>
}

View File

@@ -109,6 +109,8 @@ import {
All,
VaultItem,
VaultItemEvent,
VaultItemsTransferService,
DefaultVaultItemsTransferService,
} from "@bitwarden/vault";
import { DesktopHeaderComponent } from "../../../app/layout/header/desktop-header.component";
@@ -163,6 +165,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
provide: COPY_CLICK_LISTENER,
useExisting: VaultComponent,
},
{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService },
],
})
export class VaultComponent<C extends CipherViewLike>
@@ -201,6 +204,7 @@ export class VaultComponent<C extends CipherViewLike>
private routedVaultFilterService = inject(RoutedVaultFilterService);
private searchService = inject(SearchService);
private searchPipe = inject(SearchPipe);
private vaultItemTransferService: VaultItemsTransferService = inject(VaultItemsTransferService);
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@@ -376,6 +380,9 @@ export class VaultComponent<C extends CipherViewLike>
if (message.command === "syncCompleted" && message.successfully) {
this.refresh();
}
if (this.activeUserId) {
void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId);
}
});
});
@@ -630,6 +637,8 @@ export class VaultComponent<C extends CipherViewLike>
this.changeDetectorRef.markForCheck();
},
);
void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId);
}
ngOnDestroy() {

View File

@@ -92,6 +92,8 @@ import {
PasswordRepromptService,
CipherFormComponent,
ArchiveCipherUtilitiesService,
VaultItemsTransferService,
DefaultVaultItemsTransferService,
} from "@bitwarden/vault";
import { NavComponent } from "../../../app/layout/nav.component";
@@ -150,6 +152,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
provide: COPY_CLICK_LISTENER,
useExisting: VaultV2Component,
},
{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService },
],
})
export class VaultV2Component<C extends CipherViewLike>
@@ -264,6 +267,7 @@ export class VaultV2Component<C extends CipherViewLike>
private policyService: PolicyService,
private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
private masterPasswordService: MasterPasswordServiceAbstraction,
private vaultItemTransferService: VaultItemsTransferService,
) {}
async ngOnInit() {
@@ -317,6 +321,11 @@ export class VaultV2Component<C extends CipherViewLike>
.catch(() => {});
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
}
if (this.activeUserId) {
void this.vaultItemTransferService.enforceOrganizationDataOwnership(
this.activeUserId,
);
}
break;
case "modalShown":
this.showingModal = true;
@@ -420,6 +429,8 @@ export class VaultV2Component<C extends CipherViewLike>
.subscribe((collections) => {
this.allCollections = collections;
});
void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId);
}
ngOnDestroy() {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,11 +3,11 @@ import { svgIcon } from "../icon-service";
const BitwardenShield = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 32" fill="none">
<g clip-path="url(#bitwarden-shield-clip)">
<path class="tw-fill-text-alt2" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/>
<path class="tw-fill-fg-sidenav-text" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/>
</g>
<defs>
<clipPath id="bitwarden-shield-clip">
<path class="tw-fill-text-alt2" d="M0 0h26v32H0z"/>
<path class="tw-fill-fg-sidenav-text" d="M0 0h26v32H0z" />
</clipPath>
</defs>
</svg>

View File

@@ -71,9 +71,9 @@ const styles: Record<IconButtonType, string[]> = {
primary: ["!tw-text-primary-600", "focus-visible:before:tw-ring-primary-600", ...focusRing],
danger: ["!tw-text-danger-600", "focus-visible:before:tw-ring-primary-600", ...focusRing],
"nav-contrast": [
"!tw-text-alt2",
"!tw-text-fg-sidenav-text",
"hover:!tw-bg-hover-contrast",
"focus-visible:before:tw-ring-text-alt2",
"focus-visible:before:tw-ring-border-focus",
...focusRing,
],
};

View File

@@ -19,7 +19,7 @@
<ng-template #button>
<button
type="button"
class="tw-ms-auto"
class="tw-ms-auto tw-text-fg-sidenav-text"
[ngClass]="{
'tw-transform tw-rotate-[90deg]': variantValue === 'tree' && !open(),
}"

View File

@@ -4,15 +4,15 @@
<div
[style.padding-inline-start]="navItemIndentationPadding()"
class="tw-relative tw-rounded-md tw-h-10"
[class.tw-bg-background-alt4]="showActiveStyles"
[class.tw-bg-background-alt3]="!showActiveStyles"
[class.hover:tw-bg-hover-contrast]="!showActiveStyles"
[class.tw-bg-bg-sidenav-active-item]="showActiveStyles"
[class.tw-bg-bg-sidenav-background]="!showActiveStyles"
[class.hover:tw-bg-bg-sidenav-item-hover]="!showActiveStyles"
[class]="fvwStyles$ | async"
>
<div class="tw-relative tw-flex tw-items-center tw-h-full">
@if (open) {
<div
class="tw-absolute tw-left-[0px] tw-transform tw--translate-x-[calc(100%_+_4px)] [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
class="tw-absolute tw-left-[0px] tw-transform tw--translate-x-[calc(100%_+_4px)] [&>*:focus-visible::before]:!tw-ring-border-focus [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
>
<ng-content select="[slot=start]"></ng-content>
</div>
@@ -31,7 +31,7 @@
>
@if (icon()) {
<i
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon() }}"
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-fg-sidenav-text {{ icon() }}"
[attr.aria-hidden]="open"
[attr.aria-label]="text()"
></i>
@@ -47,7 +47,7 @@
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
<!-- The following `class` field should match the `#isButton` class field below -->
<a
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-fg-sidenav-text hover:tw-text-fg-sidenav-text hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
[class.!tw-ps-0]="variant() === 'tree' || treeDepth() > 0"
data-fvw
[routerLink]="route()"
@@ -68,7 +68,7 @@
<!-- Class field should match `#isAnchor` class field above -->
<button
type="button"
class="tw-size-full tw-px-4 tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-fg-sidenav-text hover:tw-text-fg-sidenav-text hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
[class.!tw-ps-0]="variant() === 'tree' || treeDepth() > 0"
data-fvw
(click)="mainContentClicked.emit()"
@@ -79,7 +79,7 @@
@if (open) {
<div
class="tw-flex tw-items-center tw-pe-1 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
class="tw-flex tw-items-center tw-pe-1 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-border-focus [&>*:hover]:!tw-border-border-focus [&>*]:tw-text-fg-sidenav-text empty:tw-hidden"
>
<ng-content select="[slot=end]"></ng-content>
</div>

View File

@@ -90,7 +90,7 @@ export class NavItemComponent extends NavBaseComponent {
protected focusVisibleWithin$ = new BehaviorSubject(false);
protected fvwStyles$ = this.focusVisibleWithin$.pipe(
map((value) =>
value ? "tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-inset tw-ring-text-alt2" : "",
value ? "tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-inset tw-ring-border-focus" : "",
),
);
@HostListener("focusin", ["$event.target"])

View File

@@ -8,7 +8,7 @@
<!-- absolutely position the link svg to avoid shifting layout when sidenav is closed -->
<a
[routerLink]="route()"
class="tw-relative tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast tw-h-[73px] [&_svg]:tw-absolute [&_svg]:tw-inset-[.6875rem] [&_svg]:tw-w-[200px]"
class="tw-relative tw-p-3 tw-block tw-rounded-md tw-bg-bg-sidenav tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-border-focus hover:tw-bg-bg-sidenav-item-hover tw-h-[73px] [&_svg]:tw-absolute [&_svg]:tw-inset-[.6875rem] [&_svg]:tw-w-[200px]"
[ngClass]="{
'!tw-h-[55px] [&_svg]:!tw-w-[26px]': !sideNavService.open,
}"

View File

@@ -8,14 +8,14 @@
<div class="tw-relative tw-h-full">
<nav
id="bit-side-nav"
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-full tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-full tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-bg-sidenav tw-text-fg-sidenav-text tw-outline-none"
[style.width.rem]="data.open ? (sideNavService.width$ | async) : undefined"
[ngStyle]="
variant() === 'secondary' && {
'--color-text-alt2': 'var(--color-text-main)',
'--color-background-alt3': 'var(--color-secondary-100)',
'--color-background-alt4': 'var(--color-secondary-300)',
'--color-hover-contrast': 'var(--color-hover-default)',
'--color-sidenav-text': 'var(--color-admin-sidenav-text)',
'--color-sidenav-background': 'var(--color-admin-sidenav-background)',
'--color-sidenav-active-item': 'var(--color-admin-sidenav-active-item)',
'--color-sidenav-item-hover': 'var(--color-admin-sidenav-item-hover)',
}
"
[cdkTrapFocus]="data.isOverlay"
@@ -27,7 +27,7 @@
<!-- 53rem = ~850px -->
<!-- This is a magic number. This number was selected by going to the UI and finding the number that felt the best to me and design. No real rhyme or reason :) -->
<div
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3"
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full"
>
<bit-nav-divider></bit-nav-divider>
@if (data.open) {

View File

@@ -353,6 +353,19 @@
/* Focus Border */
--color-border-focus: var(--color-black);
/**========================================
* SIDENAV BACKGROUND COLORS (Light mode)
* ======================================== */
--color-sidenav-background: var(--color-brand-800);
--color-sidenav-text: var(--color-white);
--color-sidenav-active-item: var(--color-brand-900);
--color-sidenav-item-hover: var(--color-brand-900);
--color-admin-sidenav-background: var(--color-gray-100);
--color-admin-sidenav-text: var(--color-gray-900);
--color-admin-sidenav-active-item: var(--color-gray-300);
--color-admin-sidenav-item-hover: var(--color-gray-300);
}
.theme_light {
@@ -542,6 +555,19 @@
/* Focus Border */
--color-border-focus: var(--color-white);
/**========================================
* SIDENAV BACKGROUND COLORS (Dark mode)
* ======================================== */
--color-sidenav-background: var(--color-gray-800);
--color-sidenav-text: var(--color-white);
--color-sidenav-active-item: var(--color-gray-900);
--color-sidenav-item-hover: var(--color-gray-900);
--color-admin-sidenav-background: var(--color-gray-800);
--color-admin-sidenav-text: var(--color-white);
--color-admin-sidenav-active-item: var(--color-gray-900);
--color-admin-sidenav-item-hover: var(--color-gray-900);
}
@layer components {

View File

@@ -72,11 +72,11 @@ module.exports = {
code: rgba("--color-text-code"),
},
background: {
DEFAULT: rgba("--color-background"),
alt: rgba("--color-background-alt"),
alt2: rgba("--color-background-alt2"),
alt3: rgba("--color-background-alt3"),
alt4: rgba("--color-background-alt4"),
DEFAULT: "var(--color-bg-primary)",
alt: "var(--color-bg-tertiary)",
alt2: "var(--color-bg-brand)",
alt3: "var(--color-bg-brand-strong)",
alt4: "var(--color-brand-950)",
},
bg: {
white: "var(--color-bg-white)",
@@ -117,6 +117,9 @@ module.exports = {
"accent-tertiary": "var(--color-bg-accent-tertiary)",
hover: "var(--color-bg-hover)",
overlay: "var(--color-bg-overlay)",
sidenav: "var(--color-sidenav-background)",
"sidenav-active-item": "var(--color-sidenav-active-item)",
"sidenav-item-hover": "var(--color-sidenav-item-hover)",
},
hover: {
default: "var(--color-hover-default)",
@@ -159,6 +162,7 @@ module.exports = {
"accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
"accent-tertiary": "var(--color-fg-accent-tertiary)",
"accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
"sidenav-text": "var(--color-sidenav-text)",
},
border: {
muted: "var(--color-border-muted)",
@@ -253,6 +257,7 @@ module.exports = {
"fg-accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
"fg-accent-tertiary": "var(--color-fg-accent-tertiary)",
"fg-accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
"fg-sidenav-text": "var(--color-sidenav-text)",
}),
borderColor: ({ theme }) => ({
...theme("colors"),

View File

@@ -195,6 +195,8 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
return "Brave";
} else if (format === "vivaldicsv") {
return "Vivaldi";
} else if (format === "arccsv") {
return "Arc";
}
return "Chrome";
}

View File

@@ -0,0 +1,139 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ArcCsvImporter } from "./arc-csv-importer";
import { data as missingNameAndUrlData } from "./spec-data/arc-csv/missing-name-and-url-data.csv";
import { data as missingNameWithUrlData } from "./spec-data/arc-csv/missing-name-with-url-data.csv";
import { data as passwordWithNoteData } from "./spec-data/arc-csv/password-with-note-data.csv";
import { data as simplePasswordData } from "./spec-data/arc-csv/simple-password-data.csv";
import { data as subdomainData } from "./spec-data/arc-csv/subdomain-data.csv";
import { data as urlWithWwwData } from "./spec-data/arc-csv/url-with-www-data.csv";
const CipherData = [
{
title: "should parse password",
csv: simplePasswordData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should parse password with note",
csv: passwordWithNoteData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: "This is a test note",
type: 1,
}),
},
{
title: "should strip www. prefix from name",
csv: urlWithWwwData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://www.example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should extract name from URL when name is missing",
csv: missingNameWithUrlData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/login",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should use -- as name when both name and URL are missing",
csv: missingNameAndUrlData,
expected: Object.assign(new CipherView(), {
name: "--",
login: Object.assign(new LoginView(), {
username: null,
password: "password123",
uris: null,
}),
notes: null,
type: 1,
}),
},
{
title: "should preserve subdomain in name",
csv: subdomainData,
expected: Object.assign(new CipherView(), {
name: "login.example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://login.example.com/auth",
}),
],
}),
notes: null,
type: 1,
}),
},
];
describe("Arc CSV Importer", () => {
CipherData.forEach((data) => {
it(data.title, async () => {
jest.useFakeTimers().setSystemTime(data.expected.creationDate);
const importer = new ArcCsvImporter();
const result = await importer.parse(data.csv);
expect(result != null).toBe(true);
expect(result.ciphers.length).toBeGreaterThan(0);
const cipher = result.ciphers.shift();
let property: keyof typeof data.expected;
for (property in data.expected) {
if (Object.prototype.hasOwnProperty.call(data.expected, property)) {
expect(Object.prototype.hasOwnProperty.call(cipher, property)).toBe(true);
expect(cipher[property]).toEqual(data.expected[property]);
}
}
});
});
});

View File

@@ -0,0 +1,30 @@
import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
export class ArcCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
const url = this.getValueOrDefault(value.url);
cipher.name = this.getValueOrDefault(this.nameFromUrl(url) ?? "", "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.url);
cipher.notes = this.getValueOrDefault(value.note);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,3 +1,4 @@
export { ArcCsvImporter } from "./arc-csv-importer";
export { AscendoCsvImporter } from "./ascendo-csv-importer";
export { AvastCsvImporter, AvastJsonImporter } from "./avast";
export { AviraCsvImporter } from "./avira-csv-importer";

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,,,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,https://example.com/login,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,This is a test note`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
login.example.com,https://login.example.com/auth,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
www.example.com,https://www.example.com/,user@example.com,password123,`;

View File

@@ -46,6 +46,7 @@ export const regularImportOptions = [
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "arccsv", name: "Arc" },
{ id: "edgecsv", name: "Edge" },
{ id: "operacsv", name: "Opera" },
{ id: "vivaldicsv", name: "Vivaldi" },

View File

@@ -31,6 +31,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
import { KeyService } from "@bitwarden/key-management";
import {
ArcCsvImporter,
AscendoCsvImporter,
AvastCsvImporter,
AvastJsonImporter,
@@ -256,6 +257,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "arccsv":
return new ArcCsvImporter();
case "edgecsv":
case "chromecsv":
case "operacsv":