mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[EC-183] Mono Repository - Browser (#2531)
This commit is contained in:
15
apps/browser/.editorconfig
Normal file
15
apps/browser/.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Set default charset
|
||||
[*.{js,ts,scss,html}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
9
apps/browser/.eslintignore
Normal file
9
apps/browser/.eslintignore
Normal file
@@ -0,0 +1,9 @@
|
||||
**/build
|
||||
jslib
|
||||
webpack.config.js
|
||||
karma.conf.js
|
||||
gulpfile.js
|
||||
src/content/autofill.js
|
||||
src/scripts/duo.js
|
||||
|
||||
**/node_modules
|
||||
32
apps/browser/.eslintrc.json
Normal file
32
apps/browser/.eslintrc.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"extends": ["./jslib/shared/eslintrc.json"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "jslib-*/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
21
apps/browser/.gitignore
vendored
Normal file
21
apps/browser/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
.vs
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
node_modules
|
||||
npm-debug.log
|
||||
vwd.webinfo
|
||||
css/
|
||||
dist/
|
||||
dist-safari/
|
||||
webfonts/
|
||||
*.crx
|
||||
*.pem
|
||||
*.zip
|
||||
build/
|
||||
build.safariextension/
|
||||
coverage/
|
||||
xcuserdata/
|
||||
*.hmap
|
||||
!src/safari/safari/app/popup/index.html
|
||||
src/safari/safari/app/
|
||||
1
apps/browser/.husky/.gitignore
vendored
Normal file
1
apps/browser/.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
apps/browser/.husky/pre-commit
Normal file
4
apps/browser/.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
15
apps/browser/.prettierignore
Normal file
15
apps/browser/.prettierignore
Normal file
@@ -0,0 +1,15 @@
|
||||
# Build directories
|
||||
build
|
||||
dist
|
||||
coverage
|
||||
|
||||
jslib
|
||||
|
||||
# External libraries / auto synced locales
|
||||
src/_locales
|
||||
src/scripts/duo.js
|
||||
src/content/autofill.js
|
||||
src/safari
|
||||
|
||||
# Github Workflows
|
||||
.github/workflows
|
||||
3
apps/browser/.prettierrc.json
Normal file
3
apps/browser/.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 100
|
||||
}
|
||||
88
apps/browser/README.md
Normal file
88
apps/browser/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
[](https://github.com/bitwarden/browser/actions/workflows/build.yml?query=branch:master)
|
||||
[](https://crowdin.com/project/bitwarden-browser)
|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
> **Repository Reorganization in Progress**
|
||||
>
|
||||
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
|
||||
>
|
||||
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
|
||||
|
||||
# Bitwarden Browser Extension
|
||||
|
||||
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/3C4iKO0.png" width="64" height="64"></a>
|
||||
<a href="https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/" target="_blank"><img src="https://imgur.com/ihXsdDO.png" width="64" height="64"></a>
|
||||
<a href="https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh" target="_blank"><img src="https://imgur.com/vMcaXaw.png" width="64" height="64"></a>
|
||||
<a href="https://addons.opera.com/extensions/details/bitwarden-free-password-manager/" target="_blank"><img src="https://imgur.com/nSJ9htU.png" width="64" height="64"></a>
|
||||
<a href="https://bitwarden.com/download/" target="_blank"><img src="https://imgur.com/ENbaWUu.png" width="64" height="64"></a>
|
||||
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/EuDp4vP.png" width="64" height="64"></a>
|
||||
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/z8yjLZ2.png" width="64" height="64"></a>
|
||||
<a href="https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/" target="_blank"><img src="https://imgur.com/MQYBSrD.png" width="64" height="64"></a>
|
||||
|
||||
The Bitwarden browser extension is written using the Web Extension API and Angular.
|
||||
|
||||

|
||||
|
||||
# Build/Run
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [Node.js](https://nodejs.org) v16.13.1 or greater
|
||||
- NPM v8
|
||||
- [Gulp](https://gulpjs.com/) (`npm install --global gulp-cli`)
|
||||
- Chrome (preferred), Opera, or Firefox browser
|
||||
|
||||
**Run the app**
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
You can now load the extension into your browser through the browser's extension tools page:
|
||||
|
||||
- Chrome/Opera:
|
||||
1. Type `chrome://extensions` in your address bar to bring up the extensions page.
|
||||
2. Enable developer mode (toggle switch)
|
||||
3. Click the "Load unpacked extension" button, navigate to the `build` folder of your local extension instance, and click "Ok".
|
||||
- Firefox
|
||||
1. Type `about:debugging` in your address bar to bring up the add-ons page.
|
||||
2. Click the `Load Temporary Add-on` button, navigate to the `build/manifest.json` file, and "Open".
|
||||
|
||||
**Desktop communication**
|
||||
|
||||
Native Messaging (communication between the desktop application and browser extension) works by having the browser start a lightweight proxy baked into our desktop application.
|
||||
|
||||
Out of the box, the desktop application can only communicate with the production browser extension. When you enable browser integration in the desktop application, the application generates manifests which contain the production IDs of the browser extensions. To enable communication between the desktop application and development versions of browser extensions, add the development IDs to the `allowed_extensions` section of the corresponding manifests.
|
||||
|
||||
Manifests are located in the `browser` subdirectory of the Bitwarden configuration directory. For instance, on Windows the manifests are located at `C:\Users\<user>\AppData\Roaming\Bitwarden\browsers` and on macOS these are in `Application Support` for various browsers ([for example](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location)). Note that disabling the desktop integration will delete the manifests, and the files will need to be updated again.
|
||||
|
||||
# We're Hiring!
|
||||
|
||||
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
|
||||
|
||||
# Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
||||
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||
|
||||
## Prettier
|
||||
|
||||
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
|
||||
|
||||
1. Check out your local Branch
|
||||
2. Run `git merge cebee8aa81b87cc26157e5bd0f879db254db9319`
|
||||
3. Resolve any merge conflicts, commit.
|
||||
4. Run `npm run prettier`
|
||||
5. Commit
|
||||
6. Run `git merge -Xours 8fe821b9a3f9728bcb02d607ca75add468d380c1`
|
||||
7. Push
|
||||
|
||||
### Git blame
|
||||
|
||||
We also recommend that you configure git to ignore the prettier revision using:
|
||||
|
||||
```bash
|
||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
28
apps/browser/crowdin.yml
Normal file
28
apps/browser/crowdin.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
project_id_env: _CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/_locales/en/messages.json
|
||||
dest: /src/_locales/en/%original_file_name%
|
||||
translation: /src/_locales/%two_letters_code%/%original_file_name%
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-PT: pt_PT
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
- source: /store/locales/en/copy.resx
|
||||
dest: /store/locales/en/%original_file_name%
|
||||
translation: /store/locales/%two_letters_code%/%original_file_name%
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-PT: pt_PT
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
245
apps/browser/gulpfile.js
Normal file
245
apps/browser/gulpfile.js
Normal file
@@ -0,0 +1,245 @@
|
||||
const child = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const filter = require("gulp-filter");
|
||||
const gulpif = require("gulp-if");
|
||||
const jeditor = require("gulp-json-editor");
|
||||
const replace = require("gulp-replace");
|
||||
const zip = require("gulp-zip");
|
||||
|
||||
const manifest = require("./src/manifest.json");
|
||||
|
||||
const paths = {
|
||||
build: "./build/",
|
||||
dist: "./dist/",
|
||||
coverage: "./coverage/",
|
||||
node_modules: "./node_modules/",
|
||||
popupDir: "./src/popup/",
|
||||
cssDir: "./src/popup/css/",
|
||||
safari: "./src/safari/",
|
||||
};
|
||||
|
||||
const filters = {
|
||||
fonts: [
|
||||
"!build/popup/fonts/*",
|
||||
"build/popup/fonts/Open_Sans*.woff",
|
||||
"build/popup/fonts/bwi-font.woff2",
|
||||
"build/popup/fonts/bwi-font.woff",
|
||||
"build/popup/fonts/bwi-font.ttf",
|
||||
],
|
||||
safari: ["!build/safari/**/*"],
|
||||
};
|
||||
|
||||
function buildString() {
|
||||
var build = "";
|
||||
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
|
||||
build = `-${process.env.BUILD_NUMBER}`;
|
||||
}
|
||||
return build;
|
||||
}
|
||||
|
||||
function distFileName(browserName, ext) {
|
||||
return `dist-${browserName}${buildString()}.${ext}`;
|
||||
}
|
||||
|
||||
function dist(browserName, manifest) {
|
||||
return gulp
|
||||
.src(paths.build + "**/*")
|
||||
.pipe(filter(["**"].concat(filters.fonts).concat(filters.safari)))
|
||||
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_" + browserName)))
|
||||
.pipe(gulpif("manifest.json", jeditor(manifest)))
|
||||
.pipe(zip(distFileName(browserName, "zip")))
|
||||
.pipe(gulp.dest(paths.dist));
|
||||
}
|
||||
|
||||
function distFirefox() {
|
||||
return dist("firefox", (manifest) => {
|
||||
delete manifest.content_security_policy;
|
||||
removeShortcuts(manifest);
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
||||
function distOpera() {
|
||||
return dist("opera", (manifest) => {
|
||||
delete manifest.applications;
|
||||
delete manifest.content_security_policy;
|
||||
removeShortcuts(manifest);
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
||||
function distChrome() {
|
||||
return dist("chrome", (manifest) => {
|
||||
delete manifest.applications;
|
||||
delete manifest.content_security_policy;
|
||||
delete manifest.sidebar_action;
|
||||
delete manifest.commands._execute_sidebar_action;
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
||||
function distEdge() {
|
||||
return dist("edge", (manifest) => {
|
||||
delete manifest.applications;
|
||||
delete manifest.content_security_policy;
|
||||
delete manifest.sidebar_action;
|
||||
delete manifest.commands._execute_sidebar_action;
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
||||
function removeShortcuts(manifest) {
|
||||
if (manifest.content_scripts && manifest.content_scripts.length > 1) {
|
||||
const shortcutsScript = manifest.content_scripts[1];
|
||||
if (shortcutsScript.js.indexOf("content/shortcuts.js") > -1) {
|
||||
manifest.content_scripts.splice(1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function distSafariMas(cb) {
|
||||
return distSafariApp(cb, "mas");
|
||||
}
|
||||
|
||||
function distSafariMasDev(cb) {
|
||||
return distSafariApp(cb, "masdev");
|
||||
}
|
||||
|
||||
function distSafariDmg(cb) {
|
||||
return distSafariApp(cb, "dmg");
|
||||
}
|
||||
|
||||
function distSafariApp(cb, subBuildPath) {
|
||||
const buildPath = paths.dist + "Safari/" + subBuildPath + "/";
|
||||
const builtAppexPath = buildPath + "build/Release/safari.appex";
|
||||
const builtAppexFrameworkPath = buildPath + "build/Release/safari.appex/Contents/Frameworks/";
|
||||
const entitlementsPath = paths.safari + "safari/safari.entitlements";
|
||||
var args = [
|
||||
"--verbose",
|
||||
"--force",
|
||||
"-o",
|
||||
"runtime",
|
||||
"--sign",
|
||||
"Developer ID Application: 8bit Solutions LLC",
|
||||
"--entitlements",
|
||||
entitlementsPath,
|
||||
];
|
||||
if (subBuildPath !== "dmg") {
|
||||
args = [
|
||||
"--verbose",
|
||||
"--force",
|
||||
"--sign",
|
||||
subBuildPath === "mas"
|
||||
? "3rd Party Mac Developer Application: 8bit Solutions LLC"
|
||||
: "6B287DD81FF922D86FD836128B0F62F358B38726",
|
||||
"--entitlements",
|
||||
entitlementsPath,
|
||||
];
|
||||
}
|
||||
|
||||
return del([buildPath + "**/*"])
|
||||
.then(() => safariCopyAssets(paths.safari + "**/*", buildPath))
|
||||
.then(() => safariCopyBuild(paths.build + "**/*", buildPath + "safari/app"))
|
||||
.then(() => {
|
||||
const proc = child.spawn("xcodebuild", [
|
||||
"-project",
|
||||
buildPath + "desktop.xcodeproj",
|
||||
"-alltargets",
|
||||
"-configuration",
|
||||
"Release",
|
||||
]);
|
||||
stdOutProc(proc);
|
||||
return new Promise((resolve) => proc.on("close", resolve));
|
||||
})
|
||||
.then(() => {
|
||||
const libs = fs
|
||||
.readdirSync(builtAppexFrameworkPath)
|
||||
.filter((p) => p.endsWith(".dylib"))
|
||||
.map((p) => builtAppexFrameworkPath + p);
|
||||
const libPromises = [];
|
||||
libs.forEach((i) => {
|
||||
const proc = child.spawn("codesign", args.concat([i]));
|
||||
stdOutProc(proc);
|
||||
libPromises.push(new Promise((resolve) => proc.on("close", resolve)));
|
||||
});
|
||||
return Promise.all(libPromises);
|
||||
})
|
||||
.then(() => {
|
||||
const proc = child.spawn("codesign", args.concat([builtAppexPath]));
|
||||
stdOutProc(proc);
|
||||
return new Promise((resolve) => proc.on("close", resolve));
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
return cb;
|
||||
},
|
||||
() => {
|
||||
return cb;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function safariCopyAssets(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
gulp
|
||||
.src(source)
|
||||
.on("error", reject)
|
||||
.pipe(gulpif("safari/Info.plist", replace("0.0.1", manifest.version)))
|
||||
.pipe(
|
||||
gulpif("safari/Info.plist", replace("0.0.2", process.env.BUILD_NUMBER || manifest.version))
|
||||
)
|
||||
.pipe(gulpif("desktop.xcodeproj/project.pbxproj", replace("../../../build", "../safari/app")))
|
||||
.pipe(gulp.dest(dest))
|
||||
.on("end", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function safariCopyBuild(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
gulp
|
||||
.src(source)
|
||||
.on("error", reject)
|
||||
.pipe(filter(["**"].concat(filters.fonts)))
|
||||
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_safari")))
|
||||
.pipe(
|
||||
gulpif(
|
||||
"manifest.json",
|
||||
jeditor((manifest) => {
|
||||
delete manifest.optional_permissions;
|
||||
manifest.permissions.push("nativeMessaging");
|
||||
return manifest;
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipe(gulp.dest(dest))
|
||||
.on("end", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function stdOutProc(proc) {
|
||||
proc.stdout.on("data", (data) => console.log(data.toString()));
|
||||
proc.stderr.on("data", (data) => console.error(data.toString()));
|
||||
}
|
||||
|
||||
function ciCoverage(cb) {
|
||||
return gulp
|
||||
.src(paths.coverage + "**/*")
|
||||
.pipe(filter(["**", "!coverage/coverage*.zip"]))
|
||||
.pipe(zip(`coverage${buildString()}.zip`))
|
||||
.pipe(gulp.dest(paths.coverage));
|
||||
}
|
||||
|
||||
exports["dist:firefox"] = distFirefox;
|
||||
exports["dist:chrome"] = distChrome;
|
||||
exports["dist:opera"] = distOpera;
|
||||
exports["dist:edge"] = distEdge;
|
||||
exports["dist:safari"] = gulp.parallel(distSafariMas, distSafariMasDev, distSafariDmg);
|
||||
exports["dist:safari:mas"] = distSafariMas;
|
||||
exports["dist:safari:masdev"] = distSafariMasDev;
|
||||
exports["dist:safari:dmg"] = distSafariDmg;
|
||||
exports.dist = gulp.parallel(distFirefox, distChrome, distOpera, distEdge);
|
||||
exports["ci:coverage"] = ciCoverage;
|
||||
exports.ci = ciCoverage;
|
||||
1
apps/browser/jslib
Submodule
1
apps/browser/jslib
Submodule
Submodule apps/browser/jslib added at e40e7de808
71
apps/browser/karma.conf.js
Normal file
71
apps/browser/karma.conf.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: "",
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ["jasmine", "webpack"],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [{ pattern: "src/**/*.spec.ts", watch: false }],
|
||||
|
||||
exclude: [],
|
||||
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
"src/**/*.ts": "webpack",
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["progress", "kjhtml"],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["Chrome"],
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
|
||||
webpack: {
|
||||
mode: "production",
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".tsx"],
|
||||
alias: {
|
||||
"jslib-common": path.join(__dirname, "jslib/common/src"),
|
||||
"jslib-angular": path.join(__dirname, "jslib/angular/src"),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [{ test: /\.tsx?$/, loader: "ts-loader" }],
|
||||
},
|
||||
stats: {
|
||||
colors: true,
|
||||
modules: true,
|
||||
reasons: true,
|
||||
errorDetails: true,
|
||||
},
|
||||
devtool: "inline-source-map",
|
||||
},
|
||||
});
|
||||
};
|
||||
21466
apps/browser/package-lock.json
generated
Normal file
21466
apps/browser/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
111
apps/browser/package.json
Normal file
111
apps/browser/package.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"sub:init": "git submodule update --init --recursive",
|
||||
"sub:update": "git submodule update --remote",
|
||||
"sub:pull": "git submodule foreach git pull origin master",
|
||||
"preinstall": "npm run sub:init",
|
||||
"symlink:win": "rmdir /S /Q .\\jslib && cmd /c mklink /J .\\jslib ..\\jslib",
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"build": "webpack",
|
||||
"build:watch": "webpack --watch",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
||||
"clean:l10n": "git push origin --delete l10n_master",
|
||||
"dist": "npm run build:prod && gulp dist",
|
||||
"dist:firefox": "npm run build:prod && gulp dist:firefox",
|
||||
"dist:opera": "npm run build:prod && gulp dist:opera",
|
||||
"dist:safari": "npm run build:prod && gulp dist:safari",
|
||||
"dist:safari:mas": "npm run build:prod && gulp dist:safari:mas",
|
||||
"dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev",
|
||||
"dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "karma start --single-run",
|
||||
"test:watch": "karma start",
|
||||
"prettier": "prettier --write .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^12.2.13",
|
||||
"@ngtools/webpack": "^12.2.13",
|
||||
"@types/chrome": "^0.0.139",
|
||||
"@types/firefox-webext-browser": "^82.0.0",
|
||||
"@types/jasmine": "^3.7.6",
|
||||
"@types/mousetrap": "^1.6.8",
|
||||
"@types/node": "^16.11.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||
"@typescript-eslint/parser": "^5.12.1",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.5.1",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-prettier": "^8.4.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-if": "^3.0.0",
|
||||
"gulp-json-editor": "^2.5.5",
|
||||
"gulp-replace": "^1.1.0",
|
||||
"gulp-zip": "^5.1.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"husky": "^7.0.4",
|
||||
"jasmine-core": "^3.7.1",
|
||||
"jasmine-spec-reporter": "^7.0.0",
|
||||
"karma": "^6.3.2",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-cli": "^2.0.0",
|
||||
"karma-jasmine": "^4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"lint-staged": "^12.1.3",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"prettier": "^2.5.1",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.34.1",
|
||||
"sass-loader": "^12.4.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tapable": "^1.1.3",
|
||||
"ts-loader": "^9.2.5",
|
||||
"typescript": "4.3.5",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^12.2.13",
|
||||
"@angular/cdk": "^12.2.13",
|
||||
"@angular/common": "^12.2.13",
|
||||
"@angular/compiler": "^12.2.13",
|
||||
"@angular/core": "^12.2.13",
|
||||
"@angular/forms": "^12.2.13",
|
||||
"@angular/platform-browser": "^12.2.13",
|
||||
"@angular/platform-browser-dynamic": "^12.2.13",
|
||||
"@angular/router": "^12.2.13",
|
||||
"@bitwarden/jslib-angular": "file:jslib/angular",
|
||||
"@bitwarden/jslib-common": "file:jslib/common",
|
||||
"core-js": "^3.11.0",
|
||||
"date-input-polyfill": "^2.14.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngx-toastr": "14.1.4",
|
||||
"nord": "^0.2.1",
|
||||
"sweetalert2": "^10.16.6",
|
||||
"web-animations-js": "^2.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~16",
|
||||
"npm": "~8"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./!(jslib)**": "prettier --ignore-unknown --write",
|
||||
"*.ts": "eslint --fix"
|
||||
}
|
||||
}
|
||||
1943
apps/browser/src/_locales/az/messages.json
Normal file
1943
apps/browser/src/_locales/az/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/be/messages.json
Normal file
1943
apps/browser/src/_locales/be/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/bg/messages.json
Normal file
1943
apps/browser/src/_locales/bg/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/bn/messages.json
Normal file
1943
apps/browser/src/_locales/bn/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/bs/messages.json
Normal file
1943
apps/browser/src/_locales/bs/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ca/messages.json
Normal file
1943
apps/browser/src/_locales/ca/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/cs/messages.json
Normal file
1943
apps/browser/src/_locales/cs/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/da/messages.json
Normal file
1943
apps/browser/src/_locales/da/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/de/messages.json
Normal file
1943
apps/browser/src/_locales/de/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/el/messages.json
Normal file
1943
apps/browser/src/_locales/el/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1959
apps/browser/src/_locales/en/messages.json
Normal file
1959
apps/browser/src/_locales/en/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1817
apps/browser/src/_locales/en_GB/messages.json
Normal file
1817
apps/browser/src/_locales/en_GB/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/en_IN/messages.json
Normal file
1943
apps/browser/src/_locales/en_IN/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/es/messages.json
Normal file
1943
apps/browser/src/_locales/es/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/et/messages.json
Normal file
1943
apps/browser/src/_locales/et/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/fa/messages.json
Normal file
1943
apps/browser/src/_locales/fa/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/fi/messages.json
Normal file
1943
apps/browser/src/_locales/fi/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/fil/messages.json
Normal file
1943
apps/browser/src/_locales/fil/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/fr/messages.json
Normal file
1943
apps/browser/src/_locales/fr/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/he/messages.json
Normal file
1943
apps/browser/src/_locales/he/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/hi/messages.json
Normal file
1943
apps/browser/src/_locales/hi/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/hr/messages.json
Normal file
1943
apps/browser/src/_locales/hr/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/hu/messages.json
Normal file
1943
apps/browser/src/_locales/hu/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/id/messages.json
Normal file
1943
apps/browser/src/_locales/id/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/it/messages.json
Normal file
1943
apps/browser/src/_locales/it/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ja/messages.json
Normal file
1943
apps/browser/src/_locales/ja/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ka/messages.json
Normal file
1943
apps/browser/src/_locales/ka/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/km/messages.json
Normal file
1943
apps/browser/src/_locales/km/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/kn/messages.json
Normal file
1943
apps/browser/src/_locales/kn/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ko/messages.json
Normal file
1943
apps/browser/src/_locales/ko/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/lt/messages.json
Normal file
1943
apps/browser/src/_locales/lt/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/lv/messages.json
Normal file
1943
apps/browser/src/_locales/lv/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ml/messages.json
Normal file
1943
apps/browser/src/_locales/ml/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/nb/messages.json
Normal file
1943
apps/browser/src/_locales/nb/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/nl/messages.json
Normal file
1943
apps/browser/src/_locales/nl/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/nn/messages.json
Normal file
1943
apps/browser/src/_locales/nn/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/pl/messages.json
Normal file
1943
apps/browser/src/_locales/pl/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/pt_BR/messages.json
Normal file
1943
apps/browser/src/_locales/pt_BR/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1817
apps/browser/src/_locales/pt_PT/messages.json
Normal file
1817
apps/browser/src/_locales/pt_PT/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ro/messages.json
Normal file
1943
apps/browser/src/_locales/ro/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/ru/messages.json
Normal file
1943
apps/browser/src/_locales/ru/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/si/messages.json
Normal file
1943
apps/browser/src/_locales/si/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/sk/messages.json
Normal file
1943
apps/browser/src/_locales/sk/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/sl/messages.json
Normal file
1943
apps/browser/src/_locales/sl/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/sr/messages.json
Normal file
1943
apps/browser/src/_locales/sr/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/sv/messages.json
Normal file
1943
apps/browser/src/_locales/sv/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/th/messages.json
Normal file
1943
apps/browser/src/_locales/th/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/tr/messages.json
Normal file
1943
apps/browser/src/_locales/tr/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/uk/messages.json
Normal file
1943
apps/browser/src/_locales/uk/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/vi/messages.json
Normal file
1943
apps/browser/src/_locales/vi/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1817
apps/browser/src/_locales/zh_CN/messages.json
Normal file
1817
apps/browser/src/_locales/zh_CN/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1943
apps/browser/src/_locales/zh_TW/messages.json
Normal file
1943
apps/browser/src/_locales/zh_TW/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
6
apps/browser/src/background.html
Normal file
6
apps/browser/src/background.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
6
apps/browser/src/background.ts
Normal file
6
apps/browser/src/background.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import MainBackground from "./background/main.background";
|
||||
|
||||
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
|
||||
bitwardenMain.bootstrap().then(() => {
|
||||
// Finished bootstrapping
|
||||
});
|
||||
115
apps/browser/src/background/commands.background.ts
Normal file
115
apps/browser/src/background/commands.background.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
|
||||
import { BrowserApi } from "../browser/browserApi";
|
||||
|
||||
import MainBackground from "./main.background";
|
||||
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
|
||||
|
||||
export default class CommandsBackground {
|
||||
private isSafari: boolean;
|
||||
private isVivaldi: boolean;
|
||||
|
||||
constructor(
|
||||
private main: MainBackground,
|
||||
private passwordGenerationService: PasswordGenerationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.isSafari = this.platformUtilsService.isSafari();
|
||||
this.isVivaldi = this.platformUtilsService.isVivaldi();
|
||||
}
|
||||
|
||||
async init() {
|
||||
BrowserApi.messageListener(
|
||||
"commands.background",
|
||||
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
|
||||
if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") {
|
||||
await this.processCommand(
|
||||
msg.data.commandToRetry.msg.command,
|
||||
msg.data.commandToRetry.sender
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isVivaldi && msg.command === "keyboardShortcutTriggered" && msg.shortcut) {
|
||||
await this.processCommand(msg.shortcut, sender);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!this.isVivaldi && chrome && chrome.commands) {
|
||||
chrome.commands.onCommand.addListener(async (command: string) => {
|
||||
await this.processCommand(command);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async processCommand(command: string, sender?: chrome.runtime.MessageSender) {
|
||||
switch (command) {
|
||||
case "generate_password":
|
||||
await this.generatePasswordToClipboard();
|
||||
break;
|
||||
case "autofill_login":
|
||||
await this.autoFillLogin(sender ? sender.tab : null);
|
||||
break;
|
||||
case "open_popup":
|
||||
await this.openPopup();
|
||||
break;
|
||||
case "lock_vault":
|
||||
await this.vaultTimeoutService.lock(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePasswordToClipboard() {
|
||||
const options = (await this.passwordGenerationService.getOptions())[0];
|
||||
const password = await this.passwordGenerationService.generatePassword(options);
|
||||
this.platformUtilsService.copyToClipboard(password, { window: window });
|
||||
this.passwordGenerationService.addHistory(password);
|
||||
}
|
||||
|
||||
private async autoFillLogin(tab?: chrome.tabs.Tab) {
|
||||
if (!tab) {
|
||||
tab = await BrowserApi.getTabFromCurrentWindowId();
|
||||
}
|
||||
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsItem = {
|
||||
commandToRetry: {
|
||||
msg: { command: "autofill_login" },
|
||||
sender: { tab: tab },
|
||||
},
|
||||
target: "commands.background",
|
||||
};
|
||||
await BrowserApi.tabSendMessageData(
|
||||
tab,
|
||||
"addToLockedVaultPendingNotifications",
|
||||
retryMessage
|
||||
);
|
||||
|
||||
BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd");
|
||||
}
|
||||
|
||||
private async openPopup() {
|
||||
// Chrome APIs cannot open popup
|
||||
if (!this.isSafari) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.main.openPopup();
|
||||
}
|
||||
}
|
||||
142
apps/browser/src/background/contextMenus.background.ts
Normal file
142
apps/browser/src/background/contextMenus.background.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { EventService } from "jslib-common/abstractions/event.service";
|
||||
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { TotpService } from "jslib-common/abstractions/totp.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
|
||||
import { EventType } from "jslib-common/enums/eventType";
|
||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||
|
||||
import { BrowserApi } from "../browser/browserApi";
|
||||
|
||||
import MainBackground from "./main.background";
|
||||
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
|
||||
|
||||
export default class ContextMenusBackground {
|
||||
private readonly noopCommandSuffix = "noop";
|
||||
private contextMenus: any;
|
||||
|
||||
constructor(
|
||||
private main: MainBackground,
|
||||
private cipherService: CipherService,
|
||||
private passwordGenerationService: PasswordGenerationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private authService: AuthService,
|
||||
private eventService: EventService,
|
||||
private totpService: TotpService
|
||||
) {
|
||||
this.contextMenus = chrome.contextMenus;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.contextMenus) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.contextMenus.onClicked.addListener(
|
||||
async (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => {
|
||||
if (info.menuItemId === "generate-password") {
|
||||
await this.generatePasswordToClipboard();
|
||||
} else if (info.menuItemId === "copy-identifier") {
|
||||
await this.getClickedElement(tab, info.frameId);
|
||||
} else if (
|
||||
info.parentMenuItemId === "autofill" ||
|
||||
info.parentMenuItemId === "copy-username" ||
|
||||
info.parentMenuItemId === "copy-password" ||
|
||||
info.parentMenuItemId === "copy-totp"
|
||||
) {
|
||||
await this.cipherAction(tab, info);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
BrowserApi.messageListener(
|
||||
"contextmenus.background",
|
||||
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
|
||||
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
|
||||
await this.cipherAction(
|
||||
msg.data.commandToRetry.sender.tab,
|
||||
msg.data.commandToRetry.msg.data
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async generatePasswordToClipboard() {
|
||||
const options = (await this.passwordGenerationService.getOptions())[0];
|
||||
const password = await this.passwordGenerationService.generatePassword(options);
|
||||
this.platformUtilsService.copyToClipboard(password, { window: window });
|
||||
this.passwordGenerationService.addHistory(password);
|
||||
}
|
||||
|
||||
private async getClickedElement(tab: chrome.tabs.Tab, frameId: number) {
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserApi.tabSendMessage(tab, { command: "getClickedElement" }, { frameId: frameId });
|
||||
}
|
||||
|
||||
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
||||
const id = info.menuItemId.split("_")[1];
|
||||
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsItem = {
|
||||
commandToRetry: {
|
||||
msg: { command: this.noopCommandSuffix, data: info },
|
||||
sender: { tab: tab },
|
||||
},
|
||||
target: "contextmenus.background",
|
||||
};
|
||||
await BrowserApi.tabSendMessageData(
|
||||
tab,
|
||||
"addToLockedVaultPendingNotifications",
|
||||
retryMessage
|
||||
);
|
||||
|
||||
BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
||||
return;
|
||||
}
|
||||
|
||||
let cipher: CipherView;
|
||||
if (id === this.noopCommandSuffix) {
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
|
||||
cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
|
||||
} else {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
cipher = ciphers.find((c) => c.id === id);
|
||||
}
|
||||
|
||||
if (cipher == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.parentMenuItemId === "autofill") {
|
||||
await this.startAutofillPage(tab, cipher);
|
||||
} else if (info.parentMenuItemId === "copy-username") {
|
||||
this.platformUtilsService.copyToClipboard(cipher.login.username, { window: window });
|
||||
} else if (info.parentMenuItemId === "copy-password") {
|
||||
this.platformUtilsService.copyToClipboard(cipher.login.password, { window: window });
|
||||
this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
||||
} else if (info.parentMenuItemId === "copy-totp") {
|
||||
const totpValue = await this.totpService.getCode(cipher.login.totp);
|
||||
this.platformUtilsService.copyToClipboard(totpValue, { window: window });
|
||||
}
|
||||
}
|
||||
|
||||
private async startAutofillPage(tab: chrome.tabs.Tab, cipher: CipherView) {
|
||||
this.main.loginToAutoFill = cipher;
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserApi.tabSendMessage(tab, {
|
||||
command: "collectPageDetails",
|
||||
tab: tab,
|
||||
sender: "contextMenu",
|
||||
});
|
||||
}
|
||||
}
|
||||
72
apps/browser/src/background/idle.background.ts
Normal file
72
apps/browser/src/background/idle.background.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NotificationsService } from "jslib-common/abstractions/notifications.service";
|
||||
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
|
||||
|
||||
import { StateService } from "../services/abstractions/state.service";
|
||||
|
||||
const IdleInterval = 60 * 5; // 5 minutes
|
||||
|
||||
export default class IdleBackground {
|
||||
private idle: any;
|
||||
private idleTimer: number = null;
|
||||
private idleState = "active";
|
||||
|
||||
constructor(
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private stateService: StateService,
|
||||
private notificationsService: NotificationsService
|
||||
) {
|
||||
this.idle = chrome.idle || (browser != null ? browser.idle : null);
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idleHandler = (newState: string) => {
|
||||
if (newState === "active") {
|
||||
this.notificationsService.reconnectFromActivity();
|
||||
} else {
|
||||
this.notificationsService.disconnectFromInactivity();
|
||||
}
|
||||
};
|
||||
if (this.idle.onStateChanged && this.idle.setDetectionInterval) {
|
||||
this.idle.setDetectionInterval(IdleInterval);
|
||||
this.idle.onStateChanged.addListener(idleHandler);
|
||||
} else {
|
||||
this.pollIdle(idleHandler);
|
||||
}
|
||||
|
||||
if (this.idle.onStateChanged) {
|
||||
this.idle.onStateChanged.addListener(async (newState: string) => {
|
||||
if (newState === "locked") {
|
||||
// If the screen is locked or the screensaver activates
|
||||
const timeout = await this.stateService.getVaultTimeout();
|
||||
if (timeout === -2) {
|
||||
// On System Lock vault timeout option
|
||||
const action = await this.stateService.getVaultTimeoutAction();
|
||||
if (action === "logOut") {
|
||||
await this.vaultTimeoutService.logOut();
|
||||
} else {
|
||||
await this.vaultTimeoutService.lock(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private pollIdle(handler: (newState: string) => void) {
|
||||
if (this.idleTimer != null) {
|
||||
window.clearTimeout(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
this.idle.queryState(IdleInterval, (state: string) => {
|
||||
if (state !== this.idleState) {
|
||||
this.idleState = state;
|
||||
handler(state);
|
||||
}
|
||||
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
1025
apps/browser/src/background/main.background.ts
Normal file
1025
apps/browser/src/background/main.background.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
import NotificationQueueMessage from "./notificationQueueMessage";
|
||||
|
||||
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
cipherId: string;
|
||||
newPassword: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import NotificationQueueMessage from "./notificationQueueMessage";
|
||||
|
||||
export default class AddLoginQueueMessage extends NotificationQueueMessage {
|
||||
username: string;
|
||||
password: string;
|
||||
uri: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default class AddLoginRuntimeMessage {
|
||||
username: string;
|
||||
password: string;
|
||||
url: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default class ChangePasswordRuntimeMessage {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
url: string;
|
||||
}
|
||||
8
apps/browser/src/background/models/iconDetails.ts
Normal file
8
apps/browser/src/background/models/iconDetails.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default interface IconDetails {
|
||||
path: {
|
||||
19: string;
|
||||
38: string;
|
||||
};
|
||||
// Chrome does not support windowId, only Firefox
|
||||
windowId?: number;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default class LockedVaultPendingNotificationsItem {
|
||||
commandToRetry: {
|
||||
msg: any;
|
||||
sender: chrome.runtime.MessageSender;
|
||||
};
|
||||
target: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
|
||||
|
||||
export default class NotificationQueueMessage {
|
||||
type: NotificationQueueMessageType;
|
||||
domain: string;
|
||||
tabId: number;
|
||||
expires: Date;
|
||||
wasVaultLocked: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum NotificationQueueMessageType {
|
||||
AddLogin = 0,
|
||||
ChangePassword = 1,
|
||||
}
|
||||
379
apps/browser/src/background/nativeMessaging.background.ts
Normal file
379
apps/browser/src/background/nativeMessaging.background.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { EncString } from "jslib-common/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { BrowserApi } from "../browser/browserApi";
|
||||
|
||||
import RuntimeBackground from "./runtime.background";
|
||||
|
||||
const MessageValidTimeout = 10 * 1000;
|
||||
const EncryptionAlgorithm = "sha1";
|
||||
|
||||
type Message = {
|
||||
command: string;
|
||||
|
||||
// Filled in by this service
|
||||
userId?: string;
|
||||
timestamp?: number;
|
||||
|
||||
// Used for sharing secret
|
||||
publicKey?: string;
|
||||
};
|
||||
|
||||
type OuterMessage = {
|
||||
message: Message | EncString;
|
||||
appId: string;
|
||||
};
|
||||
|
||||
type ReceiveMessage = {
|
||||
timestamp: number;
|
||||
command: string;
|
||||
response?: any;
|
||||
|
||||
// Unlock key
|
||||
keyB64?: string;
|
||||
};
|
||||
|
||||
type ReceiveMessageOuter = {
|
||||
command: string;
|
||||
appId: string;
|
||||
|
||||
// Should only have one of these.
|
||||
message?: EncString;
|
||||
sharedSecret?: string;
|
||||
};
|
||||
|
||||
export class NativeMessagingBackground {
|
||||
private connected = false;
|
||||
private connecting: boolean;
|
||||
private port: browser.runtime.Port | chrome.runtime.Port;
|
||||
|
||||
private resolver: any = null;
|
||||
private privateKey: ArrayBuffer = null;
|
||||
private publicKey: ArrayBuffer = null;
|
||||
private secureSetupResolve: any = null;
|
||||
private sharedSecret: SymmetricCryptoKey;
|
||||
private appId: string;
|
||||
private validatingFingerprint: boolean;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private runtimeBackground: RuntimeBackground,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private appIdService: AppIdService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.stateService.setBiometricFingerprintValidated(false);
|
||||
|
||||
if (chrome?.permissions?.onAdded) {
|
||||
// Reload extension to activate nativeMessaging
|
||||
chrome.permissions.onAdded.addListener((permissions) => {
|
||||
BrowserApi.reloadExtension(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.appId = await this.appIdService.getAppId();
|
||||
this.stateService.setBiometricFingerprintValidated(false);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.port = BrowserApi.connectNative("com.8bit.bitwarden");
|
||||
|
||||
this.connecting = true;
|
||||
|
||||
const connectedCallback = () => {
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Safari has a bundled native component which is always available, no need to
|
||||
// check if the desktop app is running.
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
connectedCallback();
|
||||
}
|
||||
|
||||
this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => {
|
||||
switch (message.command) {
|
||||
case "connected":
|
||||
connectedCallback();
|
||||
break;
|
||||
case "disconnected":
|
||||
if (this.connecting) {
|
||||
reject("startDesktop");
|
||||
}
|
||||
this.connected = false;
|
||||
this.port.disconnect();
|
||||
break;
|
||||
case "setupEncryption": {
|
||||
// Ignore since it belongs to another device
|
||||
if (message.appId !== this.appId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encrypted = Utils.fromB64ToArray(message.sharedSecret);
|
||||
const decrypted = await this.cryptoFunctionService.rsaDecrypt(
|
||||
encrypted.buffer,
|
||||
this.privateKey,
|
||||
EncryptionAlgorithm
|
||||
);
|
||||
|
||||
if (this.validatingFingerprint) {
|
||||
this.validatingFingerprint = false;
|
||||
this.stateService.setBiometricFingerprintValidated(true);
|
||||
}
|
||||
this.sharedSecret = new SymmetricCryptoKey(decrypted);
|
||||
this.secureSetupResolve();
|
||||
break;
|
||||
}
|
||||
case "invalidateEncryption":
|
||||
// Ignore since it belongs to another device
|
||||
if (message.appId !== this.appId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sharedSecret = null;
|
||||
this.privateKey = null;
|
||||
this.connected = false;
|
||||
|
||||
this.messagingService.send("showDialog", {
|
||||
text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
|
||||
title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "error",
|
||||
});
|
||||
break;
|
||||
case "verifyFingerprint": {
|
||||
if (this.sharedSecret == null) {
|
||||
this.validatingFingerprint = true;
|
||||
this.showFingerprintDialog();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "wrongUserId":
|
||||
this.showWrongUserDialog();
|
||||
break;
|
||||
default:
|
||||
// Ignore since it belongs to another device
|
||||
if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onMessage(message.message);
|
||||
}
|
||||
});
|
||||
|
||||
this.port.onDisconnect.addListener((p: any) => {
|
||||
let error;
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
error = p.error.message;
|
||||
} else {
|
||||
error = chrome.runtime.lastError.message;
|
||||
}
|
||||
|
||||
this.sharedSecret = null;
|
||||
this.privateKey = null;
|
||||
this.connected = false;
|
||||
|
||||
const reason = error != null ? "desktopIntegrationDisabled" : null;
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showWrongUserDialog() {
|
||||
this.messagingService.send("showDialog", {
|
||||
text: this.i18nService.t("nativeMessagingWrongUserDesc"),
|
||||
title: this.i18nService.t("nativeMessagingWrongUserTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: Message) {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
message.userId = await this.stateService.getUserId();
|
||||
message.timestamp = Date.now();
|
||||
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
this.postMessage(message as any);
|
||||
} else {
|
||||
this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) });
|
||||
}
|
||||
}
|
||||
|
||||
async encryptMessage(message: Message) {
|
||||
if (this.sharedSecret == null) {
|
||||
await this.secureCommunication();
|
||||
}
|
||||
|
||||
return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret);
|
||||
}
|
||||
|
||||
getResponse(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private postMessage(message: OuterMessage) {
|
||||
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
|
||||
try {
|
||||
this.port.postMessage(message);
|
||||
} catch (e) {
|
||||
this.logService.error("NativeMessaging port disconnected, disconnecting.");
|
||||
|
||||
this.sharedSecret = null;
|
||||
this.privateKey = null;
|
||||
this.connected = false;
|
||||
|
||||
this.messagingService.send("showDialog", {
|
||||
text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
|
||||
title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessage(rawMessage: ReceiveMessage | EncString) {
|
||||
let message = rawMessage as ReceiveMessage;
|
||||
if (!this.platformUtilsService.isSafari()) {
|
||||
message = JSON.parse(
|
||||
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecret)
|
||||
);
|
||||
}
|
||||
|
||||
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
|
||||
this.logService.error("NativeMessage is to old, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.command) {
|
||||
case "biometricUnlock": {
|
||||
await this.stateService.setBiometricAwaitingAcceptance(null);
|
||||
|
||||
if (message.response === "not enabled") {
|
||||
this.messagingService.send("showDialog", {
|
||||
text: this.i18nService.t("biometricsNotEnabledDesc"),
|
||||
title: this.i18nService.t("biometricsNotEnabledTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "error",
|
||||
});
|
||||
break;
|
||||
} else if (message.response === "not supported") {
|
||||
this.messagingService.send("showDialog", {
|
||||
text: this.i18nService.t("biometricsNotSupportedDesc"),
|
||||
title: this.i18nService.t("biometricsNotSupportedTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "error",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
const enabled = await this.stateService.getBiometricUnlock();
|
||||
if (enabled === null || enabled === false) {
|
||||
if (message.response === "unlocked") {
|
||||
await this.stateService.setBiometricUnlock(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Ignore unlock if already unlocked
|
||||
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.response === "unlocked") {
|
||||
await this.cryptoService.setKey(
|
||||
new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)
|
||||
);
|
||||
|
||||
// Verify key is correct by attempting to decrypt a secret
|
||||
try {
|
||||
await this.cryptoService.getFingerprint(await this.stateService.getUserId());
|
||||
} catch (e) {
|
||||
this.logService.error("Unable to verify key: " + e);
|
||||
await this.cryptoService.clearKey();
|
||||
this.showWrongUserDialog();
|
||||
|
||||
// Exit early
|
||||
if (this.resolver) {
|
||||
this.resolver(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setBiometricLocked(false);
|
||||
this.runtimeBackground.processMessage({ command: "unlocked" }, null, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.logService.error("NativeMessage, got unknown command: " + message.command);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.resolver) {
|
||||
this.resolver(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async secureCommunication() {
|
||||
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
|
||||
this.sendUnencrypted({
|
||||
command: "setupEncryption",
|
||||
publicKey: Utils.fromBufferToB64(publicKey),
|
||||
userId: await this.stateService.getUserId(),
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
|
||||
}
|
||||
|
||||
private async sendUnencrypted(message: Message) {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
message.timestamp = Date.now();
|
||||
|
||||
this.postMessage({ appId: this.appId, message: message });
|
||||
}
|
||||
|
||||
private async showFingerprintDialog() {
|
||||
const fingerprint = (
|
||||
await this.cryptoService.getFingerprint(await this.stateService.getUserId(), this.publicKey)
|
||||
).join(" ");
|
||||
|
||||
this.messagingService.send("showDialog", {
|
||||
html: `${this.i18nService.t(
|
||||
"desktopIntegrationVerificationText"
|
||||
)}<br><br><strong>${fingerprint}</strong>`,
|
||||
title: this.i18nService.t("desktopSyncVerificationTitle"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
449
apps/browser/src/background/notification.background.ts
Normal file
449
apps/browser/src/background/notification.background.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
import { CipherType } from "jslib-common/enums/cipherType";
|
||||
import { PolicyType } from "jslib-common/enums/policyType";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||
import { LoginUriView } from "jslib-common/models/view/loginUriView";
|
||||
import { LoginView } from "jslib-common/models/view/loginView";
|
||||
|
||||
import { BrowserApi } from "../browser/browserApi";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { StateService } from "../services/abstractions/state.service";
|
||||
|
||||
import AddChangePasswordQueueMessage from "./models/addChangePasswordQueueMessage";
|
||||
import AddLoginQueueMessage from "./models/addLoginQueueMessage";
|
||||
import AddLoginRuntimeMessage from "./models/addLoginRuntimeMessage";
|
||||
import ChangePasswordRuntimeMessage from "./models/changePasswordRuntimeMessage";
|
||||
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
|
||||
import { NotificationQueueMessageType } from "./models/notificationQueueMessageType";
|
||||
|
||||
export default class NotificationBackground {
|
||||
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
|
||||
|
||||
constructor(
|
||||
private autofillService: AutofillService,
|
||||
private cipherService: CipherService,
|
||||
private authService: AuthService,
|
||||
private policyService: PolicyService,
|
||||
private folderService: FolderService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
if (chrome.runtime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserApi.messageListener(
|
||||
"notification.background",
|
||||
async (msg: any, sender: chrome.runtime.MessageSender) => {
|
||||
await this.processMessage(msg, sender);
|
||||
}
|
||||
);
|
||||
|
||||
this.cleanupNotificationQueue();
|
||||
}
|
||||
|
||||
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
|
||||
switch (msg.command) {
|
||||
case "unlockCompleted":
|
||||
if (msg.data.target !== "notification.background") {
|
||||
return;
|
||||
}
|
||||
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
|
||||
break;
|
||||
case "bgGetDataForTab":
|
||||
await this.getDataForTab(sender.tab, msg.responseCommand);
|
||||
break;
|
||||
case "bgCloseNotificationBar":
|
||||
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
|
||||
break;
|
||||
case "bgAdjustNotificationBar":
|
||||
await BrowserApi.tabSendMessageData(sender.tab, "adjustNotificationBar", msg.data);
|
||||
break;
|
||||
case "bgAddLogin":
|
||||
await this.addLogin(msg.login, sender.tab);
|
||||
break;
|
||||
case "bgChangedPassword":
|
||||
await this.changedPassword(msg.data, sender.tab);
|
||||
break;
|
||||
case "bgAddClose":
|
||||
case "bgChangeClose":
|
||||
this.removeTabFromNotificationQueue(sender.tab);
|
||||
break;
|
||||
case "bgAddSave":
|
||||
case "bgChangeSave":
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsItem = {
|
||||
commandToRetry: {
|
||||
msg: msg,
|
||||
sender: sender,
|
||||
},
|
||||
target: "notification.background",
|
||||
};
|
||||
await BrowserApi.tabSendMessageData(
|
||||
sender.tab,
|
||||
"addToLockedVaultPendingNotifications",
|
||||
retryMessage
|
||||
);
|
||||
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
|
||||
return;
|
||||
}
|
||||
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
|
||||
break;
|
||||
case "bgNeverSave":
|
||||
await this.saveNever(sender.tab);
|
||||
break;
|
||||
case "collectPageDetailsResponse":
|
||||
switch (msg.sender) {
|
||||
case "notificationBar": {
|
||||
const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
|
||||
await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", {
|
||||
details: msg.details,
|
||||
forms: forms,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> {
|
||||
if (this.notificationQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab != null) {
|
||||
this.doNotificationQueueCheck(tab);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (currentTab != null) {
|
||||
this.doNotificationQueueCheck(currentTab);
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupNotificationQueue() {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
if (this.notificationQueue[i].expires < new Date()) {
|
||||
this.notificationQueue.splice(i, 1);
|
||||
}
|
||||
}
|
||||
setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes
|
||||
}
|
||||
|
||||
private doNotificationQueueCheck(tab: chrome.tabs.Tab): void {
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabDomain = Utils.getDomain(tab.url);
|
||||
if (tabDomain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.notificationQueue.length; i++) {
|
||||
if (
|
||||
this.notificationQueue[i].tabId !== tab.id ||
|
||||
this.notificationQueue[i].domain !== tabDomain
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.notificationQueue[i].type === NotificationQueueMessageType.AddLogin) {
|
||||
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
|
||||
type: "add",
|
||||
typeData: {
|
||||
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
|
||||
},
|
||||
});
|
||||
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
|
||||
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
|
||||
type: "change",
|
||||
typeData: {
|
||||
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
if (this.notificationQueue[i].tabId === tab.id) {
|
||||
this.notificationQueue.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) {
|
||||
const authStatus = await this.authService.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loginDomain = Utils.getDomain(loginInfo.url);
|
||||
if (loginDomain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let normalizedUsername = loginInfo.username;
|
||||
if (normalizedUsername != null) {
|
||||
normalizedUsername = normalizedUsername.toLowerCase();
|
||||
}
|
||||
|
||||
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification();
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
if (disabledAddLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.allowPersonalOwnership())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
|
||||
const usernameMatches = ciphers.filter(
|
||||
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername
|
||||
);
|
||||
if (usernameMatches.length === 0) {
|
||||
if (disabledAddLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.allowPersonalOwnership())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
|
||||
} else if (
|
||||
usernameMatches.length === 1 &&
|
||||
usernameMatches[0].login.password !== loginInfo.password
|
||||
) {
|
||||
const disabledChangePassword =
|
||||
await this.stateService.getDisableChangedPasswordNotification();
|
||||
if (disabledChangePassword) {
|
||||
return;
|
||||
}
|
||||
this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushAddLoginToQueue(
|
||||
loginDomain: string,
|
||||
loginInfo: AddLoginRuntimeMessage,
|
||||
tab: chrome.tabs.Tab,
|
||||
isVaultLocked = false
|
||||
) {
|
||||
// remove any old messages for this tab
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const message: AddLoginQueueMessage = {
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
username: loginInfo.username,
|
||||
password: loginInfo.password,
|
||||
domain: loginDomain,
|
||||
uri: loginInfo.url,
|
||||
tabId: tab.id,
|
||||
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
|
||||
wasVaultLocked: isVaultLocked,
|
||||
};
|
||||
this.notificationQueue.push(message);
|
||||
await this.checkNotificationQueue(tab);
|
||||
}
|
||||
|
||||
private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) {
|
||||
const loginDomain = Utils.getDomain(changeData.url);
|
||||
if (loginDomain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id: string = null;
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url);
|
||||
if (changeData.currentPassword != null) {
|
||||
const passwordMatches = ciphers.filter(
|
||||
(c) => c.login.password === changeData.currentPassword
|
||||
);
|
||||
if (passwordMatches.length === 1) {
|
||||
id = passwordMatches[0].id;
|
||||
}
|
||||
} else if (ciphers.length === 1) {
|
||||
id = ciphers[0].id;
|
||||
}
|
||||
if (id != null) {
|
||||
this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushChangePasswordToQueue(
|
||||
cipherId: string,
|
||||
loginDomain: string,
|
||||
newPassword: string,
|
||||
tab: chrome.tabs.Tab,
|
||||
isVaultLocked = false
|
||||
) {
|
||||
// remove any old messages for this tab
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const message: AddChangePasswordQueueMessage = {
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
cipherId: cipherId,
|
||||
newPassword: newPassword,
|
||||
domain: loginDomain,
|
||||
tabId: tab.id,
|
||||
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
|
||||
wasVaultLocked: isVaultLocked,
|
||||
};
|
||||
this.notificationQueue.push(message);
|
||||
await this.checkNotificationQueue(tab);
|
||||
}
|
||||
|
||||
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tabId !== tab.id ||
|
||||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
|
||||
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tabDomain = Utils.getDomain(tab.url);
|
||||
if (tabDomain != null && tabDomain !== queueMessage.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.notificationQueue.splice(i, 1);
|
||||
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||
const changePasswordMessage = queueMessage as AddChangePasswordQueueMessage;
|
||||
const cipher = await this.getDecryptedCipherById(changePasswordMessage.cipherId);
|
||||
if (cipher == null) {
|
||||
return;
|
||||
}
|
||||
await this.updateCipher(cipher, changePasswordMessage.newPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
|
||||
if (!queueMessage.wasVaultLocked) {
|
||||
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
|
||||
BrowserApi.tabSendMessageData(tab, "addedCipher");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the vault was locked, check if a cipher needs updating instead of creating a new one
|
||||
const addLoginMessage = queueMessage as AddLoginQueueMessage;
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(addLoginMessage.uri);
|
||||
const usernameMatches = ciphers.filter(
|
||||
(c) =>
|
||||
c.login.username != null && c.login.username.toLowerCase() === addLoginMessage.username
|
||||
);
|
||||
|
||||
if (usernameMatches.length >= 1) {
|
||||
await this.updateCipher(usernameMatches[0], addLoginMessage.password);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createNewCipher(addLoginMessage, folderId);
|
||||
BrowserApi.tabSendMessageData(tab, "addedCipher");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
|
||||
const loginModel = new LoginView();
|
||||
const loginUri = new LoginUriView();
|
||||
loginUri.uri = queueMessage.uri;
|
||||
loginModel.uris = [loginUri];
|
||||
loginModel.username = queueMessage.username;
|
||||
loginModel.password = queueMessage.password;
|
||||
const model = new CipherView();
|
||||
model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
|
||||
model.name = model.name.replace(/^www\./, "");
|
||||
model.type = CipherType.Login;
|
||||
model.login = loginModel;
|
||||
|
||||
if (!Utils.isNullOrWhitespace(folderId)) {
|
||||
const folders = await this.folderService.getAllDecrypted();
|
||||
if (folders.some((x) => x.id === folderId)) {
|
||||
model.folderId = folderId;
|
||||
}
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.encrypt(model);
|
||||
await this.cipherService.saveWithServer(cipher);
|
||||
}
|
||||
|
||||
private async getDecryptedCipherById(cipherId: string) {
|
||||
const cipher = await this.cipherService.get(cipherId);
|
||||
if (cipher != null && cipher.type === CipherType.Login) {
|
||||
return await cipher.decrypt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async updateCipher(cipher: CipherView, newPassword: string) {
|
||||
if (cipher != null && cipher.type === CipherType.Login) {
|
||||
cipher.login.password = newPassword;
|
||||
const newCipher = await this.cipherService.encrypt(cipher);
|
||||
await this.cipherService.saveWithServer(newCipher);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveNever(tab: chrome.tabs.Tab) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tabId !== tab.id ||
|
||||
queueMessage.type !== NotificationQueueMessageType.AddLogin
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tabDomain = Utils.getDomain(tab.url);
|
||||
if (tabDomain != null && tabDomain !== queueMessage.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.notificationQueue.splice(i, 1);
|
||||
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
|
||||
|
||||
const hostname = Utils.getHostname(tab.url);
|
||||
await this.cipherService.saveNeverDomain(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) {
|
||||
const responseData: any = {};
|
||||
if (responseCommand === "notificationBarGetFoldersList") {
|
||||
responseData.folders = await this.folderService.getAllDecrypted();
|
||||
}
|
||||
|
||||
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
|
||||
}
|
||||
|
||||
private async allowPersonalOwnership(): Promise<boolean> {
|
||||
return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
|
||||
}
|
||||
}
|
||||
235
apps/browser/src/background/runtime.background.ts
Normal file
235
apps/browser/src/background/runtime.background.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { NotificationsService } from "jslib-common/abstractions/notifications.service";
|
||||
import { SystemService } from "jslib-common/abstractions/system.service";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { BrowserApi } from "../browser/browserApi";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
|
||||
|
||||
import MainBackground from "./main.background";
|
||||
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
|
||||
|
||||
export default class RuntimeBackground {
|
||||
private autofillTimeout: any;
|
||||
private pageDetailsToAutoFill: any[] = [];
|
||||
private onInstalledReason: string = null;
|
||||
private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = [];
|
||||
|
||||
constructor(
|
||||
private main: MainBackground,
|
||||
private autofillService: AutofillService,
|
||||
private platformUtilsService: BrowserPlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private notificationsService: NotificationsService,
|
||||
private systemService: SystemService,
|
||||
private environmentService: EnvironmentService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
this.onInstalledReason = details.reason;
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!chrome.runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.checkOnInstalled();
|
||||
const backgroundMessageListener = async (
|
||||
msg: any,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: any
|
||||
) => {
|
||||
await this.processMessage(msg, sender, sendResponse);
|
||||
};
|
||||
|
||||
BrowserApi.messageListener("runtime.background", backgroundMessageListener);
|
||||
if (this.main.isPrivateMode) {
|
||||
(window as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
|
||||
}
|
||||
}
|
||||
|
||||
async processMessage(msg: any, sender: any, sendResponse: any) {
|
||||
switch (msg.command) {
|
||||
case "loggedIn":
|
||||
case "unlocked": {
|
||||
let item: LockedVaultPendingNotificationsItem;
|
||||
|
||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||
await BrowserApi.closeLoginTab();
|
||||
|
||||
item = this.lockedVaultPendingNotifications.pop();
|
||||
if (item.commandToRetry.sender?.tab?.id) {
|
||||
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
await this.main.setIcon();
|
||||
await this.main.refreshBadgeAndMenu(false);
|
||||
this.notificationsService.updateConnection(msg.command === "unlocked");
|
||||
this.systemService.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
await BrowserApi.tabSendMessageData(
|
||||
item.commandToRetry.sender.tab,
|
||||
"unlockCompleted",
|
||||
item
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "addToLockedVaultPendingNotifications":
|
||||
this.lockedVaultPendingNotifications.push(msg.data);
|
||||
break;
|
||||
case "logout":
|
||||
await this.main.logout(msg.expired, msg.userId);
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (msg.successfully) {
|
||||
setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000);
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
await this.main.openPopup();
|
||||
break;
|
||||
case "promptForLogin":
|
||||
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
|
||||
break;
|
||||
case "showDialogResolve":
|
||||
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
|
||||
break;
|
||||
case "bgCollectPageDetails":
|
||||
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
|
||||
break;
|
||||
case "bgUpdateContextMenu":
|
||||
case "editedCipher":
|
||||
case "addedCipher":
|
||||
case "deletedCipher":
|
||||
await this.main.refreshBadgeAndMenu();
|
||||
break;
|
||||
case "bgReseedStorage":
|
||||
await this.main.reseedStorage();
|
||||
break;
|
||||
case "collectPageDetailsResponse":
|
||||
switch (msg.sender) {
|
||||
case "autofiller":
|
||||
case "autofill_cmd": {
|
||||
const totpCode = await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
frameId: sender.frameId,
|
||||
tab: msg.tab,
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
msg.sender === "autofill_cmd"
|
||||
);
|
||||
if (totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "contextMenu":
|
||||
clearTimeout(this.autofillTimeout);
|
||||
this.pageDetailsToAutoFill.push({
|
||||
frameId: sender.frameId,
|
||||
tab: msg.tab,
|
||||
details: msg.details,
|
||||
});
|
||||
this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "authResult": {
|
||||
const vaultUrl = this.environmentService.getWebVaultUrl();
|
||||
|
||||
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BrowserApi.createNewTab(
|
||||
"popup/index.html?uilocation=popout#/sso?code=" +
|
||||
encodeURIComponent(msg.code) +
|
||||
"&state=" +
|
||||
encodeURIComponent(msg.state)
|
||||
);
|
||||
} catch {
|
||||
this.logService.error("Unable to open sso popout tab");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "webAuthnResult": {
|
||||
const vaultUrl = this.environmentService.getWebVaultUrl();
|
||||
|
||||
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params =
|
||||
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
|
||||
`remember=${encodeURIComponent(msg.remember)}`;
|
||||
BrowserApi.createNewTab(
|
||||
`popup/index.html?uilocation=popout#/2fa;${params}`,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "reloadPopup":
|
||||
this.messagingService.send("reloadPopup");
|
||||
break;
|
||||
case "emailVerificationRequired":
|
||||
this.messagingService.send("showDialog", {
|
||||
dialogId: "emailVerificationRequired",
|
||||
title: this.i18nService.t("emailVerificationRequired"),
|
||||
text: this.i18nService.t("emailVerificationRequiredDesc"),
|
||||
confirmText: this.i18nService.t("ok"),
|
||||
type: "info",
|
||||
});
|
||||
break;
|
||||
case "getClickedElementResponse":
|
||||
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async autofillPage() {
|
||||
const totpCode = await this.autofillService.doAutoFill({
|
||||
cipher: this.main.loginToAutoFill,
|
||||
pageDetails: this.pageDetailsToAutoFill,
|
||||
fillNewPassword: true,
|
||||
});
|
||||
|
||||
if (totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
|
||||
}
|
||||
|
||||
// reset
|
||||
this.main.loginToAutoFill = null;
|
||||
this.pageDetailsToAutoFill = [];
|
||||
}
|
||||
|
||||
private async checkOnInstalled() {
|
||||
setTimeout(async () => {
|
||||
if (this.onInstalledReason != null) {
|
||||
if (this.onInstalledReason === "install") {
|
||||
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
||||
}
|
||||
|
||||
this.onInstalledReason = null;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
63
apps/browser/src/background/tabs.background.ts
Normal file
63
apps/browser/src/background/tabs.background.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import MainBackground from "./main.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
|
||||
export default class TabsBackground {
|
||||
constructor(
|
||||
private main: MainBackground,
|
||||
private notificationBackground: NotificationBackground
|
||||
) {}
|
||||
|
||||
private focusedWindowId: number;
|
||||
|
||||
async init() {
|
||||
if (!chrome.tabs || !chrome.windows) {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.windows.onFocusChanged.addListener(async (windowId: number) => {
|
||||
if (windowId === null || windowId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusedWindowId = windowId;
|
||||
this.main.messagingService.send("windowChanged");
|
||||
});
|
||||
|
||||
chrome.tabs.onActivated.addListener(async (activeInfo: chrome.tabs.TabActiveInfo) => {
|
||||
await this.main.refreshBadgeAndMenu();
|
||||
this.main.messagingService.send("tabChanged");
|
||||
});
|
||||
|
||||
chrome.tabs.onReplaced.addListener(async (addedTabId: number, removedTabId: number) => {
|
||||
if (this.main.onReplacedRan) {
|
||||
return;
|
||||
}
|
||||
this.main.onReplacedRan = true;
|
||||
|
||||
await this.notificationBackground.checkNotificationQueue();
|
||||
await this.main.refreshBadgeAndMenu();
|
||||
this.main.messagingService.send("tabChanged");
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener(
|
||||
async (tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
|
||||
if (this.focusedWindowId > 0 && tab.windowId != this.focusedWindowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tab.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.main.onUpdatedRan) {
|
||||
return;
|
||||
}
|
||||
this.main.onUpdatedRan = true;
|
||||
|
||||
await this.notificationBackground.checkNotificationQueue(tab);
|
||||
await this.main.refreshBadgeAndMenu();
|
||||
this.main.messagingService.send("tabChanged");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
96
apps/browser/src/background/webRequest.background.ts
Normal file
96
apps/browser/src/background/webRequest.background.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
import { UriMatchType } from "jslib-common/enums/uriMatchType";
|
||||
|
||||
export default class WebRequestBackground {
|
||||
private pendingAuthRequests: any[] = [];
|
||||
private webRequest: any;
|
||||
private isFirefox: boolean;
|
||||
|
||||
constructor(
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.webRequest = (window as any).chrome.webRequest;
|
||||
this.isFirefox = platformUtilsService.isFirefox();
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.webRequest || !this.webRequest.onAuthRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webRequest.onAuthRequired.addListener(
|
||||
async (details: any, callback: any) => {
|
||||
if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingAuthRequests.push(details.requestId);
|
||||
|
||||
if (this.isFirefox) {
|
||||
// eslint-disable-next-line
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await this.resolveAuthCredentials(details.url, resolve, reject);
|
||||
});
|
||||
} else {
|
||||
await this.resolveAuthCredentials(details.url, callback, callback);
|
||||
}
|
||||
},
|
||||
{ urls: ["http://*/*", "https://*/*"] },
|
||||
[this.isFirefox ? "blocking" : "asyncBlocking"]
|
||||
);
|
||||
|
||||
this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
|
||||
urls: ["http://*/*"],
|
||||
});
|
||||
this.webRequest.onErrorOccurred.addListener(
|
||||
(details: any) => this.completeAuthRequest(details),
|
||||
{
|
||||
urls: ["http://*/*"],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
private async resolveAuthCredentials(domain: string, success: Function, error: Function) {
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
error();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
domain,
|
||||
null,
|
||||
UriMatchType.Host
|
||||
);
|
||||
if (ciphers == null || ciphers.length !== 1) {
|
||||
error();
|
||||
return;
|
||||
}
|
||||
|
||||
success({
|
||||
authCredentials: {
|
||||
username: ciphers[0].login.username,
|
||||
password: ciphers[0].login.password,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
error();
|
||||
}
|
||||
}
|
||||
|
||||
private completeAuthRequest(details: any) {
|
||||
const i = this.pendingAuthRequests.indexOf(details.requestId);
|
||||
if (i > -1) {
|
||||
this.pendingAuthRequests.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
236
apps/browser/src/browser/browserApi.ts
Normal file
236
apps/browser/src/browser/browserApi.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { SafariApp } from "./safariApp";
|
||||
|
||||
export class BrowserApi {
|
||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||
static isSafariApi: boolean =
|
||||
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
||||
navigator.userAgent.indexOf(" Chrome/") === -1 &&
|
||||
navigator.userAgent.indexOf(" Chromium/") === -1;
|
||||
static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
|
||||
static isFirefoxOnAndroid: boolean =
|
||||
navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1;
|
||||
|
||||
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
||||
return await BrowserApi.tabsQueryFirst({
|
||||
active: true,
|
||||
windowId: chrome.windows.WINDOW_ID_CURRENT,
|
||||
});
|
||||
}
|
||||
|
||||
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
|
||||
return await BrowserApi.tabsQueryFirst({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
}
|
||||
|
||||
static async getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
return await BrowserApi.tabsQuery({
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.query(options, (tabs: any[]) => {
|
||||
resolve(tabs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab> | null {
|
||||
const tabs = await BrowserApi.tabsQuery(options);
|
||||
if (tabs.length > 0) {
|
||||
return tabs[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static tabSendMessageData(
|
||||
tab: chrome.tabs.Tab,
|
||||
command: string,
|
||||
data: any = null
|
||||
): Promise<any[]> {
|
||||
const obj: any = {
|
||||
command: command,
|
||||
};
|
||||
|
||||
if (data != null) {
|
||||
obj.data = data;
|
||||
}
|
||||
|
||||
return BrowserApi.tabSendMessage(tab, obj);
|
||||
}
|
||||
|
||||
static async tabSendMessage(
|
||||
tab: chrome.tabs.Tab,
|
||||
obj: any,
|
||||
options: chrome.tabs.MessageSendOptions = null
|
||||
): Promise<any> {
|
||||
if (!tab || !tab.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
chrome.tabs.sendMessage(tab.id, obj, options, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
// Some error happened
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
||||
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
||||
}
|
||||
|
||||
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
|
||||
return chrome.windows.onCreated.addListener(callback);
|
||||
}
|
||||
|
||||
static getBackgroundPage(): any {
|
||||
return chrome.extension.getBackgroundPage();
|
||||
}
|
||||
|
||||
static getApplicationVersion(): string {
|
||||
return chrome.runtime.getManifest().version;
|
||||
}
|
||||
|
||||
static async isPopupOpen(): Promise<boolean> {
|
||||
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
|
||||
}
|
||||
|
||||
static createNewTab(url: string, extensionPage = false, active = true) {
|
||||
chrome.tabs.create({ url: url, active: active });
|
||||
}
|
||||
|
||||
static messageListener(
|
||||
name: string,
|
||||
callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void
|
||||
) {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(msg: any, sender: chrome.runtime.MessageSender, response: any) => {
|
||||
callback(msg, sender, response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async closeLoginTab() {
|
||||
const tabs = await BrowserApi.tabsQuery({
|
||||
active: true,
|
||||
title: "Bitwarden",
|
||||
windowType: "normal",
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabToClose = tabs[tabs.length - 1].id;
|
||||
chrome.tabs.remove(tabToClose);
|
||||
}
|
||||
|
||||
static async focusSpecifiedTab(tabId: number) {
|
||||
chrome.tabs.update(tabId, { active: true, highlighted: true });
|
||||
}
|
||||
|
||||
static closePopup(win: Window) {
|
||||
if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) {
|
||||
// Reactivating the active tab dismisses the popup tab. The promise final
|
||||
// condition is only called if the popup wasn't already dismissed (future proofing).
|
||||
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1433604
|
||||
browser.tabs.update({ active: true }).finally(win.close);
|
||||
} else {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
|
||||
static downloadFile(win: Window, blobData: any, blobOptions: any, fileName: string) {
|
||||
if (BrowserApi.isSafariApi) {
|
||||
const type = blobOptions != null ? blobOptions.type : null;
|
||||
let data: string = null;
|
||||
if (type === "text/plain" && typeof blobData === "string") {
|
||||
data = blobData;
|
||||
} else {
|
||||
data = Utils.fromBufferToB64(blobData);
|
||||
}
|
||||
SafariApp.sendMessageToApp(
|
||||
"downloadFile",
|
||||
JSON.stringify({
|
||||
blobData: data,
|
||||
blobOptions: blobOptions,
|
||||
fileName: fileName,
|
||||
}),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
const blob = new Blob([blobData], blobOptions);
|
||||
if (navigator.msSaveOrOpenBlob) {
|
||||
navigator.msSaveBlob(blob, fileName);
|
||||
} else {
|
||||
const a = win.document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fileName;
|
||||
win.document.body.appendChild(a);
|
||||
a.click();
|
||||
win.document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gaFilter() {
|
||||
return process.env.ENV !== "production";
|
||||
}
|
||||
|
||||
static getUILanguage(win: Window) {
|
||||
return chrome.i18n.getUILanguage();
|
||||
}
|
||||
|
||||
static reloadExtension(win: Window) {
|
||||
if (win != null) {
|
||||
return win.location.reload(true);
|
||||
} else {
|
||||
return chrome.runtime.reload();
|
||||
}
|
||||
}
|
||||
|
||||
static reloadOpenWindows() {
|
||||
const views = chrome.extension.getViews() as Window[];
|
||||
views
|
||||
.filter((w) => w.location.href != null)
|
||||
.forEach((w) => {
|
||||
w.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
return browser.runtime.connectNative(application);
|
||||
} else if (BrowserApi.isChromeApi) {
|
||||
return chrome.runtime.connectNative(application);
|
||||
}
|
||||
}
|
||||
|
||||
static requestPermission(permission: any) {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
return browser.permissions.request(permission);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.permissions.request(permission, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
static getPlatformInfo(): Promise<browser.runtime.PlatformInfo | chrome.runtime.PlatformInfo> {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
return browser.runtime.getPlatformInfo();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.getPlatformInfo(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
26
apps/browser/src/browser/safariApp.ts
Normal file
26
apps/browser/src/browser/safariApp.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { BrowserApi } from "./browserApi";
|
||||
|
||||
export class SafariApp {
|
||||
static sendMessageToApp(command: string, data: any = null, resolveNow = false): Promise<any> {
|
||||
if (!BrowserApi.isSafariApi) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const now = new Date();
|
||||
const messageId =
|
||||
now.getTime().toString() + "_" + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
(browser as any).runtime.sendNativeMessage(
|
||||
"com.bitwarden.desktop",
|
||||
{
|
||||
id: messageId,
|
||||
command: command,
|
||||
data: data,
|
||||
responseData: null,
|
||||
},
|
||||
(response: any) => {
|
||||
resolve(response);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
36
apps/browser/src/content/autofill.css
Normal file
36
apps/browser/src/content/autofill.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@-webkit-keyframes bitwardenfill {
|
||||
0% {
|
||||
-webkit-transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(1.2, 1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes bitwardenfill {
|
||||
0% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
span[data-bwautofill].com-bitwarden-browser-animated-fill {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.com-bitwarden-browser-animated-fill {
|
||||
animation: bitwardenfill 200ms ease-in-out 0ms 1;
|
||||
-webkit-animation: bitwardenfill 200ms ease-in-out 0ms 1;
|
||||
}
|
||||
1042
apps/browser/src/content/autofill.js
Normal file
1042
apps/browser/src/content/autofill.js
Normal file
File diff suppressed because it is too large
Load Diff
52
apps/browser/src/content/autofiller.ts
Normal file
52
apps/browser/src/content/autofiller.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
let pageHref: string = null;
|
||||
let filledThisHref = false;
|
||||
let delayFillTimeout: number;
|
||||
|
||||
const activeUserIdKey = "activeUserId";
|
||||
let activeUserId: string;
|
||||
|
||||
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
|
||||
if (obj == null || obj[activeUserIdKey] == null) {
|
||||
return;
|
||||
}
|
||||
activeUserId = obj[activeUserIdKey];
|
||||
});
|
||||
|
||||
chrome.storage.local.get(activeUserId, (obj: any) => {
|
||||
if (obj?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
|
||||
setInterval(() => doFillIfNeeded(), 500);
|
||||
}
|
||||
});
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.command === "fillForm" && pageHref === msg.url) {
|
||||
filledThisHref = true;
|
||||
}
|
||||
});
|
||||
|
||||
function doFillIfNeeded(force = false) {
|
||||
if (force || pageHref !== window.location.href) {
|
||||
if (!force) {
|
||||
// Some websites are slow and rendering all page content. Try to fill again later
|
||||
// if we haven't already.
|
||||
filledThisHref = false;
|
||||
if (delayFillTimeout != null) {
|
||||
window.clearTimeout(delayFillTimeout);
|
||||
}
|
||||
delayFillTimeout = window.setTimeout(() => {
|
||||
if (!filledThisHref) {
|
||||
doFillIfNeeded(true);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
pageHref = window.location.href;
|
||||
const msg: any = {
|
||||
command: "bgCollectPageDetails",
|
||||
sender: "autofiller",
|
||||
};
|
||||
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
66
apps/browser/src/content/contextMenuHandler.ts
Normal file
66
apps/browser/src/content/contextMenuHandler.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
const inputTags = ["input", "textarea", "select"];
|
||||
const labelTags = ["label", "span"];
|
||||
const attributes = ["id", "name", "label-aria", "placeholder"];
|
||||
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
|
||||
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
|
||||
|
||||
let clickedEl: HTMLElement = null;
|
||||
|
||||
// Find the best attribute to be used as the Name for an element in a custom field.
|
||||
function getClickedElementIdentifier() {
|
||||
if (clickedEl == null) {
|
||||
return invalidElement;
|
||||
}
|
||||
|
||||
const clickedTag = clickedEl.nodeName.toLowerCase();
|
||||
let inputEl = null;
|
||||
|
||||
// Try to identify the input element (which may not be the clicked element)
|
||||
if (labelTags.includes(clickedTag)) {
|
||||
let inputId = null;
|
||||
if (clickedTag === "label") {
|
||||
inputId = clickedEl.getAttribute("for");
|
||||
} else {
|
||||
inputId = clickedEl.closest("label")?.getAttribute("for");
|
||||
}
|
||||
|
||||
inputEl = document.getElementById(inputId);
|
||||
} else {
|
||||
inputEl = clickedEl;
|
||||
}
|
||||
|
||||
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) {
|
||||
return invalidElement;
|
||||
}
|
||||
|
||||
for (const attr of attributes) {
|
||||
const attributeValue = inputEl.getAttribute(attr);
|
||||
const selector = "[" + attr + '="' + attributeValue + '"]';
|
||||
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
|
||||
return attributeValue;
|
||||
}
|
||||
}
|
||||
return noUniqueIdentifier;
|
||||
}
|
||||
|
||||
function isNullOrEmpty(s: string) {
|
||||
return s == null || s === "";
|
||||
}
|
||||
|
||||
// We only have access to the element that's been clicked when the context menu is first opened.
|
||||
// Remember it for use later.
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
clickedEl = event.target as HTMLElement;
|
||||
});
|
||||
|
||||
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
|
||||
chrome.runtime.onMessage.addListener((event) => {
|
||||
if (event.command === "getClickedElement") {
|
||||
const identifier = getClickedElementIdentifier();
|
||||
chrome.runtime.sendMessage({
|
||||
command: "getClickedElementResponse",
|
||||
sender: "contextMenuHandler",
|
||||
identifier: identifier,
|
||||
});
|
||||
}
|
||||
});
|
||||
38
apps/browser/src/content/message_handler.ts
Normal file
38
apps/browser/src/content/message_handler.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (event.data.command && event.data.command === "authResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
code: event.data.code,
|
||||
state: event.data.state,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.command && event.data.command === "webAuthnResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
data: event.data.data,
|
||||
remember: event.data.remember,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const forwardCommands = [
|
||||
"promptForLogin",
|
||||
"addToLockedVaultPendingNotifications",
|
||||
"unlockCompleted",
|
||||
"addedCipher",
|
||||
];
|
||||
|
||||
chrome.runtime.onMessage.addListener((event) => {
|
||||
if (forwardCommands.includes(event.command)) {
|
||||
chrome.runtime.sendMessage(event);
|
||||
}
|
||||
});
|
||||
603
apps/browser/src/content/notificationBar.ts
Normal file
603
apps/browser/src/content/notificationBar.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
import AddLoginRuntimeMessage from "src/background/models/addLoginRuntimeMessage";
|
||||
import ChangePasswordRuntimeMessage from "src/background/models/changePasswordRuntimeMessage";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
if (window.location.hostname.endsWith("vault.bitwarden.com")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageDetails: any[] = [];
|
||||
const formData: any[] = [];
|
||||
let barType: string = null;
|
||||
let pageHref: string = null;
|
||||
let observer: MutationObserver = null;
|
||||
const observeIgnoredElements = new Set([
|
||||
"a",
|
||||
"i",
|
||||
"b",
|
||||
"strong",
|
||||
"span",
|
||||
"code",
|
||||
"br",
|
||||
"img",
|
||||
"small",
|
||||
"em",
|
||||
"hr",
|
||||
]);
|
||||
let domObservationCollectTimeout: number = null;
|
||||
let collectIfNeededTimeout: number = null;
|
||||
let observeDomTimeout: number = null;
|
||||
const inIframe = isInIframe();
|
||||
const cancelButtonNames = new Set(["cancel", "close", "back"]);
|
||||
const logInButtonNames = new Set([
|
||||
"log in",
|
||||
"sign in",
|
||||
"login",
|
||||
"go",
|
||||
"submit",
|
||||
"continue",
|
||||
"next",
|
||||
]);
|
||||
const changePasswordButtonNames = new Set([
|
||||
"save password",
|
||||
"update password",
|
||||
"change password",
|
||||
"change",
|
||||
]);
|
||||
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
|
||||
let disabledAddLoginNotification = false;
|
||||
let disabledChangedPasswordNotification = false;
|
||||
|
||||
const activeUserIdKey = "activeUserId";
|
||||
let activeUserId: string;
|
||||
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
|
||||
if (obj == null || obj[activeUserIdKey] == null) {
|
||||
return;
|
||||
}
|
||||
activeUserId = obj[activeUserIdKey];
|
||||
});
|
||||
|
||||
chrome.storage.local.get(activeUserId, (obj: any) => {
|
||||
if (obj?.[activeUserId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domains = obj[activeUserId].settings.neverDomains;
|
||||
// eslint-disable-next-line
|
||||
if (domains != null && domains.hasOwnProperty(window.location.hostname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
disabledAddLoginNotification = obj[activeUserId].settings.disableAddLoginNotification;
|
||||
disabledChangedPasswordNotification =
|
||||
obj[activeUserId].settings.disableChangedPasswordNotification;
|
||||
|
||||
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
|
||||
collectIfNeededWithTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
processMessages(msg, sendResponse);
|
||||
});
|
||||
|
||||
function processMessages(msg: any, sendResponse: (response?: any) => void) {
|
||||
if (msg.command === "openNotificationBar") {
|
||||
if (inIframe) {
|
||||
return;
|
||||
}
|
||||
closeExistingAndOpenBar(msg.data.type, msg.data.typeData);
|
||||
sendResponse();
|
||||
return true;
|
||||
} else if (msg.command === "closeNotificationBar") {
|
||||
if (inIframe) {
|
||||
return;
|
||||
}
|
||||
closeBar(true);
|
||||
sendResponse();
|
||||
return true;
|
||||
} else if (msg.command === "adjustNotificationBar") {
|
||||
if (inIframe) {
|
||||
return;
|
||||
}
|
||||
adjustBar(msg.data);
|
||||
sendResponse();
|
||||
return true;
|
||||
} else if (msg.command === "notificationBarPageDetails") {
|
||||
pageDetails.push(msg.data.details);
|
||||
watchForms(msg.data.forms);
|
||||
sendResponse();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function observeDom() {
|
||||
const bodies = document.querySelectorAll("body");
|
||||
if (bodies && bodies.length > 0) {
|
||||
observer = new MutationObserver((mutations) => {
|
||||
if (mutations == null || mutations.length === 0 || pageHref !== window.location.href) {
|
||||
return;
|
||||
}
|
||||
|
||||
let doCollect = false;
|
||||
for (let i = 0; i < mutations.length; i++) {
|
||||
const mutation = mutations[i];
|
||||
if (mutation.addedNodes == null || mutation.addedNodes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < mutation.addedNodes.length; j++) {
|
||||
const addedNode: any = mutation.addedNodes[j];
|
||||
if (addedNode == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagName = addedNode.tagName != null ? addedNode.tagName.toLowerCase() : null;
|
||||
if (
|
||||
tagName != null &&
|
||||
tagName === "form" &&
|
||||
(addedNode.dataset == null || !addedNode.dataset.bitwardenWatching)
|
||||
) {
|
||||
doCollect = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
(tagName != null && observeIgnoredElements.has(tagName)) ||
|
||||
addedNode.querySelectorAll == null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const forms = addedNode.querySelectorAll("form:not([data-bitwarden-watching])");
|
||||
if (forms != null && forms.length > 0) {
|
||||
doCollect = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (doCollect) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (doCollect) {
|
||||
if (domObservationCollectTimeout != null) {
|
||||
window.clearTimeout(domObservationCollectTimeout);
|
||||
}
|
||||
|
||||
domObservationCollectTimeout = window.setTimeout(collect, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(bodies[0], { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
function collectIfNeededWithTimeout() {
|
||||
if (collectIfNeededTimeout != null) {
|
||||
window.clearTimeout(collectIfNeededTimeout);
|
||||
}
|
||||
collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000);
|
||||
}
|
||||
|
||||
function collectIfNeeded() {
|
||||
if (pageHref !== window.location.href) {
|
||||
pageHref = window.location.href;
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
|
||||
collect();
|
||||
|
||||
if (observeDomTimeout != null) {
|
||||
window.clearTimeout(observeDomTimeout);
|
||||
}
|
||||
observeDomTimeout = window.setTimeout(observeDom, 1000);
|
||||
}
|
||||
|
||||
if (collectIfNeededTimeout != null) {
|
||||
window.clearTimeout(collectIfNeededTimeout);
|
||||
}
|
||||
collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000);
|
||||
}
|
||||
|
||||
function collect() {
|
||||
sendPlatformMessage({
|
||||
command: "bgCollectPageDetails",
|
||||
sender: "notificationBar",
|
||||
});
|
||||
}
|
||||
|
||||
function watchForms(forms: any[]) {
|
||||
if (forms == null || forms.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
forms.forEach((f: any) => {
|
||||
const formId: string = f.form != null ? f.form.htmlID : null;
|
||||
let formEl: HTMLFormElement = null;
|
||||
if (formId != null && formId !== "") {
|
||||
formEl = document.getElementById(formId) as HTMLFormElement;
|
||||
}
|
||||
|
||||
if (formEl == null) {
|
||||
const index = parseInt(f.form.opid.split("__")[2], null);
|
||||
formEl = document.getElementsByTagName("form")[index];
|
||||
}
|
||||
|
||||
if (formEl != null && formEl.dataset.bitwardenWatching !== "1") {
|
||||
const formDataObj: any = {
|
||||
data: f,
|
||||
formEl: formEl,
|
||||
usernameEl: null,
|
||||
passwordEl: null,
|
||||
passwordEls: null,
|
||||
};
|
||||
locateFields(formDataObj);
|
||||
formData.push(formDataObj);
|
||||
listen(formEl);
|
||||
formEl.dataset.bitwardenWatching = "1";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function listen(form: HTMLFormElement) {
|
||||
form.removeEventListener("submit", formSubmitted, false);
|
||||
form.addEventListener("submit", formSubmitted, false);
|
||||
const submitButton = getSubmitButton(form, logInButtonNames);
|
||||
if (submitButton != null) {
|
||||
submitButton.removeEventListener("click", formSubmitted, false);
|
||||
submitButton.addEventListener("click", formSubmitted, false);
|
||||
}
|
||||
}
|
||||
|
||||
function locateFields(formDataObj: any) {
|
||||
const inputs = Array.from(document.getElementsByTagName("input"));
|
||||
formDataObj.usernameEl = locateField(formDataObj.formEl, formDataObj.data.username, inputs);
|
||||
if (formDataObj.usernameEl != null && formDataObj.data.password != null) {
|
||||
formDataObj.passwordEl = locatePassword(
|
||||
formDataObj.formEl,
|
||||
formDataObj.data.password,
|
||||
inputs,
|
||||
true
|
||||
);
|
||||
} else if (formDataObj.data.passwords != null) {
|
||||
formDataObj.passwordEls = [];
|
||||
formDataObj.data.passwords.forEach((pData: any) => {
|
||||
const el = locatePassword(formDataObj.formEl, pData, inputs, false);
|
||||
if (el != null) {
|
||||
formDataObj.passwordEls.push(el);
|
||||
}
|
||||
});
|
||||
if (formDataObj.passwordEls.length === 0) {
|
||||
formDataObj.passwordEls = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function locatePassword(
|
||||
form: HTMLFormElement,
|
||||
passwordData: any,
|
||||
inputs: HTMLInputElement[],
|
||||
doLastFallback: boolean
|
||||
) {
|
||||
let el = locateField(form, passwordData, inputs);
|
||||
if (el != null && el.type !== "password") {
|
||||
el = null;
|
||||
}
|
||||
if (doLastFallback && el == null) {
|
||||
el = form.querySelector('input[type="password"]');
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function locateField(form: HTMLFormElement, fieldData: any, inputs: HTMLInputElement[]) {
|
||||
if (fieldData == null) {
|
||||
return;
|
||||
}
|
||||
let el: HTMLInputElement = null;
|
||||
if (fieldData.htmlID != null && fieldData.htmlID !== "") {
|
||||
try {
|
||||
el = form.querySelector("#" + fieldData.htmlID);
|
||||
} catch {
|
||||
// Ignore error, we perform fallbacks below.
|
||||
}
|
||||
}
|
||||
if (el == null && fieldData.htmlName != null && fieldData.htmlName !== "") {
|
||||
el = form.querySelector('input[name="' + fieldData.htmlName + '"]');
|
||||
}
|
||||
if (el == null && fieldData.elementNumber != null) {
|
||||
el = inputs[fieldData.elementNumber];
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function formSubmitted(e: Event) {
|
||||
let form: HTMLFormElement = null;
|
||||
if (e.type === "click") {
|
||||
form = (e.target as HTMLElement).closest("form");
|
||||
if (form == null) {
|
||||
const parentModal = (e.target as HTMLElement).closest("div.modal");
|
||||
if (parentModal != null) {
|
||||
const modalForms = parentModal.querySelectorAll("form");
|
||||
if (modalForms.length === 1) {
|
||||
form = modalForms[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
form = e.target as HTMLFormElement;
|
||||
}
|
||||
|
||||
if (form == null || form.dataset.bitwardenProcessed === "1") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < formData.length; i++) {
|
||||
if (formData[i].formEl !== form) {
|
||||
continue;
|
||||
}
|
||||
const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification;
|
||||
if (!disabledBoth && formData[i].usernameEl != null && formData[i].passwordEl != null) {
|
||||
const login: AddLoginRuntimeMessage = {
|
||||
username: formData[i].usernameEl.value,
|
||||
password: formData[i].passwordEl.value,
|
||||
url: document.URL,
|
||||
};
|
||||
|
||||
if (
|
||||
login.username != null &&
|
||||
login.username !== "" &&
|
||||
login.password != null &&
|
||||
login.password !== ""
|
||||
) {
|
||||
processedForm(form);
|
||||
sendPlatformMessage({
|
||||
command: "bgAddLogin",
|
||||
login: login,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!disabledChangedPasswordNotification && formData[i].passwordEls != null) {
|
||||
const passwords: string[] = formData[i].passwordEls
|
||||
.filter((el: HTMLInputElement) => el.value != null && el.value !== "")
|
||||
.map((el: HTMLInputElement) => el.value);
|
||||
|
||||
let curPass: string = null;
|
||||
let newPass: string = null;
|
||||
let newPassOnly = false;
|
||||
if (formData[i].passwordEls.length === 3 && passwords.length === 3) {
|
||||
newPass = passwords[1];
|
||||
if (passwords[0] !== newPass && newPass === passwords[2]) {
|
||||
curPass = passwords[0];
|
||||
} else if (newPass !== passwords[2] && passwords[0] === newPass) {
|
||||
curPass = passwords[2];
|
||||
}
|
||||
} else if (formData[i].passwordEls.length === 2 && passwords.length === 2) {
|
||||
if (passwords[0] === passwords[1]) {
|
||||
newPassOnly = true;
|
||||
newPass = passwords[0];
|
||||
curPass = null;
|
||||
} else {
|
||||
const buttonText = getButtonText(getSubmitButton(form, changePasswordButtonNames));
|
||||
const matches = Array.from(changePasswordButtonContainsNames).filter(
|
||||
(n) => buttonText.indexOf(n) > -1
|
||||
);
|
||||
if (matches.length > 0) {
|
||||
curPass = passwords[0];
|
||||
newPass = passwords[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((newPass != null && curPass != null) || (newPassOnly && newPass != null)) {
|
||||
processedForm(form);
|
||||
|
||||
const changePasswordRuntimeMessage: ChangePasswordRuntimeMessage = {
|
||||
newPassword: newPass,
|
||||
currentPassword: curPass,
|
||||
url: document.URL,
|
||||
};
|
||||
sendPlatformMessage({
|
||||
command: "bgChangedPassword",
|
||||
data: changePasswordRuntimeMessage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSubmitButton(wrappingEl: HTMLElement, buttonNames: Set<string>) {
|
||||
if (wrappingEl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wrappingElIsForm = wrappingEl.tagName.toLowerCase() === "form";
|
||||
|
||||
let submitButton = wrappingEl.querySelector(
|
||||
'input[type="submit"], input[type="image"], ' + 'button[type="submit"]'
|
||||
) as HTMLElement;
|
||||
if (submitButton == null && wrappingElIsForm) {
|
||||
submitButton = wrappingEl.querySelector("button:not([type])");
|
||||
if (submitButton != null) {
|
||||
const buttonText = getButtonText(submitButton);
|
||||
if (buttonText != null && cancelButtonNames.has(buttonText.trim().toLowerCase())) {
|
||||
submitButton = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (submitButton == null) {
|
||||
const possibleSubmitButtons = Array.from(
|
||||
wrappingEl.querySelectorAll(
|
||||
'a, span, button[type="button"], ' + 'input[type="button"], button:not([type])'
|
||||
)
|
||||
) as HTMLElement[];
|
||||
let typelessButton: HTMLElement = null;
|
||||
possibleSubmitButtons.forEach((button) => {
|
||||
if (submitButton != null || button == null || button.tagName == null) {
|
||||
return;
|
||||
}
|
||||
const buttonText = getButtonText(button);
|
||||
if (buttonText != null) {
|
||||
if (
|
||||
typelessButton != null &&
|
||||
button.tagName.toLowerCase() === "button" &&
|
||||
button.getAttribute("type") == null &&
|
||||
!cancelButtonNames.has(buttonText.trim().toLowerCase())
|
||||
) {
|
||||
typelessButton = button;
|
||||
} else if (buttonNames.has(buttonText.trim().toLowerCase())) {
|
||||
submitButton = button;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (submitButton == null && typelessButton != null) {
|
||||
submitButton = typelessButton;
|
||||
}
|
||||
}
|
||||
if (submitButton == null && wrappingElIsForm) {
|
||||
// Maybe it's in a modal?
|
||||
const parentModal = wrappingEl.closest("div.modal") as HTMLElement;
|
||||
if (parentModal != null) {
|
||||
const modalForms = parentModal.querySelectorAll("form");
|
||||
if (modalForms.length === 1) {
|
||||
submitButton = getSubmitButton(parentModal, buttonNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
return submitButton;
|
||||
}
|
||||
|
||||
function getButtonText(button: HTMLElement) {
|
||||
let buttonText: string = null;
|
||||
if (button.tagName.toLowerCase() === "input") {
|
||||
buttonText = (button as HTMLInputElement).value;
|
||||
} else {
|
||||
buttonText = button.innerText;
|
||||
}
|
||||
return buttonText;
|
||||
}
|
||||
|
||||
function processedForm(form: HTMLFormElement) {
|
||||
form.dataset.bitwardenProcessed = "1";
|
||||
window.setTimeout(() => {
|
||||
form.dataset.bitwardenProcessed = "0";
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function closeExistingAndOpenBar(type: string, typeData: any) {
|
||||
let barPage = "notification/bar.html";
|
||||
switch (type) {
|
||||
case "add":
|
||||
barPage = barPage + "?add=1&isVaultLocked=" + typeData.isVaultLocked;
|
||||
break;
|
||||
case "change":
|
||||
barPage = barPage + "?change=1&isVaultLocked=" + typeData.isVaultLocked;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const frame = document.getElementById("bit-notification-bar-iframe") as HTMLIFrameElement;
|
||||
if (frame != null && frame.src.indexOf(barPage) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeBar(false);
|
||||
openBar(type, barPage);
|
||||
}
|
||||
|
||||
function openBar(type: string, barPage: string) {
|
||||
barType = type;
|
||||
|
||||
if (document.body == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const barPageUrl: string = chrome.extension.getURL(barPage);
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;";
|
||||
iframe.id = "bit-notification-bar-iframe";
|
||||
iframe.src = barPageUrl;
|
||||
|
||||
const frameDiv = document.createElement("div");
|
||||
frameDiv.setAttribute("aria-live", "polite");
|
||||
frameDiv.id = "bit-notification-bar";
|
||||
frameDiv.style.cssText =
|
||||
"height: 42px; width: 100%; top: 0; left: 0; padding: 0; position: fixed; " +
|
||||
"z-index: 2147483647; visibility: visible;";
|
||||
frameDiv.appendChild(iframe);
|
||||
document.body.appendChild(frameDiv);
|
||||
|
||||
(iframe.contentWindow.location as any) = barPageUrl;
|
||||
|
||||
const spacer = document.createElement("div");
|
||||
spacer.id = "bit-notification-bar-spacer";
|
||||
spacer.style.cssText = "height: 42px;";
|
||||
document.body.insertBefore(spacer, document.body.firstChild);
|
||||
}
|
||||
|
||||
function closeBar(explicitClose: boolean) {
|
||||
const barEl = document.getElementById("bit-notification-bar");
|
||||
if (barEl != null) {
|
||||
barEl.parentElement.removeChild(barEl);
|
||||
}
|
||||
|
||||
const spacerEl = document.getElementById("bit-notification-bar-spacer");
|
||||
if (spacerEl) {
|
||||
spacerEl.parentElement.removeChild(spacerEl);
|
||||
}
|
||||
|
||||
if (!explicitClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (barType) {
|
||||
case "add":
|
||||
sendPlatformMessage({
|
||||
command: "bgAddClose",
|
||||
});
|
||||
break;
|
||||
case "change":
|
||||
sendPlatformMessage({
|
||||
command: "bgChangeClose",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function adjustBar(data: any) {
|
||||
if (data != null && data.height !== 42) {
|
||||
const newHeight = data.height + "px";
|
||||
doHeightAdjustment("bit-notification-bar-iframe", newHeight);
|
||||
doHeightAdjustment("bit-notification-bar", newHeight);
|
||||
doHeightAdjustment("bit-notification-bar-spacer", newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function doHeightAdjustment(elId: string, heightStyle: string) {
|
||||
const el = document.getElementById(elId);
|
||||
if (el != null) {
|
||||
el.style.height = heightStyle;
|
||||
}
|
||||
}
|
||||
|
||||
function sendPlatformMessage(msg: any) {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
});
|
||||
52
apps/browser/src/content/shortcuts.ts
Normal file
52
apps/browser/src/content/shortcuts.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as Mousetrap from "mousetrap";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
const isSafari =
|
||||
typeof safari !== "undefined" &&
|
||||
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
||||
navigator.userAgent.indexOf("Chrome") === -1;
|
||||
const isVivaldi = !isSafari && navigator.userAgent.indexOf(" Vivaldi/") !== -1;
|
||||
|
||||
if (!isSafari && !isVivaldi) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSafari && (window as any).__bitwardenFrameId == null) {
|
||||
(window as any).__bitwardenFrameId = Math.floor(Math.random() * Math.floor(99999999));
|
||||
}
|
||||
|
||||
Mousetrap.prototype.stopCallback = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
let autofillCommand = ["mod+shift+l"];
|
||||
if (isSafari) {
|
||||
autofillCommand = ["mod+\\", "mod+8", "mod+shift+p"];
|
||||
}
|
||||
Mousetrap.bind(autofillCommand, () => {
|
||||
sendMessage("autofill_login");
|
||||
});
|
||||
|
||||
if (isSafari) {
|
||||
Mousetrap.bind("mod+shift+y", () => {
|
||||
sendMessage("open_popup");
|
||||
});
|
||||
|
||||
Mousetrap.bind("mod+shift+s", () => {
|
||||
sendMessage("lock_vault");
|
||||
});
|
||||
} else {
|
||||
Mousetrap.bind("mod+shift+9", () => {
|
||||
sendMessage("generate_password");
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(shortcut: string) {
|
||||
const msg: any = {
|
||||
command: "keyboardShortcutTriggered",
|
||||
shortcut: shortcut,
|
||||
};
|
||||
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
});
|
||||
4
apps/browser/src/globals.d.ts
vendored
Normal file
4
apps/browser/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare function escape(s: string): string;
|
||||
declare function unescape(s: string): string;
|
||||
declare let opr: any;
|
||||
declare let safari: any;
|
||||
BIN
apps/browser/src/images/close.png
Normal file
BIN
apps/browser/src/images/close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 288 B |
BIN
apps/browser/src/images/icon128.png
Normal file
BIN
apps/browser/src/images/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/browser/src/images/icon128_gray.png
Normal file
BIN
apps/browser/src/images/icon128_gray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
apps/browser/src/images/icon16.png
Normal file
BIN
apps/browser/src/images/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user