mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
Merge branch 'master' into feature/org-admin-refresh
This commit is contained in:
@@ -363,6 +363,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getCardExpMonthDisplay() {
|
||||
return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name;
|
||||
}
|
||||
|
||||
trackByFunction(index: number, item: any) {
|
||||
return index;
|
||||
}
|
||||
@@ -580,7 +584,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
return this.cipherService.saveWithServer(cipher);
|
||||
return this.cipher.id == null
|
||||
? this.cipherService.createWithServer(cipher)
|
||||
: this.cipherService.updateWithServer(cipher);
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Component, Input, OnChanges, OnInit } from "@angular/core";
|
||||
import { DomSanitizer } from "@angular/platform-browser";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-avatar",
|
||||
template:
|
||||
'<img *ngIf="src" [src]="sanitizer.bypassSecurityTrustResourceUrl(src)" title="{{data}}" ' +
|
||||
"[ngClass]=\"{'rounded-circle': circle}\">",
|
||||
})
|
||||
export class AvatarComponent implements OnChanges, OnInit {
|
||||
@Input() data: string;
|
||||
@Input() email: string;
|
||||
@Input() size = 45;
|
||||
@Input() charCount = 2;
|
||||
@Input() textColor = "#ffffff";
|
||||
@Input() fontSize = 20;
|
||||
@Input() fontWeight = 300;
|
||||
@Input() dynamic = false;
|
||||
@Input() circle = false;
|
||||
|
||||
src: string;
|
||||
|
||||
constructor(
|
||||
public sanitizer: DomSanitizer,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.dynamic) {
|
||||
this.generate();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.dynamic) {
|
||||
this.generate();
|
||||
}
|
||||
}
|
||||
|
||||
private async generate() {
|
||||
const enableGravatars = await this.stateService.getEnableGravitars();
|
||||
if (enableGravatars && this.email != null) {
|
||||
const hashBytes = await this.cryptoFunctionService.hash(
|
||||
this.email.toLowerCase().trim(),
|
||||
"md5"
|
||||
);
|
||||
const hash = Utils.fromBufferToHex(hashBytes).toLowerCase();
|
||||
this.src = "https://www.gravatar.com/avatar/" + hash + "?s=" + this.size + "&r=pg&d=retro";
|
||||
} else {
|
||||
let chars: string = null;
|
||||
const upperData = this.data.toUpperCase();
|
||||
|
||||
if (this.charCount > 1) {
|
||||
chars = this.getFirstLetters(upperData, this.charCount);
|
||||
}
|
||||
if (chars == null) {
|
||||
chars = this.unicodeSafeSubstring(upperData, this.charCount);
|
||||
}
|
||||
|
||||
// If the chars contain an emoji, only show it.
|
||||
if (chars.match(Utils.regexpEmojiPresentation)) {
|
||||
chars = chars.match(Utils.regexpEmojiPresentation)[0];
|
||||
}
|
||||
|
||||
const charObj = this.getCharText(chars);
|
||||
const color = Utils.stringToColor(upperData);
|
||||
const svg = this.getSvg(this.size, color);
|
||||
svg.appendChild(charObj);
|
||||
const html = window.document.createElement("div").appendChild(svg).outerHTML;
|
||||
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
|
||||
this.src = "data:image/svg+xml;base64," + svgHtml;
|
||||
}
|
||||
}
|
||||
|
||||
private getFirstLetters(data: string, count: number): string {
|
||||
const parts = data.split(" ");
|
||||
if (parts.length > 1) {
|
||||
let text = "";
|
||||
for (let i = 0; i < count; i++) {
|
||||
text += this.unicodeSafeSubstring(parts[i], 1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getSvg(size: number, color: string): HTMLElement {
|
||||
const svgTag = window.document.createElement("svg");
|
||||
svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
svgTag.setAttribute("pointer-events", "none");
|
||||
svgTag.setAttribute("width", size.toString());
|
||||
svgTag.setAttribute("height", size.toString());
|
||||
svgTag.style.backgroundColor = color;
|
||||
svgTag.style.width = size + "px";
|
||||
svgTag.style.height = size + "px";
|
||||
return svgTag;
|
||||
}
|
||||
|
||||
private getCharText(character: string): HTMLElement {
|
||||
const textTag = window.document.createElement("text");
|
||||
textTag.setAttribute("text-anchor", "middle");
|
||||
textTag.setAttribute("y", "50%");
|
||||
textTag.setAttribute("x", "50%");
|
||||
textTag.setAttribute("dy", "0.35em");
|
||||
textTag.setAttribute("pointer-events", "auto");
|
||||
textTag.setAttribute("fill", this.textColor);
|
||||
textTag.setAttribute(
|
||||
"font-family",
|
||||
'"Open Sans","Helvetica Neue",Helvetica,Arial,' +
|
||||
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
);
|
||||
textTag.textContent = character;
|
||||
textTag.style.fontWeight = this.fontWeight.toString();
|
||||
textTag.style.fontSize = this.fontSize + "px";
|
||||
return textTag;
|
||||
}
|
||||
|
||||
private unicodeSafeSubstring(str: string, count: number) {
|
||||
const characters = str.match(/./gu);
|
||||
return characters != null ? characters.slice(0, count).join("") : "";
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { AvatarComponent } from "./components/avatar.component";
|
||||
import { CalloutComponent } from "./components/callout.component";
|
||||
import { ExportScopeCalloutComponent } from "./components/export-scope-callout.component";
|
||||
import { IconComponent } from "./components/icon.component";
|
||||
@@ -48,7 +47,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
||||
A11yTitleDirective,
|
||||
ApiActionDirective,
|
||||
AutofocusDirective,
|
||||
AvatarComponent,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
ColorPasswordCountPipe,
|
||||
@@ -78,7 +76,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
||||
A11yTitleDirective,
|
||||
ApiActionDirective,
|
||||
AutofocusDirective,
|
||||
AvatarComponent,
|
||||
BitwardenToastModule,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
|
||||
72
libs/common/spec/importers/chrome-csv-importer.spec.ts
Normal file
72
libs/common/spec/importers/chrome-csv-importer.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ChromeCsvImporter as Importer } from "@bitwarden/common/importers/chrome-csv-importer";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/models/view/login.view";
|
||||
|
||||
import { data as androidData } from "./testData/chromeCsv/android-data.csv";
|
||||
import { data as simplePasswordData } from "./testData/chromeCsv/simple-password-data.csv";
|
||||
|
||||
const CipherData = [
|
||||
{
|
||||
title: "should parse app name",
|
||||
csv: androidData,
|
||||
expected: Object.assign(new CipherView(), {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "com.xyz.example.app.android",
|
||||
login: Object.assign(new LoginView(), {
|
||||
username: "username@example.com",
|
||||
password: "Qh6W4Wz55YGFNU",
|
||||
uris: [
|
||||
Object.assign(new LoginUriView(), {
|
||||
uri: "android://N2H9MndUUUt3JuQSWAKexOU9oJLJeHR4nyUGac5E1TXKppkY7xtdRl6l8vKo1hQWCqAEy4gsNLUBIbVxpdmhOP==@com.xyz.example.app.android/",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
notes: null,
|
||||
type: 1,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: "should parse password",
|
||||
csv: simplePasswordData,
|
||||
expected: Object.assign(new CipherView(), {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "www.example.com",
|
||||
login: Object.assign(new LoginView(), {
|
||||
username: "username@example.com",
|
||||
password: "wpC9qFvsbWQK5Z",
|
||||
uris: [
|
||||
Object.assign(new LoginUriView(), {
|
||||
uri: "https://www.example.com/",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
notes: null,
|
||||
type: 1,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
describe("Chrome CSV Importer", () => {
|
||||
CipherData.forEach((data) => {
|
||||
it(data.title, async () => {
|
||||
const importer = new Importer();
|
||||
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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const data = `name,url,username,password
|
||||
,android://N2H9MndUUUt3JuQSWAKexOU9oJLJeHR4nyUGac5E1TXKppkY7xtdRl6l8vKo1hQWCqAEy4gsNLUBIbVxpdmhOP==@com.xyz.example.app.android/,username@example.com,Qh6W4Wz55YGFNU`;
|
||||
@@ -0,0 +1,2 @@
|
||||
export const data = `name,url,username,password
|
||||
www.example.com,https://www.example.com/,username@example.com,wpC9qFvsbWQK5Z`;
|
||||
@@ -37,9 +37,11 @@ const kdf = 0;
|
||||
const kdfIterations = 10000;
|
||||
const userId = Utils.newGuid();
|
||||
const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
||||
const name = "NAME";
|
||||
|
||||
const decodedToken = {
|
||||
sub: userId,
|
||||
name: name,
|
||||
email: email,
|
||||
premium: false,
|
||||
};
|
||||
@@ -122,6 +124,7 @@ describe("LogInStrategy", () => {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: userId,
|
||||
name: name,
|
||||
email: email,
|
||||
hasPremiumPersonally: false,
|
||||
kdfIterations: kdfIterations,
|
||||
|
||||
@@ -14,27 +14,105 @@ describe("Utils Service", () => {
|
||||
expect(Utils.getDomain("data:image/jpeg;base64,AAA")).toBeNull();
|
||||
});
|
||||
|
||||
it("should fail for about urls", () => {
|
||||
expect(Utils.getDomain("about")).toBeNull();
|
||||
expect(Utils.getDomain("about:")).toBeNull();
|
||||
expect(Utils.getDomain("about:blank")).toBeNull();
|
||||
});
|
||||
|
||||
it("should fail for file url", () => {
|
||||
expect(Utils.getDomain("file:///C://somefolder/form.pdf")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle urls without protocol", () => {
|
||||
expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("wrong://bitwarden.com")).toBe("bitwarden.com");
|
||||
});
|
||||
|
||||
it("should handle valid urls", () => {
|
||||
expect(Utils.getDomain("https://bitwarden")).toBe("bitwarden");
|
||||
expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("http://bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com");
|
||||
|
||||
expect(Utils.getDomain("www.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("http://www.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("https://www.bitwarden.com")).toBe("bitwarden.com");
|
||||
|
||||
expect(Utils.getDomain("vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("http://vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("https://vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
|
||||
expect(Utils.getDomain("www.vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("http://www.vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("https://www.vault.bitwarden.com")).toBe("bitwarden.com");
|
||||
|
||||
expect(
|
||||
Utils.getDomain("user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
||||
).toBe("bitwarden.com");
|
||||
expect(
|
||||
Utils.getDomain("http://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
||||
).toBe("bitwarden.com");
|
||||
expect(
|
||||
Utils.getDomain("https://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
||||
).toBe("bitwarden.com");
|
||||
|
||||
expect(Utils.getDomain("bitwarden.unknown")).toBe("bitwarden.unknown");
|
||||
expect(Utils.getDomain("http://bitwarden.unknown")).toBe("bitwarden.unknown");
|
||||
expect(Utils.getDomain("https://bitwarden.unknown")).toBe("bitwarden.unknown");
|
||||
});
|
||||
|
||||
it("should support localhost and IP", () => {
|
||||
it("should handle valid urls with an underscore in subdomain", () => {
|
||||
expect(Utils.getDomain("my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("http://my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
||||
expect(Utils.getDomain("https://my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
||||
});
|
||||
|
||||
it("should support urls containing umlauts", () => {
|
||||
expect(Utils.getDomain("bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getDomain("http://bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getDomain("https://bütwarden.com")).toBe("bütwarden.com");
|
||||
|
||||
expect(Utils.getDomain("subdomain.bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getDomain("http://subdomain.bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getDomain("https://subdomain.bütwarden.com")).toBe("bütwarden.com");
|
||||
});
|
||||
|
||||
it("should support punycode urls", () => {
|
||||
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
|
||||
expect(Utils.getDomain("subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getDomain("http://subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getDomain("https://subdomain.xn--btwarden-65a.com")).toBe(
|
||||
"xn--btwarden-65a.com"
|
||||
);
|
||||
});
|
||||
|
||||
it("should support localhost", () => {
|
||||
expect(Utils.getDomain("localhost")).toBe("localhost");
|
||||
expect(Utils.getDomain("http://localhost")).toBe("localhost");
|
||||
expect(Utils.getDomain("https://localhost")).toBe("localhost");
|
||||
});
|
||||
|
||||
it("should support localhost with subdomain", () => {
|
||||
expect(Utils.getDomain("subdomain.localhost")).toBe("localhost");
|
||||
expect(Utils.getDomain("http://subdomain.localhost")).toBe("localhost");
|
||||
expect(Utils.getDomain("https://subdomain.localhost")).toBe("localhost");
|
||||
});
|
||||
|
||||
it("should support IPv4", () => {
|
||||
expect(Utils.getDomain("192.168.1.1")).toBe("192.168.1.1");
|
||||
expect(Utils.getDomain("http://192.168.1.1")).toBe("192.168.1.1");
|
||||
expect(Utils.getDomain("https://192.168.1.1")).toBe("192.168.1.1");
|
||||
});
|
||||
|
||||
it("should support IPv6", () => {
|
||||
expect(Utils.getDomain("[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
expect(Utils.getDomain("http://[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
expect(Utils.getDomain("https://[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
});
|
||||
|
||||
it("should reject invalid hostnames", () => {
|
||||
expect(Utils.getDomain("https://mywebsite.com$.mywebsite.com")).toBeNull();
|
||||
expect(Utils.getDomain("https://mywebsite.com!.mywebsite.com")).toBeNull();
|
||||
@@ -47,20 +125,107 @@ describe("Utils Service", () => {
|
||||
expect(Utils.getHostname(undefined)).toBeNull();
|
||||
expect(Utils.getHostname(" ")).toBeNull();
|
||||
expect(Utils.getHostname('https://bit!:"_&ward.com')).toBeNull();
|
||||
expect(Utils.getHostname("bitwarden")).toBeNull();
|
||||
});
|
||||
|
||||
it("should fail for data urls", () => {
|
||||
expect(Utils.getHostname("data:image/jpeg;base64,AAA")).toBeNull();
|
||||
});
|
||||
|
||||
it("should fail for about urls", () => {
|
||||
expect(Utils.getHostname("about")).toBe("about");
|
||||
expect(Utils.getHostname("about:")).toBeNull();
|
||||
expect(Utils.getHostname("about:blank")).toBeNull();
|
||||
});
|
||||
|
||||
it("should fail for file url", () => {
|
||||
expect(Utils.getHostname("file:///C:/somefolder/form.pdf")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle valid urls", () => {
|
||||
expect(Utils.getHostname("bitwarden")).toBe("bitwarden");
|
||||
expect(Utils.getHostname("http://bitwarden")).toBe("bitwarden");
|
||||
expect(Utils.getHostname("https://bitwarden")).toBe("bitwarden");
|
||||
|
||||
expect(Utils.getHostname("bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getHostname("http://bitwarden.com")).toBe("bitwarden.com");
|
||||
expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com");
|
||||
|
||||
expect(Utils.getHostname("www.bitwarden.com")).toBe("www.bitwarden.com");
|
||||
expect(Utils.getHostname("http://www.bitwarden.com")).toBe("www.bitwarden.com");
|
||||
expect(Utils.getHostname("https://www.bitwarden.com")).toBe("www.bitwarden.com");
|
||||
|
||||
expect(Utils.getHostname("vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
||||
expect(Utils.getHostname("http://vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
||||
expect(Utils.getHostname("https://vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
||||
|
||||
expect(Utils.getHostname("www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
||||
expect(Utils.getHostname("http://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
||||
expect(Utils.getHostname("https://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
||||
|
||||
expect(
|
||||
Utils.getHostname("user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
||||
).toBe("bitwarden.com");
|
||||
expect(
|
||||
Utils.getHostname("https://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
||||
).toBe("bitwarden.com");
|
||||
expect(Utils.getHostname("https://bitwarden.unknown")).toBe("bitwarden.unknown");
|
||||
});
|
||||
|
||||
it("should support localhost and IP", () => {
|
||||
it("should handle valid urls with an underscore in subdomain", () => {
|
||||
expect(Utils.getHostname("my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
||||
expect(Utils.getHostname("http://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
||||
expect(Utils.getHostname("https://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
||||
});
|
||||
|
||||
it("should support urls containing umlauts", () => {
|
||||
expect(Utils.getHostname("bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getHostname("http://bütwarden.com")).toBe("bütwarden.com");
|
||||
expect(Utils.getHostname("https://bütwarden.com")).toBe("bütwarden.com");
|
||||
|
||||
expect(Utils.getHostname("subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
||||
expect(Utils.getHostname("http://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
||||
expect(Utils.getHostname("https://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
||||
});
|
||||
|
||||
it("should support punycode urls", () => {
|
||||
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
||||
|
||||
expect(Utils.getHostname("subdomain.xn--btwarden-65a.com")).toBe(
|
||||
"subdomain.xn--btwarden-65a.com"
|
||||
);
|
||||
expect(Utils.getHostname("http://subdomain.xn--btwarden-65a.com")).toBe(
|
||||
"subdomain.xn--btwarden-65a.com"
|
||||
);
|
||||
expect(Utils.getHostname("https://subdomain.xn--btwarden-65a.com")).toBe(
|
||||
"subdomain.xn--btwarden-65a.com"
|
||||
);
|
||||
});
|
||||
|
||||
it("should support localhost", () => {
|
||||
expect(Utils.getHostname("localhost")).toBe("localhost");
|
||||
expect(Utils.getHostname("http://localhost")).toBe("localhost");
|
||||
expect(Utils.getHostname("https://localhost")).toBe("localhost");
|
||||
});
|
||||
|
||||
it("should support localhost with subdomain", () => {
|
||||
expect(Utils.getHostname("subdomain.localhost")).toBe("subdomain.localhost");
|
||||
expect(Utils.getHostname("http://subdomain.localhost")).toBe("subdomain.localhost");
|
||||
expect(Utils.getHostname("https://subdomain.localhost")).toBe("subdomain.localhost");
|
||||
});
|
||||
|
||||
it("should support IPv4", () => {
|
||||
expect(Utils.getHostname("192.168.1.1")).toBe("192.168.1.1");
|
||||
expect(Utils.getHostname("http://192.168.1.1")).toBe("192.168.1.1");
|
||||
expect(Utils.getHostname("https://192.168.1.1")).toBe("192.168.1.1");
|
||||
});
|
||||
|
||||
it("should support IPv6", () => {
|
||||
expect(Utils.getHostname("[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
expect(Utils.getHostname("http://[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
expect(Utils.getHostname("https://[2620:fe::fe]")).toBe("2620:fe::fe");
|
||||
});
|
||||
});
|
||||
|
||||
describe("newGuid", () => {
|
||||
|
||||
65
libs/common/spec/view/login-uri-view.spec.ts
Normal file
65
libs/common/spec/view/login-uri-view.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { UriMatchType } from "@bitwarden/common/enums/uriMatchType";
|
||||
import { LoginUriView } from "@bitwarden/common/models/view/login-uri.view";
|
||||
|
||||
const testData = [
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "http://example.com/login",
|
||||
expected: "http://example.com/login",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.com",
|
||||
expected: "http://bitwarden.com",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.de",
|
||||
expected: "http://bitwarden.de",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.br",
|
||||
expected: "http://bitwarden.br",
|
||||
},
|
||||
];
|
||||
|
||||
describe("LoginUriView", () => {
|
||||
it("isWebsite() given an invalid domain should return false", async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.Host, uri: "bit!:_&ward.com" });
|
||||
expect(uri.isWebsite).toBe(false);
|
||||
});
|
||||
|
||||
testData.forEach((data) => {
|
||||
it(`isWebsite() given ${data.uri} should return true`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.isWebsite).toBe(true);
|
||||
});
|
||||
|
||||
it(`launchUri() given ${data.uri} should return ${data.expected}`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.launchUri).toBe(data.expected);
|
||||
});
|
||||
|
||||
it(`canLaunch() given ${data.uri} should return true`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.canLaunch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it(`canLaunch should return false when MatchDetection is set to Regex`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.RegularExpression, uri: "bitwarden.com" });
|
||||
expect(uri.canLaunch).toBe(false);
|
||||
});
|
||||
|
||||
it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" });
|
||||
expect(uri.canLaunch).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.
|
||||
import { CipherBulkShareRequest } from "../models/request/cipher-bulk-share.request";
|
||||
import { CipherCollectionsRequest } from "../models/request/cipher-collections.request";
|
||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../models/request/cipher.request";
|
||||
import { CollectionRequest } from "../models/request/collection.request";
|
||||
@@ -257,6 +258,7 @@ export abstract class ApiService {
|
||||
postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>;
|
||||
postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>;
|
||||
putCipher: (id: string, request: CipherRequest) => Promise<CipherResponse>;
|
||||
putPartialCipher: (id: string, request: CipherPartialRequest) => Promise<CipherResponse>;
|
||||
putCipherAdmin: (id: string, request: CipherRequest) => Promise<CipherResponse>;
|
||||
deleteCipher: (id: string) => Promise<any>;
|
||||
deleteCipherAdmin: (id: string) => Promise<any>;
|
||||
|
||||
@@ -33,7 +33,8 @@ export abstract class CipherService {
|
||||
updateLastUsedDate: (id: string) => Promise<void>;
|
||||
updateLastLaunchedDate: (id: string) => Promise<void>;
|
||||
saveNeverDomain: (domain: string) => Promise<void>;
|
||||
saveWithServer: (cipher: Cipher) => Promise<any>;
|
||||
createWithServer: (cipher: Cipher) => Promise<any>;
|
||||
updateWithServer: (cipher: Cipher) => Promise<any>;
|
||||
shareWithServer: (
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
|
||||
@@ -173,8 +173,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
) => Promise<void>;
|
||||
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableGravitars: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableGravitars: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableMinimizeToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableStartToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { BaseImporter } from "./baseImporter";
|
||||
import { Importer } from "./importer";
|
||||
|
||||
export class ChromeCsvImporter extends BaseImporter implements Importer {
|
||||
private androidPatternRegex = new RegExp("^android:\\/\\/.*(?<=@)(.*)(?=\\/)");
|
||||
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const results = this.parseCsv(data, true);
|
||||
@@ -14,7 +16,11 @@ export class ChromeCsvImporter extends BaseImporter implements Importer {
|
||||
|
||||
results.forEach((value) => {
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.name = this.getValueOrDefault(value.name, "--");
|
||||
let name = value.name;
|
||||
if (!name && this.androidPatternRegex.test(value.url)) {
|
||||
name = value.url.match(this.androidPatternRegex)[1];
|
||||
}
|
||||
cipher.name = this.getValueOrDefault(name, "--");
|
||||
cipher.login.username = this.getValueOrDefault(value.username);
|
||||
cipher.login.password = this.getValueOrDefault(value.password);
|
||||
cipher.login.uris = this.makeUriArray(value.url);
|
||||
@@ -105,6 +105,7 @@ export abstract class LogInStrategy {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export function getDomain(host: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isValid(host: string): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import * as tldjs from "tldjs";
|
||||
import { getHostname, parse } from "tldts";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
@@ -24,11 +24,10 @@ export class Utils {
|
||||
static isMobileBrowser = false;
|
||||
static isAppleMobileBrowser = false;
|
||||
static global: typeof global = null;
|
||||
static tldEndingRegex =
|
||||
/.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/;
|
||||
// Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers.
|
||||
static regexpEmojiPresentation =
|
||||
/(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])/g;
|
||||
static readonly validHosts: string[] = ["localhost"];
|
||||
|
||||
static init() {
|
||||
if (Utils.inited) {
|
||||
@@ -214,12 +213,39 @@ export class Utils {
|
||||
}
|
||||
|
||||
static getHostname(uriString: string): string {
|
||||
const url = Utils.getUrl(uriString);
|
||||
if (Utils.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uriString = uriString.trim();
|
||||
|
||||
if (uriString.startsWith("data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uriString.startsWith("about:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uriString.startsWith("file:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Does uriString contain invalid characters
|
||||
// TODO Needs to possibly be extended, although '!' is a reserved character
|
||||
if (uriString.indexOf("!") > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return url != null && url.hostname !== "" ? url.hostname : null;
|
||||
const hostname = getHostname(uriString, { validHosts: this.validHosts });
|
||||
if (hostname != null) {
|
||||
return hostname;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static getHost(uriString: string): string {
|
||||
@@ -232,60 +258,35 @@ export class Utils {
|
||||
}
|
||||
|
||||
static getDomain(uriString: string): string {
|
||||
if (uriString == null) {
|
||||
if (Utils.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uriString = uriString.trim();
|
||||
if (uriString === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uriString.startsWith("data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let httpUrl = uriString.startsWith("http://") || uriString.startsWith("https://");
|
||||
if (
|
||||
!httpUrl &&
|
||||
uriString.indexOf("://") < 0 &&
|
||||
Utils.tldEndingRegex.test(uriString) &&
|
||||
uriString.indexOf("@") < 0
|
||||
) {
|
||||
uriString = "http://" + uriString;
|
||||
httpUrl = true;
|
||||
}
|
||||
|
||||
if (httpUrl) {
|
||||
try {
|
||||
const url = Utils.getUrlObject(uriString);
|
||||
const validHostname = tldjs?.isValid != null ? tldjs.isValid(url.hostname) : true;
|
||||
if (!validHostname) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.hostname === "localhost" || Utils.validIpAddress(url.hostname)) {
|
||||
return url.hostname;
|
||||
}
|
||||
|
||||
const urlDomain =
|
||||
tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null;
|
||||
return urlDomain != null ? urlDomain : url.hostname;
|
||||
} catch (e) {
|
||||
// Invalid domain, try another approach below.
|
||||
}
|
||||
if (uriString.startsWith("about:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null;
|
||||
const parseResult = parse(uriString, { validHosts: this.validHosts });
|
||||
if (parseResult != null && parseResult.hostname != null) {
|
||||
if (parseResult.hostname === "localhost" || parseResult.isIp) {
|
||||
return parseResult.hostname;
|
||||
}
|
||||
|
||||
if (domain != null) {
|
||||
return domain;
|
||||
if (parseResult.domain != null) {
|
||||
return parseResult.domain;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -358,14 +359,11 @@ export class Utils {
|
||||
}
|
||||
|
||||
static getUrl(uriString: string): URL {
|
||||
if (uriString == null) {
|
||||
if (this.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uriString = uriString.trim();
|
||||
if (uriString === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = Utils.getUrlObject(uriString);
|
||||
if (url == null) {
|
||||
@@ -425,12 +423,6 @@ export class Utils {
|
||||
return this.global.bitwardenContainerService;
|
||||
}
|
||||
|
||||
private static validIpAddress(ipString: string): boolean {
|
||||
const ipRegex =
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ipString);
|
||||
}
|
||||
|
||||
private static isMobile(win: Window) {
|
||||
let mobile = false;
|
||||
((a) => {
|
||||
|
||||
@@ -175,6 +175,7 @@ export class AccountProfile {
|
||||
apiKeyClientId?: string;
|
||||
authenticationStatus?: AuthenticationStatus;
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
name?: string;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
entityId?: string;
|
||||
@@ -219,7 +220,6 @@ export class AccountSettings {
|
||||
enableAutoFillOnPageLoad?: boolean;
|
||||
enableBiometric?: boolean;
|
||||
enableFullWidth?: boolean;
|
||||
enableGravitars?: boolean;
|
||||
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
|
||||
equivalentDomains?: any;
|
||||
minimizeOnCopyToClipboard?: boolean;
|
||||
|
||||
11
libs/common/src/models/request/cipher-partial.request.ts
Normal file
11
libs/common/src/models/request/cipher-partial.request.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
export class CipherPartialRequest {
|
||||
folderId: string;
|
||||
favorite: boolean;
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
this.folderId = cipher.folderId;
|
||||
this.favorite = cipher.favorite;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export class LoginUriView implements View {
|
||||
this.uri != null &&
|
||||
(this.uri.indexOf("http://") === 0 ||
|
||||
this.uri.indexOf("https://") === 0 ||
|
||||
(this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri)))
|
||||
(this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export class LoginUriView implements View {
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
return this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri)
|
||||
return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))
|
||||
? "http://" + this.uri
|
||||
: this.uri;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CipherBulkMoveRequest } from "../models/request/cipher-bulk-move.reques
|
||||
import { CipherBulkShareRequest } from "../models/request/cipher-bulk-share.request";
|
||||
import { CipherCollectionsRequest } from "../models/request/cipher-collections.request";
|
||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../models/request/cipher.request";
|
||||
import { CollectionRequest } from "../models/request/collection.request";
|
||||
@@ -609,6 +610,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new CipherResponse(r);
|
||||
}
|
||||
|
||||
async putPartialCipher(id: string, request: CipherPartialRequest): Promise<CipherResponse> {
|
||||
const r = await this.send("PUT", "/ciphers/" + id + "/partial", request, true, true);
|
||||
return new CipherResponse(r);
|
||||
}
|
||||
|
||||
async putCipherAdmin(id: string, request: CipherRequest): Promise<CipherResponse> {
|
||||
const r = await this.send("PUT", "/ciphers/" + id + "/admin", request, true, true);
|
||||
return new CipherResponse(r);
|
||||
|
||||
@@ -37,6 +37,7 @@ import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.
|
||||
import { CipherBulkShareRequest } from "../models/request/cipher-bulk-share.request";
|
||||
import { CipherCollectionsRequest } from "../models/request/cipher-collections.request";
|
||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../models/request/cipher.request";
|
||||
import { CipherResponse } from "../models/response/cipher.response";
|
||||
@@ -157,6 +158,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher.collectionIds = model.collectionIds;
|
||||
cipher.revisionDate = model.revisionDate;
|
||||
cipher.reprompt = model.reprompt;
|
||||
cipher.edit = model.edit;
|
||||
|
||||
if (key == null && cipher.organizationId != null) {
|
||||
key = await this.cryptoService.getOrgKey(cipher.organizationId);
|
||||
@@ -594,20 +596,29 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.stateService.setNeverDomains(domains);
|
||||
}
|
||||
|
||||
async saveWithServer(cipher: Cipher): Promise<any> {
|
||||
async createWithServer(cipher: Cipher): Promise<any> {
|
||||
let response: CipherResponse;
|
||||
if (cipher.id == null) {
|
||||
if (cipher.collectionIds != null) {
|
||||
const request = new CipherCreateRequest(cipher);
|
||||
response = await this.apiService.postCipherCreate(request);
|
||||
} else {
|
||||
const request = new CipherRequest(cipher);
|
||||
response = await this.apiService.postCipher(request);
|
||||
}
|
||||
cipher.id = response.id;
|
||||
if (cipher.collectionIds != null) {
|
||||
const request = new CipherCreateRequest(cipher);
|
||||
response = await this.apiService.postCipherCreate(request);
|
||||
} else {
|
||||
const request = new CipherRequest(cipher);
|
||||
response = await this.apiService.postCipher(request);
|
||||
}
|
||||
cipher.id = response.id;
|
||||
|
||||
const data = new CipherData(response, cipher.collectionIds);
|
||||
await this.upsert(data);
|
||||
}
|
||||
|
||||
async updateWithServer(cipher: Cipher): Promise<any> {
|
||||
let response: CipherResponse;
|
||||
if (cipher.edit) {
|
||||
const request = new CipherRequest(cipher);
|
||||
response = await this.apiService.putCipher(cipher.id, request);
|
||||
} else {
|
||||
const request = new CipherPartialRequest(cipher);
|
||||
response = await this.apiService.putPartialCipher(cipher.id, request);
|
||||
}
|
||||
|
||||
const data = new CipherData(response, cipher.collectionIds);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { BitwardenPasswordProtectedImporter } from "../importers/bitwardenPasswo
|
||||
import { BlackBerryCsvImporter } from "../importers/blackBerryCsvImporter";
|
||||
import { BlurCsvImporter } from "../importers/blurCsvImporter";
|
||||
import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter";
|
||||
import { ChromeCsvImporter } from "../importers/chromeCsvImporter";
|
||||
import { ChromeCsvImporter } from "../importers/chrome-csv-importer";
|
||||
import { ClipperzHtmlImporter } from "../importers/clipperzHtmlImporter";
|
||||
import { CodebookCsvImporter } from "../importers/codebookCsvImporter";
|
||||
import { DashlaneCsvImporter } from "../importers/dashlaneImporters/dashlaneCsvImporter";
|
||||
|
||||
@@ -1228,27 +1228,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getEnableGravitars(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(
|
||||
await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
)
|
||||
)?.settings?.enableGravitars ?? false
|
||||
);
|
||||
}
|
||||
|
||||
async setEnableGravitars(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
account.settings.enableGravitars = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
|
||||
@@ -61,7 +61,6 @@ const v1Keys: { [key: string]: string } = {
|
||||
enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint",
|
||||
enableCloseToTray: "enableCloseToTray",
|
||||
enableFullWidth: "enableFullWidth",
|
||||
enableGravatars: "enableGravatars",
|
||||
enableMinimizeToTray: "enableMinimizeToTray",
|
||||
enableStartToTray: "enableStartToTrayKey",
|
||||
enableTray: "enableTray",
|
||||
@@ -305,9 +304,6 @@ export class StateMigrationService<
|
||||
enableFullWidth:
|
||||
(await this.get<boolean>(v1Keys.enableFullWidth)) ??
|
||||
defaultAccount.settings.enableFullWidth,
|
||||
enableGravitars:
|
||||
(await this.get<boolean>(v1Keys.enableGravatars)) ??
|
||||
defaultAccount.settings.enableGravitars,
|
||||
environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls,
|
||||
equivalentDomains:
|
||||
(await this.get<any>(v1Keys.equivalentDomains)) ??
|
||||
|
||||
@@ -17,9 +17,9 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
})
|
||||
export class AvatarComponent implements OnChanges {
|
||||
@Input() border = false;
|
||||
@Input() color: string;
|
||||
@Input() id: number;
|
||||
@Input() text: string;
|
||||
@Input() color?: string;
|
||||
@Input() id?: string;
|
||||
@Input() text?: string;
|
||||
@Input() size: SizeTypes = "default";
|
||||
|
||||
private svgCharCount = 2;
|
||||
@@ -42,7 +42,7 @@ export class AvatarComponent implements OnChanges {
|
||||
|
||||
private generate() {
|
||||
let chars: string = null;
|
||||
const upperCaseText = this.text.toUpperCase();
|
||||
const upperCaseText = this.text?.toUpperCase() ?? "";
|
||||
|
||||
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||
|
||||
@@ -58,9 +58,9 @@ export class AvatarComponent implements OnChanges {
|
||||
let svg: HTMLElement;
|
||||
let hexColor = this.color;
|
||||
|
||||
if (this.color != null) {
|
||||
if (!Utils.isNullOrWhitespace(this.color)) {
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else if (this.id != null) {
|
||||
} else if (!Utils.isNullOrWhitespace(this.id)) {
|
||||
hexColor = Utils.stringToColor(this.id.toString());
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -26,6 +26,12 @@ export default {
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=2070%3A17207",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
bannerType: "warning",
|
||||
},
|
||||
@@ -38,7 +44,7 @@ const Template: Story<BannerComponent> = (args: BannerComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)">
|
||||
Content Really Long Text Lorem Ipsum Ipsum Ipsum
|
||||
Content Really Long Text Lorem Ipsum Ipsum Ipsum
|
||||
<button>Button</button>
|
||||
</bit-banner>
|
||||
`,
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16733",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5115%3A26950",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17484",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17484",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5600%3A24278",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
@@ -14,6 +14,12 @@ const buttonTypes: IconButtonType[] = [
|
||||
export default {
|
||||
title: "Component Library/Icon Button",
|
||||
component: BitIconButtonComponent,
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4369%3A16686",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
bitIconButton: "bwi-plus",
|
||||
size: "default",
|
||||
|
||||
@@ -10,9 +10,7 @@ import { Icon, isIcon } from "./icon";
|
||||
export class BitIconComponent {
|
||||
@Input() icon: Icon;
|
||||
|
||||
constructor(private domSanitizer: DomSanitizer) {}
|
||||
|
||||
@HostBinding("innerHtml")
|
||||
@HostBinding()
|
||||
protected get innerHtml() {
|
||||
if (!isIcon(this.icon)) {
|
||||
return "";
|
||||
@@ -21,4 +19,6 @@ export class BitIconComponent {
|
||||
const svg = this.icon.svg;
|
||||
return this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||
}
|
||||
|
||||
constructor(private domSanitizer: DomSanitizer) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
// Put generic icons in this folder and export them here.
|
||||
// Note: Icons need to be in separate files for tree-shaking to work properly
|
||||
|
||||
export {}; // <- remove when adding icons in here
|
||||
export * from "./search";
|
||||
|
||||
18
libs/components/src/icon/icons/search.ts
Normal file
18
libs/components/src/icon/icons/search.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { svgIcon } from "../icon";
|
||||
|
||||
export const Search = svgIcon`
|
||||
<svg width="120" height="120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity=".49">
|
||||
<path class="tw-fill-secondary-300" fill-rule="evenodd" clip-rule="evenodd" d="M40.36 73.256a30.004 30.004 0 0 0 10.346 1.826c16.282 0 29.482-12.912 29.482-28.84 0-.384-.008-.766-.023-1.145h28.726v39.57H40.36v-11.41Z" />
|
||||
<path class="tw-stroke-secondary-500" d="M21.546 46.241c0 15.929 13.2 28.841 29.482 28.841S80.51 62.17 80.51 46.241c0-15.928-13.2-28.841-29.482-28.841S21.546 30.313 21.546 46.241Z" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path class="tw-fill-secondary-500" d="M35.36 70.595a1.2 1.2 0 0 0-2.4 0h2.4Zm77.475-30.356a2.343 2.343 0 0 1 2.365 2.33h2.4c0-2.593-2.107-4.73-4.765-4.73v2.4Zm2.365 2.33v46.047h2.4V42.57h-2.4Zm0 46.047c0 1.293-1.058 2.33-2.365 2.33v2.4c2.59 0 4.765-2.069 4.765-4.73h-2.4Zm-2.365 2.33h-75.11v2.4h75.11v-2.4Zm-75.11 0a2.343 2.343 0 0 1-2.365-2.33h-2.4c0 2.594 2.107 4.73 4.766 4.73v-2.4Zm-2.365-2.33v-18.02h-2.4v18.02h2.4Zm44.508-48.377h32.967v-2.4H79.868v2.4Z" />
|
||||
<path class="tw-stroke-secondary-500" d="M79.907 45.287h29.114v39.57H40.487V73.051" stroke-width="2" stroke-linejoin="round" />
|
||||
<path class="tw-stroke-secondary-500" d="M57.356 102.56h35.849" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path class="tw-stroke-secondary-500" d="M68.954 92.147v10.413m11.599-10.413v10.413" stroke-width="4" stroke-linejoin="round" />
|
||||
<path class="tw-stroke-secondary-500" d="m27.44 64.945-4.51 4.51L5.72 86.663a3 3 0 0 0 0 4.243l1.238 1.238a3 3 0 0 0 4.243 0L28.41 74.936l4.51-4.51" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path class="tw-stroke-secondary-500" d="M101.293 53.154H85.178m16.115 6.043H90.214m-5.036 0h-7.553m23.668 6.043h-7.05m-5.54 0h-15.61m28.2 6.042H85.178m-5.538 0h-8.562m30.215 6.043H78.632m-5.539 0H60m-5.54 0h-8.057" stroke-width="2" stroke-linecap="round" />
|
||||
<path class="tw-stroke-secondary-500" d="M29.164 33.01h41.529a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4h-41.53a2.4 2.4 0 0 1-2.4-2.4v-6.28a2.4 2.4 0 0 1 2.4-2.4Z" stroke-width="4" />
|
||||
<path class="tw-stroke-secondary-500" d="M22.735 54.16h34.361a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4H28.778m50.358-11.08h-6.161a2.4 2.4 0 0 0-2.4 2.4v6.414a2.266 2.266 0 0 0 2.266 2.265" stroke-width="4" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
@@ -4,13 +4,13 @@ export * from "./badge";
|
||||
export * from "./banner";
|
||||
export * from "./button";
|
||||
export * from "./callout";
|
||||
export * from "./dialog";
|
||||
export * from "./form-field";
|
||||
export * from "./icon";
|
||||
export * from "./icon-button";
|
||||
export * from "./icon";
|
||||
export * from "./link";
|
||||
export * from "./menu";
|
||||
export * from "./multi-select";
|
||||
export * from "./dialog";
|
||||
export * from "./link";
|
||||
export * from "./tabs";
|
||||
export * from "./table";
|
||||
export * from "./toggle-group";
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17952",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17952",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
Reference in New Issue
Block a user