mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
feat(DuckDuckGo): [PM-9388] Add new device type for DuckDuckGo browser
* Add new device type for DuckDuckGo browser * Added feature support property for sync domains * Added new features * Added isDuckDuckGo() to CLI * Addressed PR feedback. * Renamed new property * Fixed rename that missed CLI.
This commit is contained in:
@@ -207,6 +207,14 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutofill(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsFileDownloads(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
abstract showToast(
|
abstract showToast(
|
||||||
type: "error" | "success" | "warning" | "info",
|
type: "error" | "success" | "warning" | "info",
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutofill(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsFileDownloads(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
type: "error" | "success" | "warning" | "info",
|
type: "error" | "success" | "warning" | "info",
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutofill(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsFileDownloads(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
type: "error" | "success" | "warning" | "info",
|
type: "error" | "success" | "warning" | "info",
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class WebFileDownloadService implements FileDownloadService {
|
|||||||
download(request: FileDownloadRequest): void {
|
download(request: FileDownloadRequest): void {
|
||||||
const builder = new FileDownloadBuilder(request);
|
const builder = new FileDownloadBuilder(request);
|
||||||
const a = window.document.createElement("a");
|
const a = window.document.createElement("a");
|
||||||
if (!this.platformUtilsService.isSafari()) {
|
if (!this.platformUtilsService.supportsFileDownloads()) {
|
||||||
a.rel = "noreferrer";
|
a.rel = "noreferrer";
|
||||||
a.target = "_blank";
|
a.target = "_blank";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||||
|
|
||||||
describe("Web Platform Utils Service", () => {
|
describe("Web Platform Utils Service", () => {
|
||||||
@@ -114,4 +116,91 @@ describe("Web Platform Utils Service", () => {
|
|||||||
expect(result).toBe("2022.10.2");
|
expect(result).toBe("2022.10.2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("getDevice", () => {
|
||||||
|
const originalUserAgent = navigator.userAgent;
|
||||||
|
|
||||||
|
const setUserAgent = (userAgent: string) => {
|
||||||
|
Object.defineProperty(navigator, "userAgent", {
|
||||||
|
value: userAgent,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setWindowProperties = (props?: Record<string, any>) => {
|
||||||
|
if (!props) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(props).forEach((key) => {
|
||||||
|
Object.defineProperty(window, key, {
|
||||||
|
value: props[key],
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Reset to original after each test
|
||||||
|
setUserAgent(originalUserAgent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const testData: {
|
||||||
|
userAgent: string;
|
||||||
|
expectedDevice: DeviceType;
|
||||||
|
windowProps?: Record<string, any>;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
// DuckDuckGo macoOS browser v1.13
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15 Ddg/18.3.1",
|
||||||
|
expectedDevice: DeviceType.DuckDuckGoBrowser,
|
||||||
|
},
|
||||||
|
// DuckDuckGo Windows browser v0.109.7, which does not present the Ddg suffix and is therefore detected as Edge
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
|
||||||
|
expectedDevice: DeviceType.EdgeBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||||
|
expectedDevice: DeviceType.ChromeBrowser,
|
||||||
|
windowProps: { chrome: {} }, // set window.chrome = {} to simulate Chrome
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||||
|
expectedDevice: DeviceType.FirefoxBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15",
|
||||||
|
expectedDevice: DeviceType.SafariBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/120.0.0.0 Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
expectedDevice: DeviceType.EdgeBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.65 Safari/537.36 OPR/95.0.4635.46",
|
||||||
|
expectedDevice: DeviceType.OperaBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.57 Safari/537.36 Vivaldi/6.5.3206.48",
|
||||||
|
expectedDevice: DeviceType.VivaldiBrowser,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(testData)(
|
||||||
|
"returns $expectedDevice for User-Agent: $userAgent",
|
||||||
|
({ userAgent, expectedDevice, windowProps }) => {
|
||||||
|
setUserAgent(userAgent);
|
||||||
|
setWindowProperties(windowProps);
|
||||||
|
const result = webPlatformUtilsService.getDevice();
|
||||||
|
expect(result).toBe(expectedDevice);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
|||||||
this.browserCache = DeviceType.EdgeBrowser;
|
this.browserCache = DeviceType.EdgeBrowser;
|
||||||
} else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) {
|
} else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) {
|
||||||
this.browserCache = DeviceType.VivaldiBrowser;
|
this.browserCache = DeviceType.VivaldiBrowser;
|
||||||
|
} else if (
|
||||||
|
// We are only detecting DuckDuckGo browser on macOS currently, as
|
||||||
|
// it is not presenting the Ddg suffix on Windows. DuckDuckGo users
|
||||||
|
// on Windows will be detected as Edge.
|
||||||
|
navigator.userAgent.indexOf("Ddg") !== -1
|
||||||
|
) {
|
||||||
|
this.browserCache = DeviceType.DuckDuckGoBrowser;
|
||||||
} else if (
|
} else if (
|
||||||
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
||||||
navigator.userAgent.indexOf("Chrome") === -1
|
navigator.userAgent.indexOf("Chrome") === -1
|
||||||
@@ -83,6 +90,10 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return this.getDevice() === DeviceType.SafariBrowser;
|
return this.getDevice() === DeviceType.SafariBrowser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isWebKit(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
isMacAppStore(): boolean {
|
isMacAppStore(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -120,6 +131,15 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutofill(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safari support for blob downloads is inconsistent and requires workarounds
|
||||||
|
supportsFileDownloads(): boolean {
|
||||||
|
return !(this.getDevice() === DeviceType.SafariBrowser);
|
||||||
|
}
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
type: "error" | "success" | "warning" | "info",
|
type: "error" | "success" | "warning" | "info",
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -180,22 +180,4 @@ export class SecretsListComponent implements OnDestroy {
|
|||||||
i18nService.t("valueCopied", i18nService.t("uuid")),
|
i18nService.t("valueCopied", i18nService.t("uuid")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Remove in favor of updating `PlatformUtilsService.copyToClipboard`
|
|
||||||
*/
|
|
||||||
private static copyToClipboardAsync(
|
|
||||||
text: Promise<string>,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
) {
|
|
||||||
if (platformUtilsService.isSafari()) {
|
|
||||||
return navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
["text/plain"]: text,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.then((t) => platformUtilsService.copyToClipboard(t));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,14 +148,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSafari() {
|
|
||||||
return this.platformUtilsService.isSafari();
|
|
||||||
}
|
|
||||||
|
|
||||||
get isDateTimeLocalSupported(): boolean {
|
|
||||||
return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari());
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.accountService.activeAccount$
|
this.accountService.activeAccount$
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export enum DeviceType {
|
|||||||
WindowsCLI = 23,
|
WindowsCLI = 23,
|
||||||
MacOsCLI = 24,
|
MacOsCLI = 24,
|
||||||
LinuxCLI = 25,
|
LinuxCLI = 25,
|
||||||
|
DuckDuckGoBrowser = 26,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +56,7 @@ export const DeviceTypeMetadata: Record<DeviceType, DeviceTypeMetadata> = {
|
|||||||
[DeviceType.IEBrowser]: { category: "webVault", platform: "IE" },
|
[DeviceType.IEBrowser]: { category: "webVault", platform: "IE" },
|
||||||
[DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" },
|
[DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" },
|
||||||
[DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" },
|
[DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" },
|
||||||
|
[DeviceType.DuckDuckGoBrowser]: { category: "webVault", platform: "DuckDuckGo" },
|
||||||
[DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" },
|
[DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" },
|
||||||
[DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" },
|
[DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" },
|
||||||
[DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" },
|
[DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" },
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ export abstract class PlatformUtilsService {
|
|||||||
abstract getApplicationVersionNumber(): Promise<string>;
|
abstract getApplicationVersionNumber(): Promise<string>;
|
||||||
abstract supportsWebAuthn(win: Window): boolean;
|
abstract supportsWebAuthn(win: Window): boolean;
|
||||||
abstract supportsDuo(): boolean;
|
abstract supportsDuo(): boolean;
|
||||||
|
/**
|
||||||
|
* Returns true if the device supports autofill functionality
|
||||||
|
*/
|
||||||
|
abstract supportsAutofill(): boolean;
|
||||||
|
/**
|
||||||
|
* Returns true if the device supports native file downloads without
|
||||||
|
* the need for `target="_blank"`
|
||||||
|
*/
|
||||||
|
abstract supportsFileDownloads(): boolean;
|
||||||
/**
|
/**
|
||||||
* @deprecated use `@bitwarden/components/ToastService.showToast` instead
|
* @deprecated use `@bitwarden/components/ToastService.showToast` instead
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
|
|||||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||||
import { DeviceType } from "../enums";
|
import { ClientType, DeviceType } from "../enums";
|
||||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||||
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
|
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
|
||||||
@@ -154,8 +154,6 @@ export type HttpOperations = {
|
|||||||
export class ApiService implements ApiServiceAbstraction {
|
export class ApiService implements ApiServiceAbstraction {
|
||||||
private device: DeviceType;
|
private device: DeviceType;
|
||||||
private deviceType: string;
|
private deviceType: string;
|
||||||
private isWebClient = false;
|
|
||||||
private isDesktopClient = false;
|
|
||||||
private refreshTokenPromise: Promise<string> | undefined;
|
private refreshTokenPromise: Promise<string> | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,22 +176,6 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
) {
|
) {
|
||||||
this.device = platformUtilsService.getDevice();
|
this.device = platformUtilsService.getDevice();
|
||||||
this.deviceType = this.device.toString();
|
this.deviceType = this.device.toString();
|
||||||
this.isWebClient =
|
|
||||||
this.device === DeviceType.IEBrowser ||
|
|
||||||
this.device === DeviceType.ChromeBrowser ||
|
|
||||||
this.device === DeviceType.EdgeBrowser ||
|
|
||||||
this.device === DeviceType.FirefoxBrowser ||
|
|
||||||
this.device === DeviceType.OperaBrowser ||
|
|
||||||
this.device === DeviceType.SafariBrowser ||
|
|
||||||
this.device === DeviceType.UnknownBrowser ||
|
|
||||||
this.device === DeviceType.VivaldiBrowser;
|
|
||||||
this.isDesktopClient =
|
|
||||||
this.device === DeviceType.WindowsDesktop ||
|
|
||||||
this.device === DeviceType.MacOsDesktop ||
|
|
||||||
this.device === DeviceType.LinuxDesktop ||
|
|
||||||
this.device === DeviceType.WindowsCLI ||
|
|
||||||
this.device === DeviceType.MacOsCLI ||
|
|
||||||
this.device === DeviceType.LinuxCLI;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth APIs
|
// Auth APIs
|
||||||
@@ -838,7 +820,9 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
// Sync APIs
|
// Sync APIs
|
||||||
|
|
||||||
async getSync(): Promise<SyncResponse> {
|
async getSync(): Promise<SyncResponse> {
|
||||||
const path = this.isDesktopClient || this.isWebClient ? "/sync?excludeDomains=true" : "/sync";
|
const path = !this.platformUtilsService.supportsAutofill()
|
||||||
|
? "/sync?excludeDomains=true"
|
||||||
|
: "/sync";
|
||||||
const r = await this.send("GET", path, null, true, true);
|
const r = await this.send("GET", path, null, true, true);
|
||||||
return new SyncResponse(r);
|
return new SyncResponse(r);
|
||||||
}
|
}
|
||||||
@@ -1875,7 +1859,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
|
|
||||||
private async getCredentials(): Promise<RequestCredentials> {
|
private async getCredentials(): Promise<RequestCredentials> {
|
||||||
const env = await firstValueFrom(this.environmentService.environment$);
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
if (!this.isWebClient || env.hasBaseUrl()) {
|
if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) {
|
||||||
return "include";
|
return "include";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user