mirror of
https://github.com/bitwarden/browser
synced 2026-01-28 23:33:27 +00:00
Merge branch 'main' into beeep/dev-container
This commit is contained in:
@@ -158,7 +158,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "text",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -346,7 +346,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "text",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -379,7 +379,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "password",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -588,7 +588,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"aria-disabled": false,
|
||||
"aria-haspopup": false,
|
||||
"aria-hidden": false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
checked: false,
|
||||
"data-stripe": null,
|
||||
disabled: false,
|
||||
@@ -621,7 +621,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"aria-disabled": false,
|
||||
"aria-haspopup": false,
|
||||
"aria-hidden": false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
checked: false,
|
||||
"data-stripe": null,
|
||||
disabled: false,
|
||||
@@ -2507,9 +2507,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"class",
|
||||
"tabindex",
|
||||
"title",
|
||||
"value",
|
||||
"rel",
|
||||
"tagname",
|
||||
"checked",
|
||||
"disabled",
|
||||
"readonly",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AUTOFILL_ATTRIBUTES } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillForm from "../models/autofill-form";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -242,10 +244,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this._autofillFormElements.set(formElement, {
|
||||
opid: formElement.opid,
|
||||
htmlAction: this.getFormActionAttribute(formElement),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(formElement, "class"),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, "id"),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.NAME),
|
||||
htmlClass: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.CLASS),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.ID),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.METHOD),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,7 +262,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
* @private
|
||||
*/
|
||||
private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string {
|
||||
return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href;
|
||||
return new URL(
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ACTION),
|
||||
globalThis.location.href,
|
||||
).href;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +340,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
return priorityFormFields;
|
||||
}
|
||||
|
||||
const fieldType = this.getPropertyOrAttribute(element, "type")?.toLowerCase();
|
||||
const fieldType = this.getPropertyOrAttribute(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.TYPE,
|
||||
)?.toLowerCase();
|
||||
if (unimportantFieldTypesSet.has(fieldType)) {
|
||||
unimportantFormFields.push(element);
|
||||
continue;
|
||||
@@ -384,11 +392,11 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
elementNumber: index,
|
||||
maxLength: this.getAutofillFieldMaxLength(element),
|
||||
viewable: await this.domElementVisibilityService.isElementViewable(element),
|
||||
htmlID: this.getPropertyOrAttribute(element, "id"),
|
||||
htmlName: this.getPropertyOrAttribute(element, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
||||
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
||||
title: this.getPropertyOrAttribute(element, "title"),
|
||||
htmlID: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ID),
|
||||
htmlName: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.NAME),
|
||||
htmlClass: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.CLASS),
|
||||
tabindex: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.TABINDEX),
|
||||
title: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.TITLE),
|
||||
tagName: this.getAttributeLowerCase(element, "tagName"),
|
||||
dataSetValues: this.getDataSetValues(element),
|
||||
};
|
||||
@@ -404,16 +412,16 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
}
|
||||
|
||||
let autofillFieldLabels = {};
|
||||
const elementType = this.getAttributeLowerCase(element, "type");
|
||||
const elementType = this.getAttributeLowerCase(element, AUTOFILL_ATTRIBUTES.TYPE);
|
||||
if (elementType !== "hidden") {
|
||||
autofillFieldLabels = {
|
||||
"label-tag": this.createAutofillFieldLabelTag(element as FillableFormFieldElement),
|
||||
"label-data": this.getPropertyOrAttribute(element, "data-label"),
|
||||
"label-aria": this.getPropertyOrAttribute(element, "aria-label"),
|
||||
"label-data": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.DATA_LABEL),
|
||||
"label-aria": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ARIA_LABEL),
|
||||
"label-top": this.createAutofillFieldTopLabel(element),
|
||||
"label-right": this.createAutofillFieldRightLabel(element),
|
||||
"label-left": this.createAutofillFieldLeftLabel(element),
|
||||
placeholder: this.getPropertyOrAttribute(element, "placeholder"),
|
||||
placeholder: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.PLACEHOLDER),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -421,21 +429,21 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
const autofillField = {
|
||||
...autofillFieldBase,
|
||||
...autofillFieldLabels,
|
||||
rel: this.getPropertyOrAttribute(element, "rel"),
|
||||
rel: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.REL),
|
||||
type: elementType,
|
||||
value: this.getElementValue(element),
|
||||
checked: this.getAttributeBoolean(element, "checked"),
|
||||
checked: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.CHECKED),
|
||||
autoCompleteType: this.getAutoCompleteAttribute(element),
|
||||
disabled: this.getAttributeBoolean(element, "disabled"),
|
||||
readonly: this.getAttributeBoolean(element, "readonly"),
|
||||
disabled: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.DISABLED),
|
||||
readonly: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.READONLY),
|
||||
selectInfo: elementIsSelectElement(element)
|
||||
? this.getSelectElementOptions(element as HTMLSelectElement)
|
||||
: null,
|
||||
form: fieldFormElement ? this.getPropertyOrAttribute(fieldFormElement, "opid") : null,
|
||||
"aria-hidden": this.getAttributeBoolean(element, "aria-hidden", true),
|
||||
"aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
|
||||
"aria-hidden": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_HIDDEN, true),
|
||||
"aria-disabled": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_DISABLED, true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_HASPOPUP, true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.DATA_STRIPE),
|
||||
};
|
||||
|
||||
this.cacheAutofillFieldElement(index, element, autofillField);
|
||||
@@ -467,9 +475,9 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
*/
|
||||
private getAutoCompleteAttribute(element: ElementWithOpId<FormFieldElement>): string {
|
||||
return (
|
||||
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocomplete")
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.AUTOCOMPLETE) ||
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.X_AUTOCOMPLETE_TYPE) ||
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.AUTOCOMPLETE_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -957,6 +965,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this.mutationObserver = new MutationObserver(this.handleMutationObserverMutation);
|
||||
this.mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
/** Mutations to node attributes NOT on this list will not be observed! */
|
||||
attributeFilter: Object.values(AUTOFILL_ATTRIBUTES),
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
@@ -1321,6 +1331,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
action: () => (dataTarget.htmlAction = this.getFormActionAttribute(element)),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
method: () => updateAttribute("htmlMethod"),
|
||||
};
|
||||
|
||||
@@ -1350,29 +1361,49 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
|
||||
};
|
||||
const updateActions: Record<string, CallableFunction> = {
|
||||
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
tabindex: () => updateAttribute("tabindex"),
|
||||
title: () => updateAttribute("tabindex"),
|
||||
rel: () => updateAttribute("rel"),
|
||||
tagname: () => (dataTarget.tagName = this.getAttributeLowerCase(element, "tagName")),
|
||||
type: () => (dataTarget.type = this.getAttributeLowerCase(element, "type")),
|
||||
value: () => (dataTarget.value = this.getElementValue(element)),
|
||||
checked: () => (dataTarget.checked = this.getAttributeBoolean(element, "checked")),
|
||||
disabled: () => (dataTarget.disabled = this.getAttributeBoolean(element, "disabled")),
|
||||
readonly: () => (dataTarget.readonly = this.getAttributeBoolean(element, "readonly")),
|
||||
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
"data-label": () => updateAttribute("label-data"),
|
||||
"aria-describedby": () => updateAttribute(AUTOFILL_ATTRIBUTES.ARIA_DESCRIBEDBY),
|
||||
"aria-label": () => updateAttribute("label-aria"),
|
||||
"aria-labelledby": () => updateAttribute(AUTOFILL_ATTRIBUTES.ARIA_LABELLEDBY),
|
||||
"aria-hidden": () =>
|
||||
(dataTarget["aria-hidden"] = this.getAttributeBoolean(element, "aria-hidden", true)),
|
||||
(dataTarget["aria-hidden"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_HIDDEN,
|
||||
true,
|
||||
)),
|
||||
"aria-disabled": () =>
|
||||
(dataTarget["aria-disabled"] = this.getAttributeBoolean(element, "aria-disabled", true)),
|
||||
(dataTarget["aria-disabled"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_DISABLED,
|
||||
true,
|
||||
)),
|
||||
"aria-haspopup": () =>
|
||||
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(element, "aria-haspopup", true)),
|
||||
"data-stripe": () => updateAttribute("data-stripe"),
|
||||
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_HASPOPUP,
|
||||
true,
|
||||
)),
|
||||
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
autocompletetype: () =>
|
||||
(dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
"x-autocompletetype": () =>
|
||||
(dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
checked: () =>
|
||||
(dataTarget.checked = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.CHECKED)),
|
||||
"data-label": () => updateAttribute("label-data"),
|
||||
"data-stripe": () => updateAttribute(AUTOFILL_ATTRIBUTES.DATA_STRIPE),
|
||||
disabled: () =>
|
||||
(dataTarget.disabled = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.DISABLED)),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
placeholder: () => updateAttribute(AUTOFILL_ATTRIBUTES.PLACEHOLDER),
|
||||
readonly: () =>
|
||||
(dataTarget.readonly = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.READONLY)),
|
||||
rel: () => updateAttribute(AUTOFILL_ATTRIBUTES.REL),
|
||||
tabindex: () => updateAttribute(AUTOFILL_ATTRIBUTES.TABINDEX),
|
||||
title: () => updateAttribute(AUTOFILL_ATTRIBUTES.TITLE),
|
||||
type: () => (dataTarget.type = this.getAttributeLowerCase(element, AUTOFILL_ATTRIBUTES.TYPE)),
|
||||
};
|
||||
|
||||
if (!updateActions[attributeName]) {
|
||||
|
||||
@@ -28,6 +28,41 @@ export const EVENTS = {
|
||||
SUBMIT: "submit",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTML attributes observed by the MutationObserver for autofill form/field tracking.
|
||||
* If you need to observe a new attribute, add it here.
|
||||
*/
|
||||
export const AUTOFILL_ATTRIBUTES = {
|
||||
ACTION: "action",
|
||||
ARIA_DESCRIBEDBY: "aria-describedby",
|
||||
ARIA_DISABLED: "aria-disabled",
|
||||
ARIA_HASPOPUP: "aria-haspopup",
|
||||
ARIA_HIDDEN: "aria-hidden",
|
||||
ARIA_LABEL: "aria-label",
|
||||
ARIA_LABELLEDBY: "aria-labelledby",
|
||||
AUTOCOMPLETE: "autocomplete",
|
||||
AUTOCOMPLETE_TYPE: "autocompletetype",
|
||||
X_AUTOCOMPLETE_TYPE: "x-autocompletetype",
|
||||
CHECKED: "checked",
|
||||
CLASS: "class",
|
||||
DATA_LABEL: "data-label",
|
||||
DATA_STRIPE: "data-stripe",
|
||||
DISABLED: "disabled",
|
||||
ID: "id",
|
||||
MAXLENGTH: "maxlength",
|
||||
METHOD: "method",
|
||||
NAME: "name",
|
||||
PLACEHOLDER: "placeholder",
|
||||
POPOVER: "popover",
|
||||
POPOVERTARGET: "popovertarget",
|
||||
POPOVERTARGETACTION: "popovertargetaction",
|
||||
READONLY: "readonly",
|
||||
REL: "rel",
|
||||
TABINDEX: "tabindex",
|
||||
TITLE: "title",
|
||||
TYPE: "type",
|
||||
} as const;
|
||||
|
||||
export const ClearClipboardDelay = {
|
||||
Never: null as null,
|
||||
TenSeconds: 10,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { KeePass2XmlImporter } from "./keepass2-xml-importer";
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
TestData,
|
||||
TestData1,
|
||||
TestData2,
|
||||
TestDataWithProtectedFields,
|
||||
} from "./spec-data/keepass2-xml/keepass2-xml-importer-testdata";
|
||||
|
||||
describe("KeePass2 Xml Importer", () => {
|
||||
@@ -43,4 +45,73 @@ describe("KeePass2 Xml Importer", () => {
|
||||
const result = await importer.parse(TestData2);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
describe("protected fields handling", () => {
|
||||
it("should import protected custom fields as hidden fields", async () => {
|
||||
const importer = new KeePass2XmlImporter();
|
||||
const result = await importer.parse(TestDataWithProtectedFields);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toBe("Test Entry");
|
||||
expect(cipher.login.username).toBe("testuser");
|
||||
expect(cipher.login.password).toBe("testpass");
|
||||
expect(cipher.notes).toContain("Regular notes");
|
||||
|
||||
// Check that protected custom field is imported as hidden field
|
||||
const protectedField = cipher.fields.find((f) => f.name === "SAFE UN-LOCKING instructions");
|
||||
expect(protectedField).toBeDefined();
|
||||
expect(protectedField?.value).toBe("Secret instructions here");
|
||||
expect(protectedField?.type).toBe(FieldType.Hidden);
|
||||
|
||||
// Check that regular custom field is imported as text field
|
||||
const regularField = cipher.fields.find((f) => f.name === "CustomField");
|
||||
expect(regularField).toBeDefined();
|
||||
expect(regularField?.value).toBe("Custom value");
|
||||
expect(regularField?.type).toBe(FieldType.Text);
|
||||
});
|
||||
|
||||
it("should import long protected fields as hidden fields (not appended to notes)", async () => {
|
||||
const importer = new KeePass2XmlImporter();
|
||||
const result = await importer.parse(TestDataWithProtectedFields);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
|
||||
// Long protected field should be imported as hidden field
|
||||
const longField = cipher.fields.find((f) => f.name === "LongProtectedField");
|
||||
expect(longField).toBeDefined();
|
||||
expect(longField?.type).toBe(FieldType.Hidden);
|
||||
expect(longField?.value).toContain("This is a very long protected field");
|
||||
|
||||
// Should not be appended to notes
|
||||
expect(cipher.notes).not.toContain("LongProtectedField");
|
||||
});
|
||||
|
||||
it("should import multiline protected fields as hidden fields (not appended to notes)", async () => {
|
||||
const importer = new KeePass2XmlImporter();
|
||||
const result = await importer.parse(TestDataWithProtectedFields);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
|
||||
// Multiline protected field should be imported as hidden field
|
||||
const multilineField = cipher.fields.find((f) => f.name === "MultilineProtectedField");
|
||||
expect(multilineField).toBeDefined();
|
||||
expect(multilineField?.type).toBe(FieldType.Hidden);
|
||||
expect(multilineField?.value).toContain("Line 1");
|
||||
|
||||
// Should not be appended to notes
|
||||
expect(cipher.notes).not.toContain("MultilineProtectedField");
|
||||
});
|
||||
|
||||
it("should not append protected custom fields to notes", async () => {
|
||||
const importer = new KeePass2XmlImporter();
|
||||
const result = await importer.parse(TestDataWithProtectedFields);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.notes).not.toContain("SAFE UN-LOCKING instructions");
|
||||
expect(cipher.notes).not.toContain("Secret instructions here");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { ImportResult } from "../models/import-result";
|
||||
@@ -92,16 +93,26 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer {
|
||||
} else if (key === "Notes") {
|
||||
cipher.notes += value + "\n";
|
||||
} else {
|
||||
let type = FieldType.Text;
|
||||
const attrs = valueEl.attributes as any;
|
||||
if (
|
||||
const isProtected =
|
||||
attrs.length > 0 &&
|
||||
attrs.ProtectInMemory != null &&
|
||||
attrs.ProtectInMemory.value === "True"
|
||||
) {
|
||||
type = FieldType.Hidden;
|
||||
attrs.ProtectInMemory.value === "True";
|
||||
|
||||
if (isProtected) {
|
||||
// Protected fields should always be imported as hidden fields,
|
||||
// regardless of length or newlines (fixes #16897)
|
||||
if (cipher.fields == null) {
|
||||
cipher.fields = [];
|
||||
}
|
||||
const field = new FieldView();
|
||||
field.type = FieldType.Hidden;
|
||||
field.name = key;
|
||||
field.value = value;
|
||||
cipher.fields.push(field);
|
||||
} else {
|
||||
this.processKvp(cipher, key, value, FieldType.Text);
|
||||
}
|
||||
this.processKvp(cipher, key, value, type);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.notes = this.getValueOrDefault(value.Note);
|
||||
cipher.name = this.getValueOrDefault(value.Name, "--");
|
||||
cipher.login.username = this.getValueOrDefault(value.Login);
|
||||
cipher.login.password = this.getValueOrDefault(value.Pwd);
|
||||
cipher.login.uris = this.makeUriArray(value.Url);
|
||||
cipher.login.password =
|
||||
this.getValueOrDefault(value.Pwd) ?? this.getValueOrDefault(value.Password);
|
||||
cipher.login.uris = this.makeUriArray(value.Url) ?? this.makeUriArray(value.URL);
|
||||
|
||||
if (!this.isNullOrWhitespace(value.Rf_fields)) {
|
||||
this.parseRfFields(cipher, value);
|
||||
|
||||
@@ -354,6 +354,57 @@ line2</Value>
|
||||
</Group>
|
||||
<DeletedObjects />
|
||||
</KeePassFile>`;
|
||||
export const TestDataWithProtectedFields = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<KeePassFile>
|
||||
<Root>
|
||||
<Group>
|
||||
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
|
||||
<Name>Root</Name>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Test Entry</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>testuser</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">testpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>https://example.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>Regular notes</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>SAFE UN-LOCKING instructions</Key>
|
||||
<Value ProtectInMemory="True">Secret instructions here</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>CustomField</Key>
|
||||
<Value>Custom value</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>LongProtectedField</Key>
|
||||
<Value ProtectInMemory="True">This is a very long protected field value that exceeds 200 characters. It contains sensitive information that should be imported as a hidden field and not appended to the notes section. This text is long enough to trigger the old behavior.</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>MultilineProtectedField</Key>
|
||||
<Value ProtectInMemory="True">Line 1
|
||||
Line 2
|
||||
Line 3</Value>
|
||||
</String>
|
||||
</Entry>
|
||||
</Group>
|
||||
</Root>
|
||||
</KeePassFile>`;
|
||||
|
||||
export const TestData2 = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<Meta>
|
||||
<Generator>KeePass</Generator>
|
||||
|
||||
118
package-lock.json
generated
118
package-lock.json
generated
@@ -14,15 +14,15 @@
|
||||
"libs/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/animations": "20.3.16",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/forms": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/compiler": "20.3.16",
|
||||
"@angular/core": "20.3.16",
|
||||
"@angular/forms": "20.3.16",
|
||||
"@angular/platform-browser": "20.3.16",
|
||||
"@angular/platform-browser-dynamic": "20.3.16",
|
||||
"@angular/router": "20.3.16",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.470",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.470",
|
||||
"@electron/fuses": "1.8.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@angular-devkit/build-angular": "20.3.12",
|
||||
"@angular-eslint/schematics": "20.7.0",
|
||||
"@angular/cli": "20.3.12",
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@angular/compiler-cli": "20.3.16",
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@compodoc/compodoc": "1.1.32",
|
||||
@@ -2203,9 +2203,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.15.tgz",
|
||||
"integrity": "sha512-ikyKfhkxoqQA6JcBN0B9RaN6369sM1XYX81Id0lI58dmWCe7gYfrTp8ejqxxKftl514psQO3pkW8Gn1nJ131Gw==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.16.tgz",
|
||||
"integrity": "sha512-N83/GFY5lKNyWgPV3xHHy2rb3/eP1ZLzSVI+dmMVbf3jbqwY1YPQcMiAG8UDzaILY1Dkus91kWLF8Qdr3nHAzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2214,7 +2214,7 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "20.3.15"
|
||||
"@angular/core": "20.3.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build": {
|
||||
@@ -2627,9 +2627,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz",
|
||||
"integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.16.tgz",
|
||||
"integrity": "sha512-GRAziNlntwdnJy3F+8zCOvDdy7id0gITjDnM6P9+n2lXvtDuBLGJKU3DWBbvxcCjtD6JK/g/rEX5fbCxbUHkQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2638,14 +2638,14 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/core": "20.3.16",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz",
|
||||
"integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.16.tgz",
|
||||
"integrity": "sha512-Pt9Ms9GwTThgzdxWBwMfN8cH1JEtQ2DK5dc2yxYtPSaD+WKmG9AVL1PrzIYQEbaKcWk2jxASUHpEWSlNiwo8uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2655,9 +2655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler-cli": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.15.tgz",
|
||||
"integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.16.tgz",
|
||||
"integrity": "sha512-l3xF/fXfJAl/UrNnH9Ufkr79myjMgXdHq1mmmph2UnpeqilRB1b8lC9sLBV9MipQHVn3dwocxMIvtrcryfOaXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2678,7 +2678,7 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/compiler": "20.3.16",
|
||||
"typescript": ">=5.8 <6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -2864,9 +2864,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz",
|
||||
"integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.16.tgz",
|
||||
"integrity": "sha512-KSFPKvOmWWLCJBbEO+CuRUXfecX2FRuO0jNi9c54ptXMOPHlK1lIojUnyXmMNzjdHgRug8ci9qDuftvC2B7MKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2875,7 +2875,7 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/compiler": "20.3.16",
|
||||
"rxjs": "^6.5.3 || ^7.4.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
@@ -2889,9 +2889,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.15.tgz",
|
||||
"integrity": "sha512-gS5hQkinq52pm/7mxz4yHPCzEcmRWjtUkOVddPH0V1BW/HMni/p4Y6k2KqKBeGb9p8S5EAp6PDxDVLOPukp3mg==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.16.tgz",
|
||||
"integrity": "sha512-1yzbXpExTqATpVcqA3wGrq4ACFIP3mRxA4pbso5KoJU+/4JfzNFwLsDaFXKpm5uxwchVnj8KM2vPaDOkvtp7NA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2900,16 +2900,16 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/core": "20.3.16",
|
||||
"@angular/platform-browser": "20.3.16",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz",
|
||||
"integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.16.tgz",
|
||||
"integrity": "sha512-YsrLS6vyS77i4pVHg4gdSBW74qvzHjpQRTVQ5Lv/OxIjJdYYYkMmjNalCNgy1ZuyY6CaLIB11ccxhrNnxfKGOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2918,9 +2918,9 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/core": "20.3.15"
|
||||
"@angular/animations": "20.3.16",
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/core": "20.3.16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
@@ -2929,9 +2929,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser-dynamic": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.15.tgz",
|
||||
"integrity": "sha512-RizuRdBt0d6ongQ2y8cr8YsXFyjF8f91vFfpSNw+cFj+oiEmRC1txcWUlH5bPLD9qSDied8qazUi0Tb8VPQDGw==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.16.tgz",
|
||||
"integrity": "sha512-5mECCV9YeKH6ue239GXRTGeDSd/eTbM1j8dDejhm5cGnPBhTxRw4o+GgSrWTYtb6VmIYdwUGBTC+wCBphiaQ2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2940,16 +2940,16 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15"
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/compiler": "20.3.16",
|
||||
"@angular/core": "20.3.16",
|
||||
"@angular/platform-browser": "20.3.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/router": {
|
||||
"version": "20.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.15.tgz",
|
||||
"integrity": "sha512-6+qgk8swGSoAu7ISSY//GatAyCP36hEvvUgvjbZgkXLLH9yUQxdo77ij05aJ5s0OyB25q/JkqS8VTY0z1yE9NQ==",
|
||||
"version": "20.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.16.tgz",
|
||||
"integrity": "sha512-e1LiQFZaajKqc00cY5FboIrWJZSMnZ64GDp5R0UejritYrqorQQQNOqP1W85BMuY2owibMmxVfX+dJg/Mc8PuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2958,9 +2958,9 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/core": "20.3.16",
|
||||
"@angular/platform-browser": "20.3.16",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
@@ -32414,9 +32414,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||
"version": "1.11.8",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz",
|
||||
"integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -34690,9 +34690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ordered-binary": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz",
|
||||
"integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz",
|
||||
"integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
|
||||
18
package.json
18
package.json
@@ -41,7 +41,7 @@
|
||||
"@angular-devkit/build-angular": "20.3.12",
|
||||
"@angular-eslint/schematics": "20.7.0",
|
||||
"@angular/cli": "20.3.12",
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@angular/compiler-cli": "20.3.16",
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@compodoc/compodoc": "1.1.32",
|
||||
@@ -153,15 +153,15 @@
|
||||
"webpack-node-externals": "3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/animations": "20.3.16",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/forms": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@angular/common": "20.3.16",
|
||||
"@angular/compiler": "20.3.16",
|
||||
"@angular/core": "20.3.16",
|
||||
"@angular/forms": "20.3.16",
|
||||
"@angular/platform-browser": "20.3.16",
|
||||
"@angular/platform-browser-dynamic": "20.3.16",
|
||||
"@angular/router": "20.3.16",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.470",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.470",
|
||||
"@electron/fuses": "1.8.0",
|
||||
|
||||
Reference in New Issue
Block a user