Compare commits

..
153 Commits
Author SHA1 Message Date
GitHub Action ddd73ad740 v1.18.7 2021-07-18 14:53:18 +00:00
GitHub Action 0ceb5875fa v1.18.6 2021-05-19 09:09:09 +00:00
GitHub Action e5bd2dcf85 v1.18.5 2021-05-14 19:38:21 +00:00
GitHub Action e7625fbe23 v1.18.4 2021-05-12 20:16:27 +00:00
GitHub Action 878afe6af7 v1.18.3 2021-05-09 13:08:14 +00:00
GitHub Action 26aad741ff v1.18.2 2021-05-09 06:59:16 +00:00
GitHub Action 0cdf3ab847 v1.18.1 2021-05-08 19:35:26 +00:00
GitHub Action 7f95d2d4a4 v1.18.0 2021-05-08 18:32:39 +00:00
GitHub Action 9990002744 v1.17.6 2021-04-09 17:47:17 +00:00
GitHub Action 9d3ae2531b v1.17.5 2021-03-27 09:30:11 +00:00
GitHub Action f503671bf0 v1.17.4 2021-03-18 16:46:59 +00:00
GitHub Action cc99f830fa v1.17.3 2021-03-14 20:03:39 +00:00
GitHub Action 7aaff0464d v1.17.2 2021-03-13 17:28:31 +00:00
GitHub Action 8cbf042b08 v1.17.1 2021-03-10 18:58:02 +00:00
GitHub Action a3df687c17 v1.17.0 2021-03-07 14:15:31 +00:00
GitHub Action a5e631d7d0 v1.16.8 2021-03-02 19:12:11 +00:00
GitHub Action 34b7c9c6fc v1.16.7 2020-12-31 16:59:32 +00:00
GitHub Action 304d32552d v1.16.6 2020-12-30 13:36:48 +00:00
GitHub Action b638b3ce46 v1.16.5 2020-12-18 20:24:56 +00:00
GitHub Action bf2e74f32b v1.16.4 2020-12-17 19:20:38 +00:00
GitHub Action 2629b59035 v1.16.3 2020-12-10 20:06:10 +00:00
GitHub Action 789422861c v1.16.2 2020-12-10 18:45:09 +00:00
GitHub Action f4ecc1780c v1.16.1 2020-12-04 15:39:12 +00:00
antelle 1e2a8e0294 cache maninfest 2020-11-29 19:58:01 +01:00
GitHub Action e2a4846bf6 v1.16.0 2020-11-29 14:51:02 +00:00
GitHub Action 037f45441d v1.15.7 2020-09-12 10:53:09 +00:00
GitHub Action a2829d12f0 v1.15.6 2020-08-08 13:26:52 +00:00
GitHub Action 31b1b63f03 v1.15.5 2020-06-13 18:05:38 +00:00
GitHub Action 803866f370 v1.15.4 2020-06-13 09:33:31 +00:00
GitHub Action fae73e4f98 v1.15.3 2020-06-11 18:40:58 +00:00
GitHub Action e781e5a184 v1.15.2 2020-06-09 19:48:29 +00:00
GitHub Action 1bcb920f38 v1.15.1 2020-06-07 09:38:30 +00:00
GitHub Action 9d3d3ab446 v1.15.0 2020-06-06 15:19:05 +00:00
GitHub Action 80a956a602 v1.14.3 2020-05-15 19:04:43 +00:00
GitHub Action b8cad5bad7 v1.14.2 2020-05-04 17:48:47 +00:00
GitHub Action 97e0916b8c v1.14.1 2020-05-02 12:05:26 +00:00
GitHub Action 63789c42ba v1.14.0 2020-04-18 07:01:21 +00:00
GitHub Action e2879ace1a v1.13.4 2020-04-15 16:16:11 +00:00
GitHub Action e6909f919d v1.13.3 2020-04-11 16:55:36 +00:00
GitHub Action 9dc3c65e78 v1.13.2 2020-04-09 05:46:39 +00:00
antelle 3727fdcfc7 v1.13.1 2020-04-04 22:36:23 +02:00
antelle 8dcd0a7fbf removed a test file 2020-04-04 18:23:27 +02:00
antelle c397832062 deployment 2020-04-04 18:21:22 +02:00
antelle aa240e5915 testing deployments 2020-04-04 18:18:14 +02:00
antelle 410d6aa9c5 removed CNAME 2020-04-03 21:55:26 +02:00
antelle 0d288881f9 v1.13.0 2020-04-03 21:37:31 +02:00
antelle 241ea7ade8 404 2020-01-18 09:06:54 +01:00
antelle 2ff97d9f3e v1.12.3 2019-11-06 20:06:47 +01:00
antelle c1b1796d6c v1.12.2 2019-11-03 12:40:14 +01:00
antelle 863ac3859d v1.12.1 2019-10-27 10:45:50 +01:00
antelle f50337dc14 v1.12.0 2019-10-26 21:13:38 +02:00
antelle 15a9db8704 v1.11.10 2019-10-16 22:44:03 +02:00
antelle ecadb9ffca v1.11.9 2019-10-13 23:21:30 +02:00
antelle 5d11e29cd0 v1.11.8 2019-10-11 23:36:36 +02:00
antelle 6dd5d97970 Revert "v1.11.8"
This reverts commit 72b6aed3bc.
2019-10-11 21:50:43 +02:00
antelle 72b6aed3bc v1.11.8 2019-10-11 21:34:40 +02:00
Antelle 943446bd39 v1.11.7 2019-10-09 23:15:15 +02:00
antelle d2e9de710f v1.11.6 2019-10-04 21:22:31 +02:00
antelle 622a1ababe v1.11.5 2019-09-29 23:32:37 +02:00
antelle fa049c840f v1.11.4 2019-09-29 15:50:01 +02:00
antelle c703afd074 v1.11.3 2019-09-29 12:09:07 +02:00
antelle 6f3ef907e3 v1.11.2 2019-09-29 10:08:22 +02:00
antelle 043693d4a4 v1.11.1 2019-09-28 22:50:53 +02:00
antelle 2321d42e67 v1.11.0 2019-09-28 20:36:26 +02:00
antelle a451b4ec78 v1.10.1 2019-09-20 05:52:21 +02:00
antelle 86f9af9576 v1.10.0 2019-09-09 17:21:36 +02:00
antelle 0654349021 v1.9.3 2019-09-07 19:42:02 +02:00
antelle ef89003f04 v1.9.2 2019-08-22 22:33:31 +02:00
antelle a459a4f20d v1.9.1 2019-08-19 21:14:22 +02:00
antelle dea266ef4d v1.9.0 2019-08-18 11:32:43 +02:00
antelle 34e954ee23 v1.8.2 2019-04-22 11:38:28 +02:00
antelle fb7c8da4b0 v1.8.1 2019-04-05 20:37:54 +02:00
antelle c2ae28e450 v1.8.0 2019-03-31 16:05:11 +02:00
antelle 376a1b3865 v1.7.8 2019-03-02 17:31:30 +01:00
antelle 36436de1cf v1.7.7 2019-02-09 21:35:26 +01:00
antelle 6888df45ba v1.7.6 2019-02-07 22:14:47 +01:00
antelle 80f65aeb0e v1.7.5 2019-02-04 19:52:27 +01:00
antelle 01dd0295c2 v1.7.4 2019-01-17 20:44:10 +01:00
antelle a337b0a131 v1.7.3 2019-01-13 17:17:42 +01:00
antelle b5f5d54ff2 v1.7.2 2019-01-07 18:56:30 +01:00
antelle 17536ba0b6 v1.7.1 2019-01-06 18:53:01 +01:00
antelle a61600118f v1.7.0 2019-01-06 18:08:52 +01:00
antelle 1445ef273f removed google verification 2017-12-13 20:34:57 +01:00
antelle e62adb4ddf google verification 2017-12-13 20:34:02 +01:00
antelle ce9277ca95 v1.6.3 2017-12-11 19:50:17 +01:00
antelle 7c6935cf35 v1.6.2 2017-12-09 22:32:42 +01:00
antelle a86de235c9 v1.6.1 2017-12-03 01:14:39 +01:00
antelle cbb3e89646 v1.6.0 2017-12-02 23:04:03 +01:00
antelle ff30286472 v1.5.6 2017-08-31 19:22:56 +02:00
antelle 6edff27c5d v1.5.5 2017-08-30 19:37:01 +02:00
antelle 75812fef67 v1.5.4 2017-06-03 16:32:17 +02:00
antelle 96c8d07a57 v1.5.3 2017-05-29 20:57:53 +02:00
antelle e7e23bb319 v1.5.2 2017-05-25 08:58:14 +02:00
antelle a53a1e4c81 v1.5.1 2017-05-23 21:13:41 +02:00
antelle b0e4e96a61 v1.5.0 2017-05-20 20:34:27 +02:00
antelle 2d48166d30 v1.4.1 2017-03-26 11:35:16 +02:00
antelle d0fa63045e v1.4.0 2017-02-04 16:03:45 +01:00
antelle 4aa6bc1d95 v1.3.3 2016-09-20 23:54:43 +03:00
antelle 7467a16d9b v1.3.2 2016-09-13 23:16:30 +03:00
antelle 6f3fa8328d v1.3.1 2016-09-02 19:44:27 +03:00
antelle 86cd6e9468 v1.3.0 2016-09-01 21:20:35 +03:00
antelle dd4d71a713 v1.2.4 2016-07-20 20:12:52 +03:00
antelle 657fedaf26 v1.2.3 2016-07-17 15:36:49 +03:00
antelle f474b62966 v1.2.2 2016-07-14 22:02:01 +03:00
antelle efb7789806 v1.2.1 2016-07-12 21:00:15 +03:00
antelle 489a2f31f1 v1.2.0 2016-07-11 22:41:44 +03:00
antelle a3b76c410d v1.1.4 2016-04-21 21:30:04 +03:00
antelle 3811518fb9 v1.1.3 2016-04-21 01:03:39 +03:00
antelle 69d942edad v1.1.2 2016-04-10 23:16:48 +03:00
antelle 2ae7023e2d v1.1.1 2016-04-07 19:54:46 +03:00
antelle 6a153fa2f4 v1.1.0 2016-04-05 20:42:47 +03:00
Antelle 23d30de921 v1.0.4 2016-02-26 00:03:01 +03:00
Antelle 2bbc61c2f5 rebuild 2016-02-23 15:20:27 +03:00
Antelle ca33f4255f v1.0.3 2016-02-23 14:56:25 +03:00
Antelle 975f620698 v1.0.2 2016-02-18 00:03:47 +03:00
Antelle b827dbb557 v1.0.1 2016-02-14 19:37:59 +03:00
Antelle b621ee7ffb v1.0.0 2016-02-12 22:03:21 +03:00
Antelle 6fe581333d v0.6.1 2016-02-02 20:39:16 +03:00
Antelle 10d2455b9a cname 2016-02-02 20:17:18 +03:00
Antelle d79a6178b7 v0.6.0 2016-01-19 22:31:03 +03:00
Antelle 864061c839 v0.5.1 2015-12-15 20:59:30 +03:00
Antelle 6c37774795 v0.5.0 2015-12-14 21:20:26 +03:00
Antelle 9dd0cba100 v0.4.6 2015-11-25 22:01:49 +03:00
Antelle 44de83b497 v0.4.5 2015-11-25 01:05:52 +03:00
Antelle bb878a7004 push pages 2015-11-22 23:40:16 +03:00
Antelle 7a5a25241c v0.4.4 2015-11-22 23:22:42 +03:00
Antelle a9305839a9 v0.4.3 2015-11-22 21:23:26 +03:00
Antelle 73b7868ee4 v0.4.2 2015-11-22 15:05:52 +03:00
Antelle 4c1d451d24 v0.4.1 2015-11-22 14:44:21 +03:00
Antelle 0532ce2901 v0.4.0 2015-11-22 14:30:56 +03:00
Antelle 1fa31c449b v0.3.0 2015-11-14 21:25:23 +03:00
Antelle 329a4e76d2 v0.2.1 2015-11-10 20:10:49 +03:00
Antelle 39c06fa368 v0.2.0 2015-11-09 21:33:38 +03:00
Antelle 15c40708e1 v0.1.1 2015-11-04 09:31:48 +03:00
Antelle bef16e8114 features page is now inside a repo 2015-11-01 01:21:06 +03:00
Antelle 2d3db5583c Features page 2015-11-01 00:58:52 +03:00
Antelle a457d98fe4 v0.1.0 2015-10-31 22:29:40 +03:00
Antelle 9c99db6579 v0.0.6 2015-10-28 00:58:42 +03:00
Antelle 29ebef03ed v0.0.5 2015-10-27 23:00:33 +03:00
Antelle 04c6147ae7 v0.0.4 2015-10-25 23:05:19 +03:00
Antelle 83fe3d269b v0.0.3 2015-10-25 22:28:47 +03:00
Antelle b241813f95 v0.0.2 2015-10-24 22:10:36 +03:00
Antelle aaf304aa99 fix screenshot 2015-10-22 21:14:58 +03:00
Antelle 3d26658ecc new screenshots 2015-10-22 00:18:28 +03:00
Antelle e6da3c4281 v0.0.1 2015-10-22 00:05:19 +03:00
Antelle fbeba063cc manifest 2015-10-22 00:05:06 +03:00
Antelle 6805b0a6b5 generator 2015-10-20 23:00:16 +03:00
Antelle 1e75ac210d new dist 2015-10-18 18:47:08 +03:00
Antelle c51b3c6b6c new dist 2015-10-18 17:03:28 +03:00
Antelle c9e75b2097 screenshots 2015-10-18 12:14:14 +03:00
Antelle b8f74fe737 frameless screenshot 2015-10-18 01:22:19 +03:00
Antelle 0afc1ec8cf screenshot 2015-10-18 01:13:48 +03:00
Antelle be37b586e8 first build 2015-10-18 00:59:43 +03:00
515 changed files with 8 additions and 67233 deletions
-14
View File
@@ -1,14 +0,0 @@
{
"presets": [],
"plugins": [
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-object-rest-spread", { "loose": true,
"useBuiltIns": true
}],
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-logical-assignment-operators",
"@babel/plugin-external-helpers"
]
}
-19
View File
@@ -1,19 +0,0 @@
# http://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
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[{*.nsh,*.yml,*.yaml,*.json}]
indent_size = 2
-69
View File
@@ -1,69 +0,0 @@
{
"plugins": ["prettier", "import", "babel"],
"extends": ["standard", "eslint:recommended", "plugin:prettier/recommended"],
"rules": {
"semi": ["error", "always"],
"one-var": "off",
"space-before-function-paren": "off",
"no-throw-literal": "off",
"camelcase": ["error", { "properties": "always" }],
"no-console": "error",
"no-alert": "error",
"no-debugger": "error",
"prefer-arrow-callback": "error",
"object-property-newline": "off",
"no-useless-escape": "off",
"no-var": "error",
"prefer-const": "error",
"no-unused-expressions": "off",
"strict": ["error", "never"],
"no-mixed-operators": "off",
"prefer-promise-reject-errors": "off",
"standard/no-callback-literal": "off",
"object-curly-spacing": "off",
"quote-props": "off",
"no-new-object": "error",
"object-shorthand": "error",
"no-array-constructor": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-new-func": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-useless-constructor": "error",
"no-dupe-class-members": "error",
"no-duplicate-imports": "error",
"eqeqeq": "error",
"no-unneeded-ternary": "error",
"curly": "error",
"prettier/prettier": "error",
"no-empty": "off",
"no-restricted-syntax": [
"error",
{
"selector": "ExportDefaultDeclaration",
"message": "Prefer named exports"
}
],
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off"
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"parser": "babel-eslint",
"globals": {
"_": true,
"$": true
},
"env": {
"browser": true
}
}
-46
View File
@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at antelle.net@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
-12
View File
@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [keeweb, antelle]
patreon: # Replace with a single Patreon username
open_collective: keeweb
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # for example PayPal links
-34
View File
@@ -1,34 +0,0 @@
---
name: Bug report
about: Create a report to help us improve KeeWeb
title: ''
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
Please copy all version information from Settings/Help
**Kdbx File**
Does it happen on Demo or New database?
If you have a test db without your personal data, please attach it.
**Logs**
Please open dev tools in your browser and attach output log from Console tab. If you are using a desktop app, devtools can be opened from Settings/General/Advanced.
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: true
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
-15
View File
@@ -1,15 +0,0 @@
Thanks for taking the time to contribute! :gift:
Please follow these steps:
1. work on the right branch: it must be `develop` for new features and `master` for hotfixes
2. make sure the feature you add is not listed in [banned features](https://github.com/keeweb/keeweb/wiki/Unsupported-Features)
3. check compatibility with [the project roadmap](https://github.com/keeweb/keeweb/wiki/TODO) and key features: you should not break anything
4. if your feature is introducing a lot of new UI or changing any UX or design concept, please open an issue to discuss it first
5. please respect existing style in code, styles, and markup (hint: install and enable [editorconfig](http://editorconfig.org/) in your editor)
6. don't add any dependencies
7. test your code: it must work in all browsers, Windows, OSX, and Linux
8. if your code is platform-dependent, please add corresponding switches
9. respect each platform: don't create a behaviour that breaks interface guidelines or user expectations
10. run `grunt` and make sure it's passing
11. replace this message with the actual description
-11
View File
@@ -1,11 +0,0 @@
FROM snapcore/snapcraft:stable
COPY entrypoint.sh /entrypoint.sh
RUN apt-get update
RUN apt-get install -y build-essential git-core unzip curl pkg-config rpm
RUN curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash -
RUN apt-get install -y nodejs
RUN npm i -g grunt-cli
ENTRYPOINT ["/entrypoint.sh"]
-5
View File
@@ -1,5 +0,0 @@
name: 'Linux Build'
description: 'Build KeeWeb on Linux in Docker'
runs:
using: 'docker'
image: 'Dockerfile'
@@ -1,5 +0,0 @@
#!/bin/bash -e
cd /github/workspace
npm ci
grunt desktop-linux
@@ -1,404 +0,0 @@
#!/usr/bin/env python3
#
# git-restore-mtime - Change mtime of files based on commit date of last change
#
# Copyright (C) 2012 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. See <http://www.gnu.org/licenses/gpl.html>
#
"""
Change the modification time (mtime) of all files in work tree, based on the
date of the most recent commit that modified the file.
Useful prior to generating release tarballs, so each file is archived with a
date that is similar to the date when the file was actually last modified,
assuming the actual modification date and its commit date are close.
By default ignores all ignored and untracked files, and also refuses to work
on trees with uncommitted changes.
"""
# TODO:
# - Add -z on git whatchanged/ls-files so we don't deal with filename decode/OS normalization
# - When Python is bumped to 3.7, use text instead of universal_newlines on subprocess
# - Update "Statistics for some large projects" with modern hardware and repositories.
# - Create a README.md for git-restore-mtime alone. It deserves extensive documentation
# - Move Statistics there
# FIXME:
# - When current dir is outside the worktree, e.g. using --work-tree, `git ls-files`
# assume any relative pathspecs are to worktree root, not the current dir. As such,
# relative pathspecs may not work.
# - Renames and mode changes should not change file mtime:
# - Must check on status 'R100' and mode changes with same blobs
# - Should require status to be (A, C, M, R<100, T). D will never be processed as
# filelist is a subset of lsfileslist.
# - Check file (A, D) for directory mtime is not sufficient:
# - Renames also change dir mtime, unless rename was on a parent dir
# - If most recent change of all files in a dir was a [M]odification,
# dir might not be touched at all.
# - Dirs containing only subdirectories but no direct files will also
# not be touched. They're files' [grand]parent dir, but never their dirname().
# - Some solutions:
# - After files done, perform some dir processing for missing dirs, finding latest
# file (A, D, R)
# - Simple approach: dir mtime is most recent child (dir or file) mtime
# - Use a virtual concept of "created at most at" to fill missing info, bubble up
# to parents and grandparents
# - When handling [grand]parent dirs, stay inside <pathspec>
# - Better handling of merge commits. `-m` is plain *wrong*. `-c/--cc` is perfect, but
# painfully slow. First pass without merge commits is not accurate. Maybe add a new
# `--accurate` mode for `--cc`?
if __name__ != "__main__":
raise ImportError("{} should not be used as a module.".format(__name__))
import subprocess, shlex
import sys, os.path
import logging
import argparse
import time
# Update symlinks only if the OS supports not following them
UPDATE_SYMLINKS = bool(os.utime in getattr(os, 'supports_follow_symlinks', []))
STEPMISSING = 100
# Command-line interface ######################################################
def parse_args():
parser = argparse.ArgumentParser(
description="""Restore original modification time of files based on the date of the
most recent commit that modified them. Useful when generating release tarballs.""")
group = parser.add_mutually_exclusive_group()
group.add_argument('--quiet', '-q', dest='loglevel',
action="store_const", const=logging.WARNING, default=logging.INFO,
help="Suppress informative messages and summary statistics.")
group.add_argument('--verbose', '-v', action="count",
help="Print additional information for each processed file.")
parser.add_argument('--force', '-f', action="store_true",
help="Force execution on trees with uncommitted changes.")
parser.add_argument('--merge', '-m', action="store_true",
help="""Include merge commits. Leads to more recent mtimes and more files per
commit, thus with the same mtime (which may or may not be what you want). Including
merge commits may lead to less commits being evaluated (all files are found sooner),
which improves performance, sometimes substantially. But since merge commits are
usually huge, processing them may also take longer, sometimes substantially.
By default merge logs are only used for files missing from regular commit logs.""")
parser.add_argument('--first-parent', action="store_true",
help="""Consider only the first parent, the "main branch", when parsing merge
commit logs. Only effective when merge commits are included in the log, either
by --merge or to find missing files after first log parse. See --skip-missing.""")
parser.add_argument('--skip-missing', '-s',
action="store_false", default=True, dest="missing",
help="""Do not try to find missing files. If some files were not found in regular
commit logs, by default it re-tries using merge commit logs for these files (if
--merge was not already used). This option disables this behavior, which may slightly
improve performance, but files found only in merge commits will not be updated.""")
parser.add_argument('--no-directories', '-D',
action="store_false", default=True, dest='dirs',
help="""Do not update directory mtime for files created, renamed or deleted in it.
Note: just modifying a file will not update its directory mtime.""")
parser.add_argument('--test', '-t', action="store_true", default=False,
help="Test run: do not actually update any file")
parser.add_argument('--commit-time', '-c',
action='store_true', default=False, dest='commit_time',
help="Use commit time instead of author time")
parser.add_argument('pathspec', nargs='*', metavar='PATH',
help="""Only modify paths matching PATH, directories or files, relative to current
directory. Default is to modify all files handled by git, ignoring untracked files
and submodules.""")
parser.add_argument('--work-tree', dest='workdir',
help="Path to the work tree, if not current directory or one of its parents.")
parser.add_argument('--git-dir', dest='gitdir',
help="Path to the git repository, if not the default <work-tree-root>/.git")
parser.add_argument('--skip-older-than', metavar='SECONDS', type=int,
help="""Do not modify files that are older than %(metavar)s.
It can significantly improve performance if fewer files are processed.
Useful on CI builds, which can eventually switch workspace to different branch,
but mostly performs builds on the same one (e.g. master).
""")
return parser.parse_args()
# Helper functions ############################################################
def setup_logging(args):
TRACE = logging.DEBUG // 2
logging.Logger.trace = lambda _, m, *a, **k: _.log(TRACE, m, *a, **k)
level = (args.verbose and max(TRACE, logging.DEBUG // args.verbose)) or args.loglevel
logging.basicConfig(level=level, format='%(message)s')
return logging.getLogger()
def normalize(path):
"""Normalize paths from git, handling non-ASCII characters.
Git for Windows, as of v1.7.10, stores paths as UTF-8 normalization form C. If path
contains non-ASCII or non-printable chars it outputs the UTF-8 in octal-escaped
notation, double-quoting the whole path. Double-quotes and backslashes are also escaped.
https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
https://github.com/msysgit/msysgit/wiki/Git-for-Windows-Unicode-Support
https://github.com/git/git/blob/master/Documentation/i18n.txt
Example on git output, this function reverts this:
r'back\slash_double"quote_açaí' -> r'"back\\slash_double\"quote_a\303\247a\303\255"'
"""
if path and path[0] == '"':
# Python 2: path = path[1:-1].decode("string-escape")
# Python 3: https://stackoverflow.com/a/46650050/624066
path = (path[1:-1] # Remove enclosing double quotes
.encode('latin1') # Convert to bytes, required 'unicode-escape'
.decode('unicode-escape') # Perform the actual octal-escaping decode
.encode('latin1') # 1:1 mapping to bytes, forming UTF-8 encoding
.decode('utf8')) # Decode from UTF-8
# Make sure the slash matches the OS; for Windows we need a backslash
return os.path.normpath(path)
if UPDATE_SYMLINKS:
def touch(path, mtime, test=False):
"""The actual mtime update"""
if test: return
os.utime(path, (mtime, mtime), follow_symlinks=False)
else:
def touch(path, mtime, test=False):
"""The actual mtime update"""
if test: return
os.utime(path, (mtime, mtime))
def isodate(secs):
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(secs))
# Git class and parselog(), the heart of the script ###########################
class Git():
def __init__(self, workdir=None, gitdir=None):
self.gitcmd = ['git']
if workdir: self.gitcmd.extend(('--work-tree', workdir))
if gitdir : self.gitcmd.extend(('--git-dir', gitdir))
self.workdir, self.gitdir = self._repodirs()
def ls_files(self, pathlist=None):
return (normalize(_) for _ in self._run('ls-files --full-name', pathlist))
def is_dirty(self):
return bool(self._run('diff --no-ext-diff --quiet', output=False))
def log(self, merge=False, first_parent=False, commit_time=False, pathlist=None):
cmd = 'whatchanged --pretty={}'.format('%ct' if commit_time else '%at')
if merge: cmd += ' -m'
if first_parent: cmd += ' --first-parent'
return self._run(cmd, pathlist)
def _repodirs(self):
return (os.path.normpath(_) for _ in
self._run('rev-parse --show-toplevel --absolute-git-dir', check=True))
def _run(self, cmdstr, pathlist=None, output=True, check=False):
cmdlist = self.gitcmd + shlex.split(cmdstr)
if pathlist:
cmdlist.append('--')
cmdlist.extend(pathlist)
log.trace("Executing: %s", ' '.join(cmdlist))
if not output:
return subprocess.call(cmdlist)
if check:
try:
stdout = subprocess.check_output(cmdlist, universal_newlines=True)
return stdout.splitlines()
except subprocess.CalledProcessError as e:
raise self.Error(e.returncode, e.cmd, e.output, e.stderr)
self.proc = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, universal_newlines=True)
return (_.strip() for _ in self.proc.stdout)
class Error(subprocess.CalledProcessError): pass
def parselog(filelist, dirlist, stats, git, merge=False, filterlist=None):
mtime = 0
for line in git.log(merge, args.first_parent, args.commit_time, filterlist):
stats['loglines'] += 1
# Blank line between Date and list of files
if not line: continue
# File line
if line[0] == ':': # Faster than line.startswith(':')
# If line describes a rename, linetok has three tokens, otherwise two
linetok = line.split('\t')
status = linetok[0]
file = linetok[-1]
# Handles non-ASCII chars and OS path separator
file = normalize(file)
if file in filelist:
stats['files'] -= 1
log.debug("%d\t%d\t%d\t%s\t%s",
stats['loglines'], stats['commits'], stats['files'],
isodate(mtime), file)
filelist.remove(file)
try:
touch(os.path.join(git.workdir, file), mtime, args.test)
stats['touches'] += 1
except Exception as e:
log.error("ERROR: %s", e)
stats['errors'] += 1
if args.dirs:
dirname = os.path.dirname(file)
if status[-1] in ('A', 'D') and dirname in dirlist:
log.debug("%d\t%d\t-\t%s\t%s",
stats['loglines'], stats['commits'],
isodate(mtime), "{}/".format(dirname or '.'))
dirlist.remove(dirname)
try:
touch(os.path.join(git.workdir, dirname), mtime, args.test)
stats['dirtouches'] += 1
except Exception as e:
log.error("ERROR: %s", e)
stats['direrrors'] += 1
# Date line
else:
stats['commits'] += 1
mtime = int(line)
# All files done?
if not stats['files']:
git.proc.terminate() # hackish, but does the job. Not needed anyway
return
# Main Logic ##################################################################
def main():
start = time.time() # yes, Wall time. CPU time is not realistic for users.
stats = {_: 0 for _ in ('loglines', 'commits', 'touches', 'errors', 'dirtouches', 'direrrors')}
# First things first: Where and Who are we?
try:
git = Git(args.workdir, args.gitdir)
except Git.Error as e:
# Not in a git repository, and git already informed user on stderr. So we just...
return e.returncode
# Do not work on dirty repositories, unless --force
if not args.force and git.is_dirty():
log.critical(
"ERROR: There are local changes in the working directory.\n"
"This could lead to undesirable results for modified files.\n"
"Please, commit your changes (or use --force) and try again.\n"
"Aborting")
return 1
# Get the files managed by git and build file and dir list to be processed
filelist = set()
dirlist = set()
if UPDATE_SYMLINKS and not args.skip_older_than:
filelist = set(git.ls_files(args.pathspec))
dirlist = set(os.path.dirname(_) for _ in filelist)
else:
for path in git.ls_files(args.pathspec):
fullpath = os.path.join(git.workdir, path)
# Symlink (to file, to dir or broken - git handles the same way)
if not UPDATE_SYMLINKS and os.path.islink(fullpath):
log.warning("WARNING: Skipping symlink, OS does not support update: %s", path)
continue
# skip files which are older than given threshold
if args.skip_older_than and start - os.path.getmtime(fullpath) > args.skip_older_than:
continue
# Always add them relative to worktree root
filelist.add(path)
dirlist.add(os.path.dirname(path))
stats['totalfiles'] = stats['files'] = len(filelist)
log.info("{0:,} files to be processed in work dir".format(stats['totalfiles']))
if not filelist:
# Nothing to do. Exit silently and without errors, just like git does
return
# Process the log until all files are 'touched'
log.debug("Line #\tLog #\tF.Left\tModification Time\tFile Name")
parselog(filelist, dirlist, stats, git, args.merge, args.pathspec)
# Missing files
if filelist:
# Try to find them in merge logs, if not done already
# (usually HUGE, thus MUCH slower!)
if args.missing and not args.merge:
filterlist = list(filelist)
for i in range(0, len(filterlist), STEPMISSING):
parselog(filelist, dirlist, stats, git,
merge=True, filterlist=filterlist[i:i+STEPMISSING])
# Still missing some?
for file in filelist:
log.warning("WARNING: not found in log: %s", file)
# Final statistics
# Suggestion: use git-log --before=mtime to brag about skipped log entries
log.info(
"Statistics:\n"
"{:13,.2f} seconds\n"
"{:13,} log lines processed\n"
"{:13,} commits evaluated"
"".format(time.time()-start, stats['loglines'], stats['commits']))
if args.dirs:
if stats['direrrors']: log.info("{:13,} directory update errors".format(stats['direrrors']))
log.info("{:13,} directories updated".format(stats['dirtouches']))
if stats['touches'] != stats['totalfiles']: log.info("{:13,} files".format(stats['totalfiles']))
if stats['files']: log.info("{:13,} files missing".format(stats['files']))
if stats['errors']: log.info("{:13,} file update errors".format(stats['errors']))
log.info("{:13,} files updated".format(stats['touches']))
if args.test:
log.info("TEST RUN - No files modified!")
args = parse_args()
log = setup_logging(args)
log.trace("Arguments: %s", args)
# UI done, it's show time!
try:
sys.exit(main())
except KeyboardInterrupt:
log.info("Aborting")
sys.exit(-1)
-426
View File
@@ -1,426 +0,0 @@
name: Build
on:
push:
tags: [ '*' ]
jobs:
web:
runs-on: ubuntu-latest
steps:
- name: Get current git tag
id: get_tag
uses: keeweb/get-git-tag@v3.0.2
with:
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
tagRegexGroup: 1
- name: Checkout
uses: actions/checkout@v2
with:
repository: keeweb/keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Test
run: npm test
- name: Grunt
run: grunt
- name: Upload artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Write secrets
env:
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
run: |
mkdir keys
echo "$VIRUS_TOTAL" > keys/virus-total.json
- name: Check on VirusTotal
run: grunt virustotal
if: ${{ github.repository == 'keeweb/keeweb' }}
linux:
runs-on: ubuntu-latest
needs:
- web
steps:
- name: Get current git tag
id: get_tag
uses: keeweb/get-git-tag@v3.0.2
with:
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
tagRegexGroup: 1
- name: Checkout
uses: actions/checkout@v2
with:
repository: keeweb/keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Download artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Write secrets
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
run: |
mkdir keys
echo "$PRIVATE_KEY" > keys/private-key.pem
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
- name: Build in Docker
uses: ./.github/actions/linux-build
- name: Upload AppImage artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
- name: Upload snap artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
- name: Upload deb artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
- name: Upload zip artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
- name: Upload rpm artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
darwin:
runs-on: macos-latest
needs:
- web
steps:
- name: Get current git tag
id: get_tag
uses: keeweb/get-git-tag@v3.0.2
with:
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
tagRegexGroup: 1
- name: Checkout
uses: actions/checkout@v2
with:
repository: keeweb/keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Download artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
run: sudo npm i -g grunt-cli
- name: Write secrets
env:
CODESIGN: ${{ secrets.CODESIGN }}
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
run: |
mkdir keys
echo "$CODESIGN" > keys/codesign.json
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
- name: Import certificates
uses: keeweb/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Grunt
run: grunt --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Upload x64 dmg artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
- name: Upload arm64 dmg artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
win32:
runs-on: windows-latest
needs:
- web
steps:
- name: Get current git tag
id: get_tag
uses: keeweb/get-git-tag@v3.0.2
with:
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
tagRegexGroup: 1
- name: Checkout
uses: actions/checkout@v2
with:
repository: keeweb/keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Download artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
run: npm i -g grunt-cli
- name: Write secrets
env:
CODESIGN: ${{ secrets.CODESIGN }}
MS_CODESIGN_PRIVATE_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PRIVATE_KEY_CLIENT }}
MS_CODESIGN_PUBLIC_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_CLIENT }}
MS_CODESIGN_PUBLIC_KEY_SERVER: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_SERVER }}
run: |
mkdir keys
echo $Env:CODESIGN > keys/codesign.json
mkdir keys/code-signing
echo $Env:MS_CODESIGN_PRIVATE_KEY_CLIENT > keys/code-signing/private-key-client.pem
echo $Env:MS_CODESIGN_PUBLIC_KEY_CLIENT > keys/code-signing/public-key-client.pem
echo $Env:MS_CODESIGN_PUBLIC_KEY_SERVER > keys/code-signing/public-key-server.pem
- name: Grunt
run: grunt desktop-win32 ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Upload ia32 exe artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
- name: Upload ia32 zip artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
- name: Upload x64 exe artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
- name: Upload x64 zip artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
- name: Upload arm64 exe artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
- name: Upload arm64 zip artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
publish:
runs-on: ubuntu-latest
needs:
- linux
- darwin
- win32
steps:
- name: Get current git tag
id: get_tag
uses: keeweb/get-git-tag@v3.0.2
with:
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
tagRegexGroup: 1
- name: Setup GCloud
uses: google-github-actions/setup-gcloud@master
with:
version: '285.0.0'
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
- name: Checkout
uses: actions/checkout@v2
with:
repository: keeweb/keeweb
path: keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
working-directory: keeweb
run: npm ci
- name: Download html artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: html
- name: Download linux.AppImage artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
path: assets
- name: Download linux.snap artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
path: assets
- name: Download linux.deb artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
path: assets
- name: Download linux.zip artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
path: assets
- name: Download linux.rpm artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
path: assets
- name: Download darwin.x64.dmg artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
path: assets
- name: Download darwin.arm64.dmg artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
path: assets
- name: Download win32.ia32.exe artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
path: assets
- name: Download win32.ia32.zip artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
path: assets
- name: Download win32.x64.exe artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
path: assets
- name: Download win32.x64.zip artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
path: assets
- name: Download win32.arm64.exe artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
path: assets
- name: Download win32.arm64.zip artifact
uses: actions/download-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
path: assets
- name: Zip html
working-directory: html
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
- name: Copy assets to dist
run: mkdir -p keeweb/dist/desktop && cp assets/* keeweb/dist/desktop
- name: Write secrets
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
working-directory: keeweb
run: |
mkdir keys
echo "$PRIVATE_KEY" > keys/private-key.pem
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
- name: Grunt
working-directory: keeweb
run: grunt finish-release
- name: Copy signatures to assets
run: cp keeweb/dist/desktop/Verify.sign.sha256 assets
- name: Copy checksums to assets
run: cp keeweb/dist/desktop/Verify.sha256 assets
- name: Login to DockerHub Registry
env:
DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: echo $DOCKERHUB_ACCESS_TOKEN | docker login -u $DOCKERHUB_USERNAME --password-stdin
- name: Copy dist to the Docker context
run: cp -r html keeweb/package/docker/dist
- name: Build the Docker image
working-directory: keeweb
run: docker build -t antelle/keeweb:latest -t antelle/keeweb:${{ steps.get_tag.outputs.tag }} package/docker
- name: Push the Docker image to dockerhub
if: ${{ github.repository == 'keeweb/keeweb' }}
run: docker push --all-tags antelle/keeweb
- name: Extract release notes
run: node util/extract-release-notes.js --version=${{ steps.get_tag.outputs.tag }} --output=tmp/release-body.md
working-directory: keeweb
- name: Publish GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.get_tag.outputs.tag }}
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
files: assets/*
body_path: keeweb/tmp/release-body.md
- name: Checkout gh-pages
uses: actions/checkout@v2
with:
ref: gh-pages
path: gh-pages
fetch-depth: 0
- name: Commit dist to gh-pages
working-directory: gh-pages
run: |
git rm -r '*'
cp -r ../html/* .
mkdir -p .github/workflows
echo $GITHUB_SHA > build.txt
date >> build.txt
git add .
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -am v${{ steps.get_tag.outputs.tag }}
- name: Push gh-pages
uses: keeweb/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
directory: gh-pages
- name: Restore git mtime
working-directory: gh-pages
run: python3 ../keeweb/.github/actions/scripts/git-restore-mtime.py
- name: Sync the website
if: ${{ github.repository == 'keeweb/keeweb' }}
run: gsutil -m rsync -r -d -x "^\." gh-pages gs://app.keeweb.info/
- name: Purge CloudFlare cache
if: ${{ github.repository == 'keeweb/keeweb' }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/dbda8e7e454d23edb08d4bc3b3f6f872/purge_cache" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
-19
View File
@@ -1,19 +0,0 @@
workspace.xml
misc.xml
.DS_Store
node_modules/
*.log
/dist/
tmp/
coverage/
keys
*.user
bin/
obj/
*.xcworkspace/
xcuserdata/
*.suo
.idea/
.vscode/
*.iml
test/dist/
-8
View File
@@ -1,8 +0,0 @@
{
"tabWidth": 4,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"quoteProps": "preserve",
"endOfLine": "auto"
}
View File
-66
View File
@@ -1,66 +0,0 @@
# Contributing to KeeWeb
Thanks for taking the time to contribute! :gift:
There are different ways to help the project, you will find a summary here.
## Code of conduct
This project and everyone participating in it is governed by the [KeeWeb Code of Conduct](https://github.com/keeweb/keeweb/blob/master/.github/CODE_OF_CONDUCT.md#readme).
By participating, you are expected to uphold this code. Please report unacceptable behavior to antelle.net@gmail.com.
## Issues
First, let's check if it's [already there](https://github.com/keeweb/keeweb/issues).
Perhaps, someone has already reported it?
[Click here](https://github.com/keeweb/keeweb/issues/new/choose) to create a new issue.
When creating issues, please answer some questions asked on that page.
They will help to understand, reproduce, and fix your issue.
## Vulnerabilities
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
## Questions, great ideas, etc...
You can [create an issue](https://github.com/keeweb/keeweb/issues/new/choose) to discuss any topic.
Indeed, questions on that page are not applicable in this case.
## Development
We don't accept (and in foreseeable future, we won't accept) anyone as a project collaborator directly.
Instead, the code should be contributed via pull requests. To get started with development, take a look at these pages:
- [Architecture](https://github.com/keeweb/keeweb/wiki/Architecture)
- [Engineering](https://github.com/keeweb/keeweb/wiki/Engineering)
- [Unsupported Features](https://github.com/keeweb/keeweb/wiki/Unsupported%20Features)
- [Pull Request Guidelines](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme)
## Pull requests
Please follow [this guide](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme).
## Docs (Wiki pages)
KeeWeb has [Wiki pages](https://github.com/keeweb/keeweb/wiki), however it's not possible to contribute with pull requests.
If you would like to improve it, please [open an issue](https://github.com/keeweb/keeweb/issues/new/choose) and submit your changes in Markdown format.
## Donations
There are two ways of donating:
- join as backer on [OpenCollective](https://opencollective.com/keeweb#support);
- sponsor the developer directly on [GitHub](https://github.com/sponsors/antelle).
Please note: donation does not imply any type of service contract.
## Translations
All KeeWeb translations are contributed by the community. To improve a translation or add a missing one,
join us on [OneSky](https://keeweb.oneskyapp.com).
## PR
We would be happy to support you if you decide to post, blog, or review the project,
or provide any other kind of PR. Please [contact the developer](mailto:antelle.net@gmail.com)
if you have anything to share. You can also poke us on [Twitter](https://twitter.com/kee_web).
-211
View File
@@ -1,211 +0,0 @@
======================================================================================
Dependencies Licenses
======================================================================================
jsqrcode license: Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================================
-794
View File
@@ -1,794 +0,0 @@
/* eslint-env node */
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const debug = require('debug');
const webpackConfig = require('./build/webpack.config');
const webpackConfigTest = require('./test/test.webpack.config');
const pkg = require('./package.json');
debug.enable('electron-notarize');
module.exports = function (grunt) {
require('time-grunt')(grunt);
require('load-grunt-tasks')(grunt);
grunt.loadTasks('build/tasks');
require('./grunt.tasks')(grunt);
require('./grunt.entrypoints')(grunt);
const date = new Date();
grunt.config.set('date', date);
const dt = date.toISOString().replace(/T.*/, '');
const year = date.getFullYear();
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
const skipSign = grunt.option('skip-sign');
const getCodeSignConfig = () =>
skipSign ? { identities: {} } : require('./keys/codesign.json');
let sha = grunt.option('commit-sha');
if (!sha) {
try {
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
} catch (e) {
grunt.warn(
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
'you can override what will be displayed in the UI with --commit-sha=xxx.'
);
}
}
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
const webpackOptions = {
date,
beta: !!grunt.option('beta'),
sha,
appleTeamId: '3LE7JZ657W'
};
const windowsAppVersionString = {
CompanyName: 'KeeWeb',
FileDescription: pkg.description,
OriginalFilename: 'KeeWeb.exe',
ProductName: 'KeeWeb',
InternalName: 'KeeWeb'
};
const appdmgOptions = (arch) => ({
title: 'KeeWeb',
icon: 'graphics/icon.icns',
background: 'graphics/dmg-background.png',
'background-color': '#E0E6F9',
'icon-size': 80,
window: { size: { width: 658, height: 498 } },
contents: [
{ x: 438, y: 344, type: 'link', path: '/Applications' },
{
x: 192,
y: 344,
type: 'file',
path: `tmp/desktop/KeeWeb-darwin-${arch}/KeeWeb.app`
}
]
});
const linuxDependencies = [
'libappindicator1',
'libgconf-2-4',
'gnome-keyring',
'libxtst6',
'libx11-6',
'libatspi2.0-0'
];
grunt.initConfig({
noop: { noop: {} },
clean: {
dist: ['dist', 'tmp'],
desktop: ['tmp/desktop', 'dist/desktop']
},
copy: {
html: {
src: 'app/index.html',
dest: 'tmp/index.html',
nonull: true
},
'content-dist': {
cwd: 'app/content/',
src: '**',
dest: 'dist/',
expand: true,
nonull: true
},
icons: {
cwd: 'app/icons/',
src: ['*.png', '*.svg'],
dest: 'tmp/icons/',
expand: true,
nonull: true
},
'dist-icons': {
cwd: 'app/icons/',
src: ['*.png', '*.svg'],
dest: 'dist/icons/',
expand: true,
nonull: true
},
manifest: {
cwd: 'app/manifest/',
src: ['*.json', '*.xml'],
dest: 'tmp/',
expand: true,
nonull: true
},
'dist-manifest': {
cwd: 'app/manifest/',
src: ['*.json', '*.xml'],
dest: 'dist/',
expand: true,
nonull: true
},
'desktop-html': {
src: 'dist/index.html',
dest: 'tmp/desktop/app/index.html',
nonull: true
},
'desktop-app-content': {
cwd: 'desktop/',
src: ['**', '!package-lock.json'],
dest: 'tmp/desktop/app/',
expand: true,
nonull: true
},
'desktop-darwin-installer-helper-x64': {
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
expand: true,
nonull: true,
options: { mode: true }
},
'desktop-darwin-installer-helper-arm64': {
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
expand: true,
nonull: true,
options: { mode: true }
},
'desktop-win32-dist-x64': {
src: 'tmp/desktop/KeeWeb.win.x64.exe',
dest: `dist/desktop/KeeWeb-${pkg.version}.win.x64.exe`,
nonull: true
},
'desktop-win32-dist-ia32': {
src: 'tmp/desktop/KeeWeb.win.ia32.exe',
dest: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.exe`,
nonull: true
},
'desktop-win32-dist-arm64': {
src: 'tmp/desktop/KeeWeb.win.arm64.exe',
dest: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.exe`,
nonull: true
},
'native-modules-darwin-x64': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-x64.node',
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
nonull: true
},
'native-modules-darwin-arm64': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-arm64.node',
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
nonull: true
},
'native-modules-win32-x64': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-x64.node',
dest: 'tmp/desktop/KeeWeb-win32-x64/resources/',
nonull: true
},
'native-modules-win32-ia32': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-ia32.node',
dest: 'tmp/desktop/KeeWeb-win32-ia32/resources/',
nonull: true
},
'native-modules-win32-arm64': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-arm64.node',
dest: 'tmp/desktop/KeeWeb-win32-arm64/resources/',
nonull: true
},
'native-modules-linux-x64': {
src: 'node_modules/@keeweb/keeweb-native-modules/*-linux-x64.node',
dest: 'tmp/desktop/keeweb-linux-x64/resources/',
nonull: true
},
'electron-builder-dist-linux-rpm': {
src: `tmp/desktop/electron-builder/KeeWeb-${pkg.version}.x86_64.rpm`,
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.x86_64.rpm`,
nonull: true
},
'electron-builder-dist-linux-snap': {
src: `tmp/desktop/electron-builder/KeeWeb_${pkg.version}_amd64.snap`,
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.snap`,
nonull: true
},
'electron-builder-dist-linux-appimage': {
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
nonull: true
},
'darwin-installer-icon': {
src: 'graphics/icon.icns',
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
nonull: true
},
'native-messaging-host-darwin-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-darwin-arm64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
dest:
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-linux-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-win32-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
nonull: true
},
'native-messaging-host-win32-ia32': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
nonull: true
},
'native-messaging-host-win32-arm64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
nonull: true
}
},
eslint: {
app: ['app/scripts/**/*.js'],
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
plugins: ['plugins/**/*.js'],
util: ['util/**/*.js'],
installer: ['package/osx/installer.js']
},
inline: {
app: {
src: 'tmp/index.html',
dest: 'tmp/app.html'
}
},
'csp-hashes': {
options: {
algo: 'sha512',
expected: {
style: 1,
script: 1
}
},
app: {
src: 'tmp/app.html',
dest: 'dist/index.html'
}
},
htmlmin: {
options: {
removeComments: true,
collapseWhitespace: true
},
app: {
files: {
'tmp/app.html': 'tmp/app.html'
}
}
},
'string-replace': {
'update-manifest': {
options: {
replacements: [
{
pattern: /"version":\s*".*?"/,
replacement: `"version": "${pkg.version}"`
},
{
pattern: /"date":\s*".*?"/,
replacement: `"date": "${dt}"`
}
]
},
files: { 'dist/update.json': 'app/update.json' }
},
'service-worker': {
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
files: { 'dist/service-worker.js': 'app/service-worker.js' }
},
'desktop-public-key': {
options: {
replacements: [
{
pattern: "'@@PUBLIC_KEY_CONTENT'",
replacement:
'`' +
fs
.readFileSync('app/resources/public-key.pem', {
encoding: 'utf8'
})
.trim() +
'`'
}
]
},
files: { 'tmp/desktop/app/main.js': 'desktop/main.js' }
}
},
webpack: {
app: webpackConfig.config(webpackOptions),
test: webpackConfigTest
},
'webpack-dev-server': {
options: {
webpack: webpackConfig.config({
...webpackOptions,
mode: 'development',
sha: 'dev'
}),
publicPath: '/',
contentBase: [
path.resolve(__dirname, 'tmp'),
path.resolve(__dirname, 'app/content')
],
progress: false
},
js: {
keepalive: true,
port: 8085
}
},
electron: {
options: {
name: 'KeeWeb',
dir: 'tmp/desktop/app',
out: 'tmp/desktop',
electronVersion,
overwrite: true,
asar: true,
appCopyright: `Copyright © ${year} Antelle`,
appVersion: pkg.version,
buildVersion: sha
},
linux: {
options: {
name: 'keeweb',
platform: 'linux',
arch: 'x64',
icon: 'graphics/icon.ico'
}
},
'darwin-x64': {
options: {
platform: 'darwin',
arch: 'x64',
icon: 'graphics/icon.icns',
appBundleId: 'net.antelle.keeweb',
appCategoryType: 'public.app-category.productivity',
extendInfo: 'package/osx/extend.plist'
}
},
'darwin-arm64': {
options: {
platform: 'darwin',
arch: 'arm64',
icon: 'graphics/icon.icns',
appBundleId: 'net.antelle.keeweb',
appCategoryType: 'public.app-category.productivity',
extendInfo: 'package/osx/extend.plist'
}
},
'win32-x64': {
options: {
platform: 'win32',
arch: 'x64',
icon: 'graphics/icon.ico',
buildVersion: pkg.version,
'version-string': windowsAppVersionString
}
},
'win32-ia32': {
options: {
platform: 'win32',
arch: 'ia32',
icon: 'graphics/icon.ico',
buildVersion: pkg.version,
'version-string': windowsAppVersionString
}
},
'win32-arm64': {
options: {
platform: 'win32',
arch: 'arm64',
icon: 'graphics/icon.ico',
buildVersion: pkg.version,
'version-string': windowsAppVersionString
}
}
},
'electron-builder': {
linux: {
options: {
publish: 'never',
targets: 'linux',
prepackaged: 'tmp/desktop/keeweb-linux-x64',
config: {
appId: 'net.antelle.keeweb',
productName: 'keeweb',
copyright: `Copyright © ${year} Antelle`,
directories: {
output: 'tmp/desktop/electron-builder',
app: 'desktop',
buildResources: 'graphics'
},
fileAssociations: {
ext: 'kdbx',
name: 'KeePass 2 database',
mimeType: 'application/x-keepass2'
},
linux: {
target: ['AppImage', 'snap', 'rpm'],
category: 'Utility'
},
rpm: {
// depends: linuxDependencies
},
snap: {
stagePackages: linuxDependencies
}
}
}
}
},
'electron-patch': {
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
},
osacompile: {
options: {
language: 'JavaScript'
},
installer: {
files: {
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
}
}
},
compress: {
options: {
level: 6
},
'win32-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
},
'win32-ia32': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-ia32', src: '**', expand: true }]
},
'win32-arm64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-arm64', src: '**', expand: true }]
},
'linux-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` },
files: [
{ cwd: 'tmp/desktop/keeweb-linux-x64', src: '**', expand: true },
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
]
}
},
appdmg: {
x64: {
options: appdmgOptions('x64'),
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.x64.dmg`
},
arm64: {
options: appdmgOptions('arm64'),
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.arm64.dmg`
}
},
nsis: {
options: {
vars: {
version: pkg.version,
rev: sha,
homepage: pkg.homepage
}
},
'win32-x64': {
options: {
installScript: 'package/nsis/main.nsi',
arch: 'x64',
output: 'tmp/desktop/KeeWeb.win.x64.exe'
}
},
'win32-un-x64': {
options: {
installScript: 'package/nsis/main-un.nsi',
arch: 'x64',
output: 'tmp/desktop/KeeWeb-win32-x64/uninst.exe'
}
},
'win32-ia32': {
options: {
installScript: 'package/nsis/main.nsi',
arch: 'ia32',
output: 'tmp/desktop/KeeWeb.win.ia32.exe'
}
},
'win32-un-ia32': {
options: {
installScript: 'package/nsis/main-un.nsi',
arch: 'ia32',
output: 'tmp/desktop/KeeWeb-win32-ia32/uninst.exe'
}
},
'win32-arm64': {
options: {
installScript: 'package/nsis/main.nsi',
arch: 'arm64',
output: 'tmp/desktop/KeeWeb.win.arm64.exe'
}
},
'win32-un-arm64': {
options: {
installScript: 'package/nsis/main-un.nsi',
arch: 'arm64',
output: 'tmp/desktop/KeeWeb-win32-arm64/uninst.exe'
}
}
},
chmod: {
'linux-desktop-x64': {
options: {
mode: '4755'
},
src: ['tmp/desktop/keeweb-linux-x64/chrome-sandbox']
}
},
deb: {
options: {
tmpPath: 'tmp/desktop/',
package: {
name: 'keeweb-desktop',
version: pkg.version,
description: pkg.description,
author: pkg.author,
homepage: pkg.homepage,
rev: sha
}
},
'linux-x64': {
options: {
info: {
arch: 'amd64',
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
targetDir: 'dist/desktop',
appName: 'KeeWeb',
depends: linuxDependencies.join(', '),
scripts: {
postinst: 'package/deb/scripts/postinst'
}
}
},
files: [
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true },
{
cwd: 'tmp/desktop/keeweb-linux-x64/',
src: '**',
dest: '/usr/share/keeweb-desktop',
expand: true,
nonull: true
},
{
src: 'graphics/128x128.png',
dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png',
nonull: true
}
]
}
},
'osx-sign': {
options: {
get identity() {
return getCodeSignConfig().identities.app;
},
hardenedRuntime: true,
entitlements: 'package/osx/entitlements.plist',
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
'gatekeeper-assess': false
},
'desktop-x64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
},
'desktop-arm64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
},
'installer': {
src: 'tmp/desktop/KeeWeb Installer.app'
}
},
notarize: {
options: {
appBundleId: 'net.antelle.keeweb',
get appleId() {
return getCodeSignConfig().appleId;
},
appleIdPassword: '@keychain:AC_PASSWORD',
get ascProvider() {
return getCodeSignConfig().teamId;
}
},
'desktop-x64': {
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
},
'desktop-arm64': {
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
}
},
'sign-exe': {
options: {
url: pkg.homepage,
get windows() {
return getCodeSignConfig().windows;
},
get certHash() {
return getCodeSignConfig().microsoftCertHash;
}
},
'win32-build-x64': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-x64/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
'tmp/desktop/KeeWeb-win32-x64/libGLESv2.dll':
'ANGLE libGLESv2 Dynamic Link Library'
}
}
},
'win32-build-ia32': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-ia32/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
'tmp/desktop/KeeWeb-win32-ia32/libGLESv2.dll':
'ANGLE libGLESv2 Dynamic Link Library'
}
}
},
'win32-build-arm64': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-arm64/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-arm64/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
'tmp/desktop/KeeWeb-win32-arm64/libGLESv2.dll':
'ANGLE libGLESv2 Dynamic Link Library'
}
}
},
'win32-uninst-x64': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-x64/uninst.exe': 'KeeWeb Uninstaller'
}
}
},
'win32-uninst-ia32': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-ia32/uninst.exe': 'KeeWeb Uninstaller'
}
}
},
'win32-uninst-arm64': {
options: {
files: {
'tmp/desktop/KeeWeb-win32-arm64/uninst.exe': 'KeeWeb Uninstaller'
}
}
},
'win32-installer-x64': {
options: {
files: {
'tmp/desktop/KeeWeb.win.x64.exe': 'KeeWeb Setup'
}
}
},
'win32-installer-ia32': {
options: {
files: {
'tmp/desktop/KeeWeb.win.ia32.exe': 'KeeWeb Setup'
}
}
},
'win32-installer-arm64': {
options: {
files: {
'tmp/desktop/KeeWeb.win.arm64.exe': 'KeeWeb Setup'
}
}
}
},
'sign-dist': {
dist: {
options: {
sign: 'dist/desktop/Verify.sign.sha256'
},
files: {
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
}
}
},
'run-test': {
options: {
headless: true
},
default: 'test/runner.html'
},
virustotal: {
options: {
prefix: `keeweb.v${pkg.version}-${sha}.`,
timeout: 10 * 60 * 1000,
get apiKey() {
return require('./keys/virus-total.json').apiKey;
}
},
html: 'dist/index.html'
}
});
};
-24
View File
@@ -1,24 +0,0 @@
MIT License
Copyright (c) 2021 Antelle https://antelle.net
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Licences of dependencies can be found here:
https://raw.githubusercontent.com/keeweb/keeweb/master/DEPS-LICENSE
-100
View File
@@ -1,100 +0,0 @@
# Free cross-platform password manager compatible with KeePass
This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources.
The app can run either in browser, or as a desktop app.
![screenshot](img/screenshot.png)
## Quick Links
Apps: [Web](https://app.keeweb.info/), [Desktop](https://github.com/keeweb/keeweb/releases/latest)
Timeline: [Release Notes](release-notes.md), [TODO](https://github.com/keeweb/keeweb/wiki/TODO)
On one page: [Features](https://keeweb.info/#features), [FAQ](https://github.com/keeweb/keeweb/wiki/FAQ)
Website: [keeweb.info](https://keeweb.info)
Twitter: [kee_web](https://twitter.com/kee_web)
Donate: [OpenCollective](https://opencollective.com/keeweb#support), [GitHub](https://github.com/sponsors/antelle)
## Status
The app is quite stable now. Basic stuff, as well as more advanced operations, should be rather reliable.
## Self-hosting
Everything you need to host this app on your server is any static file server. The app is a single HTML file + a service worker (optionally; for offline access).
You can download the latest distribution files from [gh-pages](https://github.com/keeweb/keeweb/archive/gh-pages.zip) branch.
If you are using Docker:
1. put your dh.pem, cert.pem, key.pem to /etc/nginx/external/
2. run this script:
```bash
docker run --name keeweb -d -p 443:443 -p 80:80 -v $EXT_DIR:/etc/nginx/external/ antelle/keeweb
```
To make Dropbox work in your self-hosted app, [go to this Wiki page](https://github.com/keeweb/keeweb/wiki/Dropbox-and-GDrive).
## Building
The easiest way to clone all KeeWeb repos is:
```bash
curl https://raw.githubusercontent.com/keeweb/keeweb/develop/dev-env.sh | bash -
```
The app can be built with grunt: `grunt` (html files will be in `dist/`).
Desktop apps are built with `grunt desktop`. This requires some magic and currently works only on CI,
you can find more details in [the GitHub Actions workflow](.github/workflows/build.yaml).
To run the desktop (electron) app without building an installer, build the app with `grunt` and start it this way:
```bash
npm run dev
npm run electron
```
For debug build:
1. run `npm run dev`
2. open `http://localhost:8085`
To build desktop apps, use these goals, the result can be found in `tmp`:
```
npm run dev-desktop-macos
npm run dev-desktop-windows
npm run dev-desktop-linux
```
## Contributing
Please read contribution guidelines [for pull requests](.github/PULL_REQUEST_TEMPLATE.md).
Here's a [list of issues](https://github.com/keeweb/keeweb/labels/help%20wanted) where your help would be very welcome.
Also you can help by [translating KeeWeb](https://keeweb.oneskyapp.com) to your language.
Other ways of contribution can be found [on this page](CONTRIBUTING.md).
#### Important notes for pull requests
- please branch from `develop`, not `master`
- don't edit translation files except base.json, they will be replaced
## Donations
KeeWeb is not free to develop. It takes time, requires paid code signing certificates and domains.
You can help the project or say "thank you" with this button:
[<img src="https://opencollective.com/keeweb/tiers/backer.svg?avatarHeight=42&width=880" alt="OpenCollective">](https://opencollective.com/keeweb#support)
You can also sponsor the developer directly [on GitHub](https://github.com/sponsors/antelle).
Please note: donation does not imply any type of service contract.
## Thank you
Notable contributions to KeeWeb:
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
## License
[MIT](https://github.com/keeweb/keeweb/blob/master/LICENSE)
-11
View File
@@ -1,11 +0,0 @@
# Security Policy
## Supported Versions
Only the latest version of the product is supported. This version can be accessed at https://app.keeweb.info
and downloaded from https://keeweb.info or GitHub releases ([latest release link](https://github.com/keeweb/keeweb/releases/latest)).
## Reporting a Vulnerability
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
-85
View File
@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>KeeWeb</title>
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
font-src data:;
script-src 'self' 'unsafe-eval';
style-src 'self' blob:;
connect-src 'self' ws: https:;
child-src 'self' blob:;
worker-src 'self' blob:;
img-src 'self' data: blob: https://services.keeweb.info/;
object-src 'none';
form-action 'none';
"
/>
<meta name="application-name" content="KeeWeb" />
<meta name="kw-config" content="(no-config)" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="KeeWeb" />
<meta name="theme-color" content="#6386ec" />
<meta name="msapplication-config" content="browserconfig.xml" />
<meta name="msapplication-TileColor" content="#6386ec" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
<link rel="mask-icon" href="icons/safari-pinned-tab.svg" color="#6386ec" />
<link
rel="apple-touch-startup-image"
href="icons/splash-640x1136.png"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-750x1294.png"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-1242x2148.png"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-1125x2436.png"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-1536x2048.png"
media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-1668x2224.png"
media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="icons/splash-2048x2732.png"
media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="css/app.css?__inline=true" />
<script src="js/app.js?__inline=true"></script>
</head>
<body class="th-d">
<noscript>
<h1>KeeWeb</h1>
<p>
KeeWeb is a password manager written in JavaScript. Please enable JavaScript to run
it.
</p>
</noscript>
</body>
</html>
-71
View File
@@ -1,71 +0,0 @@
(function (global) {
var babelHelpers = global.babelHelpers = {};
babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
babelHelpers.defineProperty = function (obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
};
babelHelpers.slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
babelHelpers.toConsumableArray = function (arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
} else {
return Array.from(arr);
}
};
})(typeof global === "undefined" ? self : global);
Binary file not shown.
-9
View File
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZAmWXMpKsh9WCyUO5TR
2/QIJsgyaViWRAgY5nHZhTR/sjhfG97OeurW7NYC4XPsnyJDepEjDsj9LNzquMES
xsLRSmxo+m1lVWn8Q59LwqOvUK+BKH4eZYF89Nwv2HZjZnr/w7xYcxJbhOuAzpbq
FD/gAvlozrsHG9uI95h1FHPd6mfolEHLK30LXPQ5FdJ/AWb29ILKYbVEdM8qsjoN
fSICw7XTbd7wB0TMcgRpjWllzKaftxCO38svhBjAz4cUvtZ05yi4dzK0VsJHfW33
uno3YBn0/bwLkqMzD1/Hv05p6CVzuDTOCBsXkEobra8OBviatoSukot6cS4Famki
9QIDAQAB
-----END PUBLIC KEY-----
-9
View File
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRq2k3TTx0ewTe6wDr6Q
VeB5diwiIWzsJD+ApfZu1KNPedcAgslAfjpNsYF1if6cYsPMJH70xJ2np6RQBl1V
PdwShOuxkD7m0BD5Hw/Aar8Hdp5cvAdOOMdBO+0DbGeUMy+z66s+oUCJmqVp19T6
PkkxbhN08rgtT7v+aFvrbqbO/vlsskbJpH2K2io+e1XmRGPnSr9q4KSqfGbTfe5g
LwDIOFd66Z4mb5Utb5wWpsy6Gjh06Yf257AccGD3A1bkTNOyeeX0tqciYBePWMk0
icP/aZ6hnErfhnUKf3tOgPLppSHiGcaSKekhChZ2xLUs3U64JwrXSmwHj+TzdO3S
0QIDAQAB
-----END PUBLIC KEY-----
-5
View File
@@ -1,5 +0,0 @@
{
"rules": {
"import/no-commonjs": "error"
}
}
-212
View File
@@ -1,212 +0,0 @@
import { Events } from 'framework/events';
import { StartProfiler } from 'comp/app/start-profiler';
import { FileInfoCollection } from 'collections/file-info-collection';
import { AppRightsChecker } from 'comp/app/app-rights-checker';
import { ExportApi } from 'comp/app/export-api';
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
import { Updater } from 'comp/app/updater';
import { UsbListener } from 'comp/app/usb-listener';
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
import { FeatureTester } from 'comp/browser/feature-tester';
import { FocusDetector } from 'comp/browser/focus-detector';
import { IdleTracker } from 'comp/browser/idle-tracker';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { KeyHandler } from 'comp/browser/key-handler';
import { PopupNotifier } from 'comp/browser/popup-notifier';
import { Launcher } from 'comp/launcher';
import { SettingsManager } from 'comp/settings/settings-manager';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { AppModel } from 'models/app-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { RuntimeDataModel } from 'models/runtime-data-model';
import { UpdateModel } from 'models/update-model';
import { PluginManager } from 'plugins/plugin-manager';
import { Features } from 'util/features';
import { KdbxwebInit } from 'util/kdbxweb/kdbxweb-init';
import { Locale } from 'util/locale';
import { AppView } from 'views/app-view';
import 'hbs-helpers';
import { AutoType } from './auto-type';
import { Storage } from './storage';
StartProfiler.milestone('loading modules');
const ready = (Launcher && Launcher.ready) || $;
ready(() => {
StartProfiler.milestone('document ready');
const appModel = new AppModel();
StartProfiler.milestone('creating app model');
Promise.resolve()
.then(loadConfigs)
.then(initModules)
.then(loadRemoteConfig)
.then(ensureCanRun)
.then(initStorage)
.then(initUsbListener)
.then(showApp)
.then(postInit)
.catch((e) => {
appModel.appLogger.error('Error starting app', e);
});
function ensureCanRun() {
if (Features.isFrame && !appModel.settings.allowIframes) {
return Promise.reject(
'Running in iframe is not allowed (this can be changed in the app config).'
);
}
return FeatureTester.test()
.catch((e) => {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appNotSupportedError,
pre: e,
buttons: [],
esc: false,
enter: false,
click: false
});
throw 'Feature testing failed: ' + e;
})
.then(() => {
StartProfiler.milestone('checking features');
});
}
function loadConfigs() {
return Promise.all([
AppSettingsModel.load(),
UpdateModel.load(),
RuntimeDataModel.load(),
FileInfoCollection.load()
]).then(() => {
StartProfiler.milestone('loading configs');
});
}
function initModules() {
KeyHandler.init();
PopupNotifier.init();
KdbxwebInit.init();
FocusDetector.init();
AutoType.init();
ThemeWatcher.init();
SettingsManager.init();
window.kw = ExportApi;
return PluginManager.init().then(() => {
StartProfiler.milestone('initializing modules');
});
}
function showSettingsLoadError() {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appSettingsErrorBody,
buttons: [],
esc: false,
enter: false,
click: false
});
}
function loadRemoteConfig() {
return Promise.resolve()
.then(() => {
SettingsManager.setBySettings();
const configParam = getConfigParam();
if (configParam) {
return appModel
.loadConfig(configParam)
.then(() => {
SettingsManager.setBySettings();
})
.catch((e) => {
if (!appModel.settings.cacheConfigSettings) {
showSettingsLoadError();
throw e;
}
});
}
})
.then(() => {
StartProfiler.milestone('loading remote config');
});
}
function initStorage() {
for (const prv of Object.values(Storage)) {
prv.init();
}
StartProfiler.milestone('initializing storage');
}
function initUsbListener() {
UsbListener.init();
StartProfiler.milestone('starting usb');
}
function showApp() {
return Promise.resolve().then(() => {
const skipHttpsWarning =
localStorage.skipHttpsWarning || appModel.settings.skipHttpsWarning;
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
const hostIsInsecure = location.hostname !== 'localhost';
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
return new Promise((resolve) => {
Alerts.error({
header: Locale.appSecWarn,
icon: 'user-secret',
esc: false,
enter: false,
click: false,
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
complete: () => {
showView();
resolve();
}
});
});
} else {
showView();
return new Promise((resolve) => requestAnimationFrame(resolve));
}
});
}
function postInit() {
setTimeout(() => {
Updater.init();
SingleInstanceChecker.init();
AppRightsChecker.init();
IdleTracker.init();
BrowserExtensionConnector.init(appModel);
PluginManager.runAutoUpdate();
}, Timeouts.AutoUpdatePluginsAfterStart);
}
function showView() {
new AppView(appModel).render();
StartProfiler.milestone('first view rendering');
Events.emit('app-ready');
StartProfiler.milestone('app ready event');
StartProfiler.report();
}
function getConfigParam() {
const metaConfig = document.head.querySelector('meta[name=kw-config]');
if (metaConfig && metaConfig.content && metaConfig.content[0] !== '(') {
return metaConfig.content;
}
const match = location.search.match(/[?&]config=([^&]+)/i);
if (match && match[1]) {
return match[1];
}
}
});
-160
View File
@@ -1,160 +0,0 @@
import { Features } from 'util/features';
import { NativeModules } from 'comp/launcher/native-modules';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
const logger = new Logger('auto-type-emitter');
const KeyMap = {
tab: 'Tab',
enter: 'Return',
space: 'Space',
up: 'UpArrow',
down: 'DownArrow',
left: 'LeftArrow',
right: 'RightArrow',
home: 'Home',
end: 'End',
pgup: 'PageUp',
pgdn: 'PageDown',
ins: 'Insert',
del: 'ForwardDelete',
bs: 'BackwardDelete',
esc: 'Escape',
win: 'Meta',
rwin: 'RightMeta',
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
f13: 'F13',
f14: 'F14',
f15: 'F15',
f16: 'F16',
add: 'KeypadPlus',
subtract: 'KeypadMinus',
multiply: 'KeypadMultiply',
divide: 'KeypadDivide',
n0: 'D0',
n1: 'D1',
n2: 'D2',
n3: 'D3',
n4: 'D4',
n5: 'D5',
n6: 'D6',
n7: 'D7',
n8: 'D8',
n9: 'D9'
};
const ModMap = {
'^': Features.isMac ? 'Command' : 'Ctrl',
'+': 'Shift',
'%': 'Alt',
'^^': 'Ctrl'
};
class AutoTypeEmitter {
constructor(callback) {
this.callback = callback;
this.mod = {};
}
begin() {
this.withCallback(NativeModules.kbdEnsureModifierNotPressed());
}
setMod(mod, enabled) {
const nativeMod = ModMap[mod];
if (!nativeMod) {
return this.callback(`Bad modifier: ${mod}`);
}
NativeModules.kbdKeyMoveWithModifier(!!enabled, [nativeMod]).catch((e) => {
logger.error('Error moving modifier', mod, enabled ? 'down' : 'up', e);
});
if (enabled) {
this.mod[nativeMod] = true;
} else {
delete this.mod[nativeMod];
}
}
text(str) {
if (!str) {
return this.withCallback(Promise.resolve());
}
const mods = Object.keys(this.mod);
if (mods.length) {
this.withCallback(NativeModules.kbdTextAsKeys(str, mods));
} else {
this.withCallback(NativeModules.kbdText(str));
}
}
key(key) {
const mods = Object.keys(this.mod);
if (typeof key === 'number') {
this.withCallback(NativeModules.kbdKeyPressWithCharacter(0, key, mods));
} else {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
const code = KeyMap[key];
this.withCallback(NativeModules.kbdKeyPress(code, mods));
}
}
copyPaste(text) {
setTimeout(() => {
Launcher.setClipboardText(text);
setTimeout(() => {
this.withCallback(NativeModules.kbdShortcut('V'));
}, Timeouts.AutoTypeCopyPaste);
}, Timeouts.AutoTypeCopyPaste);
}
wait(time) {
setTimeout(() => this.withCallback(Promise.resolve()), time);
}
waitComplete() {
this.withCallback(Promise.resolve());
}
setDelay() {
this.callback('Not implemented');
}
withCallback(promise) {
promise
.then(() => {
try {
this.callback();
} catch (err) {
logger.error('Callback error', err);
}
})
.catch((err) => {
const keyPressFailed = err.message === 'Key press failed';
if (keyPressFailed) {
err.keyPressFailed = true;
}
try {
this.callback(err);
} catch (err) {
logger.error('Callback error', err);
}
});
}
}
export { AutoTypeEmitter };
-16
View File
@@ -1,16 +0,0 @@
import { NativeModules } from 'comp/launcher/native-modules';
class AutoTypeHelper {
getActiveWindowInfo(callback) {
NativeModules.kbdGetActiveWindow({
getWindowTitle: true,
getBrowserUrl: true
})
.then((win) => {
callback(undefined, win);
})
.catch((err) => callback(err));
}
}
export { AutoTypeHelper };
@@ -1,209 +0,0 @@
import { Logger } from 'util/logger';
const logger = new Logger(
'auto-type-obfuscator',
undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
);
const MaxFakeOps = 30;
const MaxSteps = 1000;
const MaxCopy = 2;
const FakeCharAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789O0oIl';
const AutoTypeObfuscator = function (chars) {
this.chars = chars;
this.inputChars = [];
this.inputCursor = 0;
this.inputSel = 0;
this.ops = [];
this.stepCount = 0;
this.copyCount = 0;
};
AutoTypeObfuscator.prototype.obfuscate = function () {
while (!this.finished()) {
this.step();
if (this.stepCount++ > MaxSteps) {
throw 'Obfuscate failed';
}
}
for (let i = 0; i < this.chars.length; i++) {
this.chars[i] = null;
this.inputChars[i] = null;
}
return this.ops;
};
AutoTypeObfuscator.prototype.finished = function () {
return (
this.chars.length === this.inputChars.length &&
this.chars.every(function (ch, ix) {
return this.inputChars[ix].ch === ch;
}, this)
);
};
AutoTypeObfuscator.prototype.step = function () {
const isFake = this.stepCount < MaxFakeOps && Math.random() > this.stepCount / MaxFakeOps;
if (isFake) {
this.stepFake();
} else {
this.stepReal();
}
if (logger.getLevel() >= Logger.Level.Debug) {
logger.debug('value', this.inputChars.map((ic) => ic.ch).join(''));
}
};
AutoTypeObfuscator.prototype.stepFake = function () {
const pos = Math.floor(Math.random() * (this.inputChars.length + 1));
const ch = FakeCharAlphabet[Math.floor(Math.random() * FakeCharAlphabet.length)];
logger.info('step.fake', pos, ch);
this.moveToPos(pos);
const insert = this.inputChars.length === 0 || Math.random() > 0.3;
if (insert) {
this.inputChar(ch);
} else {
let moveLeft = Math.random() > 0.5;
let maxMove = moveLeft ? pos : this.inputChars.length - pos;
if (maxMove === 0) {
moveLeft = !moveLeft;
maxMove = moveLeft ? pos : this.inputChars.length - pos;
}
const moveCount = Math.max(Math.floor(Math.pow(Math.random(), 3) * maxMove), 1);
if (moveCount <= 1 && Math.random() > 0.5) {
this.deleteText(moveLeft);
} else {
this.selectText(moveLeft, moveCount);
if (Math.random() > 0.3) {
this.deleteText(Math.random() > 0.5);
} else {
this.inputChar(ch);
}
}
}
};
AutoTypeObfuscator.prototype.stepReal = function () {
const possibleActions = [];
const inputRealPositions = [];
let i;
for (i = 0; i < this.chars.length; i++) {
inputRealPositions.push(-1);
}
for (i = 0; i < this.inputChars.length; i++) {
const ix = this.inputChars[i].ix;
if (ix === undefined) {
possibleActions.push({ del: true, pos: i });
} else {
inputRealPositions[ix] = i;
}
}
for (i = 0; i < this.chars.length; i++) {
if (inputRealPositions[i] < 0) {
let from = 0,
to = this.inputChars.length;
for (let j = 0; j < this.chars.length; j++) {
if (j < i && inputRealPositions[j] >= 0) {
from = inputRealPositions[j] + 1;
}
if (j > i && inputRealPositions[j] >= 0) {
to = inputRealPositions[j];
break;
}
}
possibleActions.push({ ins: true, ch: this.chars[i], ix: i, from, to });
}
}
const action = possibleActions[Math.floor(Math.random() * possibleActions.length)];
logger.info('step.real', inputRealPositions, action);
if (action.del) {
this.moveToPos(action.pos + 1);
this.deleteText(true);
} else {
const insPos = action.from + Math.floor(Math.random() * (action.to - action.from));
this.moveToPos(insPos);
if (this.copyCount < MaxCopy && action.ch !== '\n' && Math.random() > 0.5) {
this.copyCount++;
this.copyPaste(action.ch);
} else {
this.inputChar(action.ch);
}
this.inputChars[insPos].ix = action.ix;
}
};
AutoTypeObfuscator.prototype.moveToPos = function (pos) {
logger.debug('moveToPos', pos);
while (this.inputCursor > pos) {
this.moveLeft();
}
while (this.inputCursor < pos) {
this.moveRight();
}
};
AutoTypeObfuscator.prototype.moveLeft = function () {
logger.debug('moveLeft');
this.ops.push({ type: 'key', value: 'left' });
this.inputCursor--;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.moveRight = function () {
logger.debug('moveRight');
this.ops.push({ type: 'key', value: 'right' });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.inputChar = function (ch) {
logger.debug('inputChar', ch);
this.ops.push({ type: 'text', value: ch });
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.copyPaste = function (ch) {
logger.debug('copyPaste', ch);
this.ops.push({ type: 'cmd', value: 'copyPaste', arg: ch });
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.selectText = function (backward, count) {
logger.debug('selectText', backward ? 'left' : 'right', count);
const ops = [];
for (let i = 0; i < count; i++) {
ops.push({ type: 'key', value: backward ? 'left' : 'right' });
}
if (ops.length === 1) {
ops[0].mod = { '+': true };
this.ops.push(ops[0]);
} else {
this.ops.push({ type: 'group', value: ops, mod: { '+': true } });
}
if (backward) {
this.inputCursor -= count;
}
this.inputSel = count;
};
AutoTypeObfuscator.prototype.deleteText = function (backward) {
logger.debug('deleteText', backward ? 'left' : 'right');
this.ops.push({ type: 'key', value: backward ? 'bs' : 'del' });
if (this.inputSel) {
this.inputChars.splice(this.inputCursor, this.inputSel);
this.inputSel = 0;
} else {
this.inputChars.splice(backward ? this.inputCursor - 1 : this.inputCursor, 1);
if (backward) {
this.inputCursor--;
}
}
};
export { AutoTypeObfuscator };
-127
View File
@@ -1,127 +0,0 @@
import { AutoTypeRunner } from 'auto-type/auto-type-runner';
const AutoTypeParser = function (sequence) {
this.sequence = sequence;
this.ix = 0;
this.states = [];
};
AutoTypeParser.opRegex = /^(.*?)(?:([\s:=])[\s:=]*(.*))?$/;
AutoTypeParser.prototype.parse = function () {
const len = this.sequence.length;
this.pushState();
while (this.ix < len) {
const ch = this.sequence[this.ix];
switch (ch) {
case '{':
this.readOp();
continue;
case '+':
case '%':
case '^':
this.readModifier(ch);
break;
case '(':
this.pushState();
break;
case ')':
this.popState();
break;
case ' ':
break;
case '~':
this.addOp('enter');
break;
default:
this.addChar(ch);
break;
}
this.ix++;
}
if (this.states.length !== 1) {
throw 'Groups count mismatch';
}
return new AutoTypeRunner(this.state().ops);
};
AutoTypeParser.prototype.pushState = function () {
this.states.unshift({
modifiers: null,
ops: []
});
};
AutoTypeParser.prototype.popState = function () {
if (this.states.length <= 1) {
throw 'Unexpected ")" at index ' + this.ix;
}
const state = this.states.shift();
this.addState(state);
};
AutoTypeParser.prototype.state = function () {
return this.states[0];
};
AutoTypeParser.prototype.readOp = function () {
const toIx = this.sequence.indexOf('}', this.ix + 2);
if (toIx < 0) {
throw 'Mismatched "{" at index ' + this.ix;
}
const contents = this.sequence.substring(this.ix + 1, toIx);
this.ix = toIx + 1;
if (contents.length === 1) {
this.addChar(contents);
return;
}
const [, op, sep, arg] = contents.match(AutoTypeParser.opRegex);
this.addOp(op, sep, arg);
};
AutoTypeParser.prototype.readModifier = function (modifier) {
const state = this.state();
if (!state.modifiers) {
state.modifiers = {};
}
if (modifier === '^' && state.modifiers['^']) {
delete state.modifiers['^'];
modifier = '^^';
}
state.modifiers[modifier] = true;
};
AutoTypeParser.prototype.resetModifiers = function () {
const state = this.state();
const modifiers = state.modifiers;
state.modifiers = null;
return modifiers;
};
AutoTypeParser.prototype.addState = function (state) {
this.state().ops.push({
type: 'group',
value: state.ops,
mod: this.resetModifiers()
});
};
AutoTypeParser.prototype.addChar = function (ch) {
this.state().ops.push({
type: 'text',
value: ch,
mod: this.resetModifiers()
});
};
AutoTypeParser.prototype.addOp = function (op, sep, arg) {
this.state().ops.push({
type: 'op',
value: op,
mod: this.resetModifiers(),
sep,
arg
});
};
export { AutoTypeParser };
-541
View File
@@ -1,541 +0,0 @@
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
const emitterLogger = new Logger(
'auto-type-emitter',
undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
);
const AutoTypeRunner = function (ops) {
this.ops = ops;
this.pendingResolvesCount = 0;
this.entry = null;
this.now = new Date();
};
AutoTypeRunner.PendingResolve = { pending: true };
AutoTypeRunner.Keys = {
tab: 'tab',
enter: 'enter',
space: 'space',
up: 'up',
down: 'down',
left: 'left',
right: 'right',
home: 'home',
end: 'end',
pgup: 'pgup',
pgdn: 'pgdn',
insert: 'ins',
ins: 'ins',
delete: 'del',
del: 'del',
backspace: 'bs',
bs: 'bs',
bksp: 'bs',
esc: 'esc',
win: 'win',
lwin: 'win',
rwin: 'rwin',
f1: 'f1',
f2: 'f2',
f3: 'f3',
f4: 'f4',
f5: 'f5',
f6: 'f6',
f7: 'f7',
f8: 'f8',
f9: 'f9',
f10: 'f10',
f11: 'f11',
f12: 'f12',
f13: 'f13',
f14: 'f14',
f15: 'f15',
f16: 'f16',
add: 'add',
subtract: 'subtract',
multiply: 'multiply',
divide: 'divide',
numpad0: 'n0',
numpad1: 'n1',
numpad2: 'n2',
numpad3: 'n3',
numpad4: 'n4',
numpad5: 'n5',
numpad6: 'n6',
numpad7: 'n7',
numpad8: 'n8',
numpad9: 'n9'
};
AutoTypeRunner.Substitutions = {
title(runner, op) {
return runner.getEntryFieldKeys('Title', op);
},
username(runner, op) {
return runner.getEntryFieldKeys('UserName', op);
},
url(runner, op) {
return runner.getEntryFieldKeys('URL', op);
},
password(runner, op) {
return runner.getEntryFieldKeys('Password', op);
},
notes(runner, op) {
return runner.getEntryFieldKeys('Notes', op);
},
group(runner) {
return runner.getEntryGroupName();
},
totp(runner, op) {
return runner.getOtp(op);
},
s(runner, op) {
return runner.getEntryFieldKeys(op.arg, op);
},
'dt_simple'(runner) {
return runner.dt('simple');
},
'dt_year'(runner) {
return runner.dt('Y');
},
'dt_month'(runner) {
return runner.dt('M');
},
'dt_day'(runner) {
return runner.dt('D');
},
'dt_hour'(runner) {
return runner.dt('h');
},
'dt_minute'(runner) {
return runner.dt('m');
},
'dt_second'(runner) {
return runner.dt('s');
},
'dt_utc_simple'(runner) {
return runner.udt('simple');
},
'dt_utc_year'(runner) {
return runner.udt('Y');
},
'dt_utc_month'(runner) {
return runner.udt('M');
},
'dt_utc_day'(runner) {
return runner.udt('D');
},
'dt_utc_hour'(runner) {
return runner.udt('h');
},
'dt_utc_minute'(runner) {
return runner.udt('m');
},
'dt_utc_second'(runner) {
return runner.udt('s');
}
};
AutoTypeRunner.prototype.resolve = function (entry, context, callback) {
this.entry = entry;
this.context = context;
try {
this.resolveOps(this.ops);
if (!this.pendingResolvesCount) {
callback();
} else {
this.resolveCallback = callback;
}
} catch (e) {
return callback(e);
}
};
AutoTypeRunner.prototype.resolveOps = function (ops) {
for (let i = 0, len = ops.length; i < len; i++) {
const op = ops[i];
if (op.type === 'group') {
this.resolveOps(op.value);
} else {
this.resolveOp(op);
}
}
};
AutoTypeRunner.prototype.resolveOp = function (op) {
if (op.value.length === 1 && !op.sep) {
// {x}
op.type = 'text';
return;
}
if (op.value.length === 1 && op.sep === ' ') {
// {x 3}
op.type = 'text';
const ch = op.value;
let text = ch;
const len = +op.arg;
while (text.length < len) {
text += ch;
}
op.value = text;
return;
}
const lowerValue = op.value.toLowerCase();
const key = AutoTypeRunner.Keys[lowerValue];
if (key) {
if (op.sep === ' ' && +op.arg > 0) {
// {TAB 3}
op.type = 'group';
op.value = [];
const count = +op.arg;
for (let i = 0; i < count; i++) {
op.value.push({ type: 'key', value: key });
}
} else {
// {TAB}
op.type = 'key';
op.value = key;
}
return;
}
if (this.context?.resolved?.[lowerValue]) {
op.type = 'text';
op.value = this.context.resolved[lowerValue];
return;
}
const substitution = AutoTypeRunner.Substitutions[lowerValue];
if (substitution) {
// {title}
op.type = 'text';
op.value = substitution(this, op);
if (op.value === AutoTypeRunner.PendingResolve) {
this.pendingResolvesCount++;
}
return;
}
if (!this.tryParseCommand(op)) {
throw 'Bad op: ' + op.value;
}
};
AutoTypeRunner.prototype.tryParseCommand = function (op) {
switch (op.value.toLowerCase()) {
case 'clearfield':
// {CLEARFIELD}
op.type = 'group';
op.value = [
{ type: 'key', value: 'end' },
{ type: 'key', value: 'home', mod: { '+': true } },
{ type: 'key', value: 'bs' }
];
return true;
case 'vkey':
// {VKEY 10} {VKEY 0x1F}
op.type = 'key';
op.value = parseInt(op.arg);
if (isNaN(op.value) || op.value < 0) {
throw 'Bad vkey: ' + op.arg;
}
return true;
case 'delay':
// {DELAY 5} {DELAY=5}
op.type = 'cmd';
op.value = op.sep === '=' ? 'setDelay' : 'wait';
if (!op.arg) {
throw 'Delay requires milliseconds count';
}
if (isNaN(+op.arg)) {
throw 'Bad delay: ' + op.arg;
}
if (op.arg < 0) {
throw 'Delay requires positive interval';
}
op.arg = +op.arg;
return true;
default:
return false;
}
};
AutoTypeRunner.prototype.getEntryFieldKeys = function (field, op) {
if (!field || !this.entry) {
return '';
}
const value = this.entry.getFieldValue(field);
if (!value) {
return '';
}
if (value.isProtected) {
op.type = 'group';
const ops = [];
value.forEachChar((ch) => {
if (ch === 10 || ch === 13) {
ops.push({ type: 'key', value: 'enter' });
} else {
ops.push({ type: 'text', value: String.fromCharCode(ch) });
}
});
return ops;
} else {
const parts = value.split(/[\r\n]/g);
if (parts.length === 1) {
return value;
}
op.type = 'group';
const partsOps = [];
parts.forEach((part) => {
if (partsOps.length) {
partsOps.push({ type: 'key', value: 'enter' });
}
if (part) {
partsOps.push({ type: 'text', value: part });
}
});
return partsOps;
}
};
AutoTypeRunner.prototype.getEntryGroupName = function () {
return this.entry && this.entry.group.title;
};
AutoTypeRunner.prototype.dt = function (part) {
switch (part) {
case 'simple':
return (
this.dt('Y') +
this.dt('M') +
this.dt('D') +
this.dt('h') +
this.dt('m') +
this.dt('s')
);
case 'Y':
return this.now.getFullYear().toString();
case 'M':
return StringFormat.pad(this.now.getMonth() + 1, 2);
case 'D':
return StringFormat.pad(this.now.getDate(), 2);
case 'h':
return StringFormat.pad(this.now.getHours(), 2);
case 'm':
return StringFormat.pad(this.now.getMinutes(), 2);
case 's':
return StringFormat.pad(this.now.getSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
AutoTypeRunner.prototype.udt = function (part) {
switch (part) {
case 'simple':
return (
this.udt('Y') +
this.udt('M') +
this.udt('D') +
this.udt('h') +
this.udt('m') +
this.udt('s')
);
case 'Y':
return this.now.getUTCFullYear().toString();
case 'M':
return StringFormat.pad(this.now.getUTCMonth() + 1, 2);
case 'D':
return StringFormat.pad(this.now.getUTCDate(), 2);
case 'h':
return StringFormat.pad(this.now.getUTCHours(), 2);
case 'm':
return StringFormat.pad(this.now.getUTCMinutes(), 2);
case 's':
return StringFormat.pad(this.now.getUTCSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
AutoTypeRunner.prototype.getOtp = function (op) {
if (!this.entry) {
return '';
}
this.entry.initOtpGenerator();
if (!this.entry.otpGenerator) {
return '';
}
this.entry.otpGenerator.next((err, otp) => {
this.pendingResolved(op, otp, err);
});
return AutoTypeRunner.PendingResolve;
};
AutoTypeRunner.prototype.pendingResolved = function (op, value, error) {
const wasPending = op.value === AutoTypeRunner.PendingResolve;
if (value) {
op.value = value;
}
if (!wasPending) {
return;
}
this.pendingResolvesCount--;
if ((this.pendingResolvesCount === 0 || error) && this.resolveCallback) {
this.resolveCallback(error);
this.resolveCallback = null;
}
};
AutoTypeRunner.prototype.obfuscate = function () {
this.obfuscateOps(this.ops);
};
AutoTypeRunner.prototype.obfuscateOps = function (ops) {
for (let i = 0, len = ops.length; i < len; i++) {
const op = ops[i];
if (op.mod) {
continue;
}
if (op.type === 'text') {
this.obfuscateOp(op);
} else if (op.type === 'group') {
const onlyText = op.value.every((grOp) => grOp.type === 'text' && !grOp.mod);
if (onlyText) {
this.obfuscateOp(op);
} else {
this.obfuscateOps(op.value);
}
}
}
};
AutoTypeRunner.prototype.obfuscateOp = function (op) {
let letters = [];
if (op.type === 'text') {
if (!op.value || op.value.length <= 1) {
return;
}
letters = op.value.split('');
} else {
op.value.forEach((grOp) => letters.push(...grOp.value.split('')));
}
if (letters.length <= 1) {
return;
}
const obfuscator = new AutoTypeObfuscator(letters);
op.value = obfuscator.obfuscate();
op.type = 'group';
};
AutoTypeRunner.prototype.run = function (callback, windowId) {
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
this.emitterState = {
callback,
stack: [],
ops: this.ops,
opIx: 0,
mod: {},
activeMod: {},
finished: null
};
this.emitter.begin();
};
AutoTypeRunner.prototype.emitNext = function (err) {
if (err) {
this.emitterState.finished = true;
this.emitterState.callback(err);
return;
}
if (this.emitterState.finished) {
this.emitterState.callback();
return;
}
this.resetEmitterMod(this.emitterState.mod);
if (this.emitterState.opIx >= this.emitterState.ops.length) {
const state = this.emitterState.stack.pop();
if (state) {
Object.assign(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
this.emitNext();
} else {
this.resetEmitterMod({});
this.emitterState.finished = true;
emitterLogger.debug('waitComplete');
this.emitter.waitComplete();
}
return;
}
const op = this.emitterState.ops[this.emitterState.opIx];
if (op.type === 'group') {
if (op.mod) {
this.setEmitterMod(op.mod);
}
this.emitterState.stack.push({
ops: this.emitterState.ops,
opIx: this.emitterState.opIx + 1,
mod: { ...this.emitterState.mod }
});
Object.assign(this.emitterState, {
ops: op.value,
opIx: 0,
mod: { ...this.emitterState.activeMod }
});
this.emitNext();
return;
}
this.emitterState.opIx++;
if (op.mod) {
this.setEmitterMod(op.mod);
}
switch (op.type) {
case 'text':
emitterLogger.debug('text', op.value);
if (op.value) {
this.emitter.text(op.value);
} else {
this.emitNext();
}
break;
case 'key':
emitterLogger.debug('key', op.value);
this.emitter.key(op.value);
break;
case 'cmd': {
const method = this.emitter[op.value];
if (!method) {
throw 'Bad cmd: ' + op.value;
}
emitterLogger.debug(op.value, op.arg);
method.call(this.emitter, op.arg);
break;
}
default:
throw 'Bad op: ' + op.type;
}
};
AutoTypeRunner.prototype.setEmitterMod = function (addedMod) {
Object.keys(addedMod).forEach(function (mod) {
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
emitterLogger.debug('mod', mod, true);
this.emitter.setMod(mod, true);
this.emitterState.activeMod[mod] = true;
}
}, this);
};
AutoTypeRunner.prototype.resetEmitterMod = function (targetState) {
Object.keys(this.emitterState.activeMod).forEach(function (mod) {
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
emitterLogger.debug('mod', mod, false);
this.emitter.setMod(mod, false);
delete this.emitterState.activeMod[mod];
}
}, this);
};
export { AutoTypeRunner };
-312
View File
@@ -1,312 +0,0 @@
import { Events } from 'framework/events';
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
import { AutoTypeParser } from 'auto-type/auto-type-parser';
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model';
import { AppModel } from 'models/app-model';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { Links } from 'const/links';
import { SelectEntryView } from 'views/select/select-entry-view';
const logger = new Logger('auto-type');
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
const AutoType = {
enabled: !!(Launcher && Launcher.autoTypeSupported),
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
selectEntryView: false,
running: false,
init() {
if (!this.enabled) {
return;
}
Events.on('auto-type', (e) => this.handleEvent(e));
},
handleEvent(e) {
const entry = (e && e.entry) || null;
const sequence = (e && e.sequence) || null;
const context = (e && e.context) || null;
logger.debug('Auto type event', entry);
if (this.running) {
logger.debug('Already running, skipping event');
return;
}
if (entry) {
this.hideWindow(() => {
this.runAndHandleResult({ entry, sequence, context });
});
} else {
if (this.selectEntryView) {
return;
}
if (Launcher.isAppFocused()) {
return Alerts.error({
header: Locale.autoTypeError,
body: Locale.autoTypeErrorGlobal,
skipIfAlertDisplayed: true
});
}
this.selectEntryAndRun();
}
},
runAndHandleResult(result, windowId) {
this.run(result, windowId, (err) => {
if (err) {
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
let link;
if (err.keyPressFailed && Features.isMac) {
body = Locale.autoTypeErrorAccessibilityMacOS;
link = Links.AutoTypeMacOS;
}
Alerts.error({
header: Locale.autoTypeError,
body,
link
});
}
});
if (AppSettingsModel.lockOnAutoType) {
Events.emit('lock-workspace');
}
},
run(result, windowId, callback) {
this.running = true;
const sequence = result.sequence || result.entry.getEffectiveAutoTypeSeq();
const context = result.context;
logger.debug('Start', sequence);
const ts = logger.ts();
try {
const parser = new AutoTypeParser(sequence);
const runner = parser.parse();
logger.debug('Parsed', this.printOps(runner.ops));
runner.resolve(result.entry, context, (err) => {
if (err) {
this.running = false;
logger.error('Resolve error', err);
return callback && callback(err);
}
logger.debug('Resolved', this.printOps(runner.ops));
if (result.entry.autoTypeObfuscation) {
try {
runner.obfuscate();
} catch (e) {
this.running = false;
logger.error('Obfuscate error', e);
return callback && callback(e);
}
logger.debug('Obfuscated');
}
runner.run((err) => {
this.running = false;
if (err) {
logger.error('Run error', err);
return callback && callback(err);
}
logger.debug('Complete', logger.ts(ts));
return callback && callback();
}, windowId);
});
} catch (ex) {
this.running = false;
logger.error('Parse error', ex);
return callback && callback(ex);
}
},
validate(entry, sequence, callback) {
try {
const parser = new AutoTypeParser(sequence);
const runner = parser.parse();
runner.resolve(entry, null, callback);
} catch (ex) {
return callback(ex);
}
},
printOps(ops) {
return '[' + ops.map(this.printOp, this).join(',') + ']';
},
printOp(op) {
const mod = op.mod ? Object.keys(op.mod).join('') : '';
if (op.type === 'group') {
return mod + this.printOps(op.value);
}
if (op.type === 'text') {
let value = op.value;
if (!clearTextAutoTypeLog) {
value = value.replace(/./g, '*');
}
return mod + value;
}
return mod + op.type + ':' + op.value;
},
hideWindow(callback) {
logger.debug('Hide window');
if (Launcher.isAppFocused()) {
Launcher.hideApp();
setTimeout(callback, Timeouts.AutoTypeAfterHide);
} else {
callback();
}
},
getActiveWindowInfo(callback) {
logger.debug('Getting window info');
const helper = new AutoTypeHelper();
return helper.getActiveWindowInfo((err, windowInfo) => {
if (err) {
logger.error('Error getting window info', err);
} else {
if (!windowInfo.url) {
// try to find a URL in the title
const urlMatcher = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\\+.~#?&\/=]*)/;
const urlMatches = urlMatcher.exec(windowInfo.title);
windowInfo.url = urlMatches && urlMatches.length > 0 ? urlMatches[0] : null;
}
logger.debug('Window info', windowInfo.id, windowInfo.title, windowInfo.url);
}
return callback(err, windowInfo);
});
},
activeWindowMatches(windowInfo, callback) {
if (!windowInfo || !windowInfo.id) {
logger.debug('Skipped active window check because window id is unknown');
return callback(true);
}
this.getActiveWindowInfo((err, activeWindowInfo) => {
if (!activeWindowInfo) {
logger.debug('Error during active window check, something is wrong', err);
return callback(false);
}
if (activeWindowInfo.id !== windowInfo.id && !this.supportsEventsWithWindowId) {
logger.info(
`Active window doesn't match: ID is different. ` +
`Expected ${windowInfo.id}, got ${activeWindowInfo.id}`
);
return callback(false, activeWindowInfo);
}
if (activeWindowInfo.url !== windowInfo.url && !this.supportsEventsWithWindowId) {
logger.info(
`Active window doesn't match: url is different. ` +
`Expected "${windowInfo.url}", got "${activeWindowInfo.url}"`
);
return callback(false, activeWindowInfo);
}
logger.info('Active window matches');
callback(true, activeWindowInfo);
});
},
selectEntryAndRun() {
this.getActiveWindowInfo(async (e, windowInfo) => {
const filter = new SelectEntryFilter(
windowInfo,
AppModel.instance,
AppModel.instance.files,
{
autoType: true
}
);
const evt = { filter, windowInfo };
if (!AppModel.instance.files.hasOpenFiles()) {
logger.debug('auto-type event delayed');
this.focusMainWindow();
try {
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
} catch {
logger.debug('auto-type event canceled');
return;
}
if (this.selectEntryView) {
this.selectEntryView.show();
}
}
logger.debug('processing auto-type event');
this.processEventWithFilter(evt);
});
},
focusMainWindow() {
if (!Launcher.isAppFocused()) {
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
}
},
processEventWithFilter(evt) {
let entries = evt.filter.getEntries();
if (entries.length === 1 && AppSettingsModel.directAutotype) {
this.hideWindow(() => {
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
});
return;
}
this.focusMainWindow();
if (entries.length === 0) {
if (evt.filter.useUrl) {
evt.filter.useUrl = false;
if (evt.filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
evt.filter.useTitle = true;
}
}
entries = evt.filter.getEntries();
if (entries.length === 0 && evt.filter.useTitle) {
evt.filter.useTitle = false;
}
}
const humanReadableTarget = evt.filter.title || evt.filter.url;
const topMessage = humanReadableTarget
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
: Locale.autoTypeMsgNoWindow;
this.selectEntryView = new SelectEntryView({
isAutoType: true,
itemOptions: true,
filter: evt.filter,
topMessage
});
this.selectEntryView.on('result', (result) => {
logger.debug('Entry selected', result);
this.selectEntryView.off('result');
this.selectEntryView.remove();
this.selectEntryView = null;
this.hideWindow(() => {
if (result?.entry) {
this.activeWindowMatches(evt.windowInfo, (matches, activeWindowInfo) => {
if (matches) {
this.runAndHandleResult(result, evt.windowInfo.id);
}
});
}
});
});
this.selectEntryView.render();
this.selectEntryView.on('show-open-files', async () => {
this.selectEntryView.hide();
try {
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
} catch {
this.selectEntryView.emit('result', undefined);
return;
}
this.selectEntryView.show();
this.selectEntryView.render();
});
}
};
export { AutoType };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { EntryModel } from 'models/entry-model';
class EntryCollection extends Collection {
static model = EntryModel;
}
export { EntryCollection };
@@ -1,24 +0,0 @@
import { Collection } from 'framework/collection';
import { Model } from 'framework/model';
class FileCollection extends Collection {
static model = Model;
hasOpenFiles() {
return this.some((file) => file.active);
}
hasUnsavedFiles() {
return this.some((file) => file.modified);
}
hasDirtyFiles() {
return this.some((file) => file.dirty);
}
getByName(name) {
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
}
}
export { FileCollection };
@@ -1,39 +0,0 @@
import { Collection } from 'framework/collection';
import { SettingsStore } from 'comp/settings/settings-store';
import { FileInfoModel } from 'models/file-info-model';
class FileInfoCollection extends Collection {
static model = FileInfoModel;
load() {
return SettingsStore.load('file-info').then((data) => {
if (data) {
for (const item of data) {
this.push(new FileInfoModel(item));
}
}
});
}
save() {
SettingsStore.save('file-info', this);
}
getMatch(storage, name, path) {
return this.find((fi) => {
return (
(fi.storage || '') === (storage || '') &&
(fi.name || '') === (name || '') &&
(fi.path || '') === (path || '')
);
});
}
getByName(name) {
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
}
}
const instance = new FileInfoCollection();
export { instance as FileInfoCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { GroupModel } from 'models/group-model';
class GroupCollection extends Collection {
static model = GroupModel;
}
export { GroupCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuItemModel } from 'models/menu/menu-item-model';
class MenuItemCollection extends Collection {
static model = MenuItemModel;
}
export { MenuItemCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuOptionModel } from 'models/menu/menu-option-model';
class MenuOptionCollection extends Collection {
static model = MenuOptionModel;
}
export { MenuOptionCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuSectionModel } from 'models/menu/menu-section-model';
class MenuSectionCollection extends Collection {
static model = MenuSectionModel;
}
export { MenuSectionCollection };
@@ -1,52 +0,0 @@
import { Collection } from 'framework/collection';
import { Model } from 'framework/model';
import { Comparators } from 'util/data/comparators';
class SearchResultCollection extends Collection {
static model = Model;
comparators = {
'none': null,
'title': Comparators.stringComparator('title', true),
'-title': Comparators.stringComparator('title', false),
'website': Comparators.stringComparator('url', true),
'-website': Comparators.stringComparator('url', false),
'user': Comparators.stringComparator('user', true),
'-user': Comparators.stringComparator('user', false),
'created': Comparators.dateComparator('created', true),
'-created': Comparators.dateComparator('created', false),
'updated': Comparators.dateComparator('updated', true),
'-updated': Comparators.dateComparator('updated', false),
'-attachments': (x, y) => {
return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y));
},
'-rank': Comparators.rankComparator().bind(this)
};
defaultComparator = 'title';
entryFilter = null;
constructor(models, options) {
super(models);
const comparatorName = (options && options.comparator) || this.defaultComparator;
this.comparator = this.comparators[comparatorName];
}
sortEntries(comparator, filter) {
this.entryFilter = filter;
this.comparator = this.comparators[comparator] || this.comparators[this.defaultComparator];
this.sort();
}
attachmentSortVal(entry) {
const att = entry.attachments;
let str = att.length ? String.fromCharCode(64 + att.length) : 'Z';
if (att[0]) {
str += att[0].title;
}
return str;
}
}
export { SearchResultCollection };
@@ -1,81 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
const AppRightsChecker = {
AppPath: '/Applications/KeeWeb.app',
init() {
if (!Features.isDesktop || !Features.isMac) {
return;
}
if (AppSettingsModel.skipFolderRightsWarning) {
return;
}
if (!Launcher.getAppPath().startsWith(this.AppPath)) {
return;
}
this.needRunInstaller((needRun) => {
if (needRun) {
this.showAlert();
this.runInstaller();
}
});
},
needRunInstaller(callback) {
Launcher.statFile(this.AppPath, (stat) => {
const folderIsRoot = stat && stat.uid === 0;
callback(!folderIsRoot);
});
},
showAlert() {
const command = 'sudo chown -R root ' + this.AppPath;
this.alert = Alerts.alert({
icon: 'lock',
header: Locale.appRightsAlert,
body:
Locale.appRightsAlertBody1.replace('{}', this.AppPath) +
'\n' +
Locale.appRightsAlertBody2,
pre: command,
buttons: [
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
Alerts.buttons.ok
],
success: (result) => {
if (result === 'skip') {
this.dontAskAnymore();
}
this.alert = null;
}
});
},
runInstaller() {
Launcher.spawn({
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
args: ['--install'],
complete: () => {
this.needRunInstaller((needRun) => {
if (this.alert && !needRun) {
this.alert.closeWithResult('cancel');
}
});
}
});
},
dontAskAnymore() {
this.needRunInstaller((needRun) => {
if (needRun) {
AppSettingsModel.skipFolderRightsWarning = true;
}
});
}
};
export { AppRightsChecker };
@@ -1,194 +0,0 @@
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { YubiKey } from 'comp/app/yubikey';
import { UsbListener } from 'comp/app/usb-listener';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { Timeouts } from 'const/timeouts';
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
const logger = new Logger('chal-resp');
const ChalRespCalculator = {
cache: {},
getCacheKey(params) {
return `${params.vid}:${params.pid}:${params.serial}:${params.slot}`;
},
build(params) {
if (!params) {
return null;
}
return (challenge) => {
return new Promise((resolve, reject) => {
challenge = Buffer.from(challenge);
const hexChallenge = challenge.toString('hex');
const cacheKey = this.getCacheKey(params);
const respFromCache = this.cache[cacheKey]?.[hexChallenge];
if (respFromCache) {
logger.debug('Found ChalResp in cache');
return resolve(Buffer.from(respFromCache, 'hex'));
}
if (!AppSettingsModel.enableUsb) {
logger.debug('USB is disabled');
Alerts.error({
header: Locale.yubiKeyDisabledErrorHeader,
body: Locale.yubiKeyDisabledErrorBody,
complete() {
const err = new Error(Locale.yubiKeyDisabledErrorHeader);
err.userCanceled = true;
err.ykError = true;
reject(err);
}
});
return;
}
logger.debug('Calculating ChalResp using a YubiKey', params);
this._calc(params, challenge, (err, response) => {
if (err) {
return reject(err);
}
resolve(response);
});
});
};
},
_calc(params, challenge, callback) {
let touchAlert = null;
let userCanceled = false;
YubiKey.calculateChalResp(params, challenge, (err, response) => {
if (userCanceled) {
userCanceled = false;
return;
}
if (touchAlert) {
touchAlert.closeWithoutResult();
touchAlert = null;
}
if (err) {
if (err.noKey) {
logger.info('YubiKey ChalResp: no key');
this._showNoKeyAlert(params.serial, (err) => {
if (err) {
return callback(err);
}
this._calc(params, challenge, callback);
});
return;
} else if (err.touchRequested) {
logger.info('YubiKey ChalResp: touch requested');
touchAlert = this._showTouchAlert(params.serial, (err) => {
touchAlert = null;
userCanceled = true;
logger.info('YubiKey ChalResp canceled');
YubiKey.cancelChalResp();
callback(err);
});
return;
} else {
logger.error('YubiKey ChalResp error', err);
}
return callback(err);
}
const cacheKey = this.getCacheKey(params);
if (!this.cache[cacheKey]) {
this.cache[cacheKey] = {};
}
const hexChallenge = challenge.toString('hex');
this.cache[cacheKey][hexChallenge] = response.toString('hex');
logger.info('Calculated YubiKey ChalResp');
callback(null, response);
});
},
_showNoKeyAlert(serial, callback) {
Launcher.showMainWindow();
let noKeyAlert = null;
let deviceEnumerationTimer;
const onUsbDevicesChanged = () => {
if (!UsbListener.attachedYubiKeys) {
return;
}
deviceEnumerationTimer = setTimeout(() => {
YubiKey.list((err, list) => {
if (err) {
logger.error('YubiKey list error', err);
return;
}
const isAttached = list.some((yk) => yk.serial === serial);
logger.info(isAttached ? 'YubiKey found' : 'YubiKey not found');
if (isAttached) {
Events.off('usb-devices-changed', onUsbDevicesChanged);
if (noKeyAlert) {
noKeyAlert.closeWithoutResult();
}
callback();
}
});
}, Timeouts.ExternalDeviceAfterReconnect);
};
Events.on('usb-devices-changed', onUsbDevicesChanged);
noKeyAlert = Alerts.alert({
header: Locale.yubiKeyNoKeyHeader,
body: Locale.yubiKeyNoKeyBody.replace('{}', serial),
buttons: [Alerts.buttons.cancel],
icon: 'usb-token',
cancel: () => {
logger.info('No key alert closed');
clearTimeout(deviceEnumerationTimer);
Events.off('usb-devices-changed', onUsbDevicesChanged);
const err = new Error('User canceled the YubiKey no key prompt');
err.userCanceled = true;
err.ykError = true;
return callback(err);
}
});
},
_showTouchAlert(serial, callback) {
Launcher.showMainWindow();
return Alerts.alert({
header: Locale.yubiKeyTouchRequestedHeader,
body: Locale.yubiKeyTouchRequestedBody.replace('{}', serial),
buttons: [Alerts.buttons.cancel],
icon: 'usb-token',
cancel: () => {
logger.info('Touch alert closed');
const err = new Error('User canceled the YubiKey touch prompt');
err.userCanceled = true;
err.ykError = true;
return callback(err);
}
});
},
clearCache(params) {
delete this.cache[this.getCacheKey(params)];
}
};
export { ChalRespCalculator };
-5
View File
@@ -1,5 +0,0 @@
const DragDropInfo = {
dragObject: null
};
export { DragDropInfo };
-102
View File
@@ -1,102 +0,0 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { UrlFormat } from 'util/formatting/url-format';
const ChooserAppKey = 'qp7ctun6qt5n9d6';
const DropboxChooser = function (callback) {
this.cb = callback;
this.onMessage = this.onMessage.bind(this);
};
DropboxChooser.prototype.callback = function (err, res) {
if (this.cb) {
this.cb(err, res);
}
this.cb = null;
};
DropboxChooser.prototype.choose = function () {
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
const url = this.buildUrl();
this.popup = window.open(url, 'dropbox', windowFeatures);
if (!this.popup) {
return this.callback('Failed to open window');
}
window.addEventListener('message', this.onMessage);
this.closeInt = setInterval(this.checkClose.bind(this), 200);
};
DropboxChooser.prototype.buildUrl = function () {
return UrlFormat.makeUrl('https://www.dropbox.com/chooser', {
origin: window.location.protocol + '//' + window.location.host,
'app_key': AppSettingsModel.dropboxAppKey || ChooserAppKey,
'link_type': 'direct',
trigger: 'js',
multiselect: 'false',
extensions: '',
folderselect: 'false',
iframe: 'false',
version: 2
});
};
DropboxChooser.prototype.onMessage = function (e) {
if (e.source !== this.popup || e.origin !== 'https://www.dropbox.com') {
return;
}
const data = JSON.parse(e.data);
switch (data.method) {
case 'origin_request':
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
break;
case 'files_selected':
this.popup.close();
this.success(data.params);
break;
case 'close_dialog':
this.popup.close();
break;
case 'web_session_error':
case 'web_session_unlinked':
this.callback(data.method);
break;
case 'resize':
this.popup.resize(data.params);
break;
case 'error':
this.callback(data.params);
break;
}
};
DropboxChooser.prototype.checkClose = function () {
if (this.popup.closed) {
clearInterval(this.closeInt);
window.removeEventListener('message', this.onMessage);
if (!this.result) {
this.callback('closed');
}
}
};
DropboxChooser.prototype.success = function (params) {
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
return this.callback('bad result');
}
this.result = params[0];
this.readFile(this.result.link);
};
DropboxChooser.prototype.readFile = function (url) {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
this.callback(null, { name: this.result.name, data: xhr.response });
});
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
};
export { DropboxChooser };
-29
View File
@@ -1,29 +0,0 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { RuntimeDataModel } from 'models/runtime-data-model';
const ExportApi = {
settings: {
get(key) {
return key ? AppSettingsModel[key] : { ...AppSettingsModel };
},
set(key, value) {
AppSettingsModel[key] = value;
},
del(key) {
delete AppSettingsModel[key];
}
},
runtimeData: {
get(key) {
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
},
set(key, value) {
RuntimeDataModel[key] = value;
},
del(key) {
delete RuntimeDataModel[key];
}
}
};
export { ExportApi };
-18
View File
@@ -1,18 +0,0 @@
import { Logger } from 'util/logger';
const logger = new Logger(
'focus-manager',
undefined,
localStorage.debugFocusManager ? Logger.Level.Debug : Logger.Level.Info
);
const FocusManager = {
modal: null,
setModal(modal) {
this.modal = modal;
logger.debug('Set modal', modal);
}
};
export { FocusManager };
-177
View File
@@ -1,177 +0,0 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { Locale } from 'util/locale';
const GeneratorPresets = {
get defaultPreset() {
return {
name: 'Default',
title: Locale.genPresetDefault,
length: 16,
upper: true,
lower: true,
digits: true
};
},
get browserExtensionPreset() {
return {
name: 'BrowserExtension',
length: 20,
upper: true,
lower: true,
special: true,
brackets: true,
ambiguous: true
};
},
get builtIn() {
return [
this.defaultPreset,
{
name: 'Pronounceable',
title: Locale.genPresetPronounceable,
length: 10,
lower: true,
upper: true
},
{
name: 'Med',
title: Locale.genPresetMed,
length: 16,
upper: true,
lower: true,
digits: true,
special: true,
brackets: true,
ambiguous: true
},
{
name: 'Long',
title: Locale.genPresetLong,
length: 32,
upper: true,
lower: true,
digits: true
},
{ name: 'Pin4', title: Locale.genPresetPin4, length: 4, digits: true },
{
name: 'Mac',
title: Locale.genPresetMac,
length: 17,
include: '0123456789ABCDEF',
pattern: 'XX-'
},
{
name: 'Hash128',
title: Locale.genPresetHash128,
length: 32,
include: '0123456789abcdef'
},
{
name: 'Hash256',
title: Locale.genPresetHash256,
length: 64,
include: '0123456789abcdef'
}
];
},
get all() {
let presets = this.builtIn;
presets.forEach((preset) => {
preset.builtIn = true;
});
const setting = AppSettingsModel.generatorPresets;
if (setting) {
if (setting.user) {
presets = presets.concat(setting.user.map((item) => ({ ...item })));
}
let hasDefault = false;
presets.forEach((preset) => {
if (setting.disabled && setting.disabled[preset.name]) {
preset.disabled = true;
}
if (setting.default === preset.name) {
hasDefault = true;
preset.default = true;
}
});
if (!hasDefault) {
presets[0].default = true;
}
}
return presets;
},
get enabled() {
const allPresets = this.all.filter((preset) => !preset.disabled);
if (!allPresets.length) {
allPresets.push(this.defaultPreset);
}
return allPresets;
},
getOrCreateSetting() {
let setting = AppSettingsModel.generatorPresets;
if (!setting) {
setting = { user: [] };
}
return setting;
},
add(preset) {
const setting = this.getOrCreateSetting();
if (preset.name && !setting.user.filter((p) => p.name === preset.name).length) {
setting.user.push(preset);
this.save(setting);
}
},
remove(name) {
const setting = this.getOrCreateSetting();
setting.user = setting.user.filter((p) => p.name !== name);
this.save(setting);
},
setPreset(name, props) {
const setting = this.getOrCreateSetting();
const preset = setting.user.filter((p) => p.name === name)[0];
if (preset) {
Object.assign(preset, props);
this.save(setting);
}
},
setDisabled(name, disabled) {
const setting = this.getOrCreateSetting();
if (disabled) {
if (!setting.disabled) {
setting.disabled = {};
}
setting.disabled[name] = true;
} else {
if (setting.disabled) {
delete setting.disabled[name];
}
}
this.save(setting);
},
setDefault(name) {
const setting = this.getOrCreateSetting();
if (name) {
setting.default = name;
} else {
delete setting.default;
}
this.save(setting);
},
save(setting) {
AppSettingsModel.set({ generatorPresets: undefined }, { silent: true });
AppSettingsModel.generatorPresets = setting;
}
};
export { GeneratorPresets };
@@ -1,37 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
const logger = new Logger('online-password-checker');
const exposedPasswords = {};
function checkIfPasswordIsExposedOnline(password) {
if (!password || !password.isProtected || !password.byteLength) {
return false;
}
const saltedValue = password.saltedValue();
const cached = exposedPasswords[saltedValue];
if (cached !== undefined) {
return cached;
}
const passwordBytes = password.getBinary();
return crypto.subtle
.digest({ name: 'SHA-1' }, passwordBytes)
.then((sha1) => {
kdbxweb.ByteUtils.zeroBuffer(passwordBytes);
sha1 = kdbxweb.ByteUtils.bytesToHex(sha1).toUpperCase();
const shaFirst = sha1.substr(0, 5);
return fetch(`https://api.pwnedpasswords.com/range/${shaFirst}`)
.then((response) => response.text())
.then((response) => {
const isPresent = response.includes(sha1.substr(5));
exposedPasswords[saltedValue] = isPresent;
return isPresent;
});
})
.catch((e) => {
logger.error('Error checking password online', e);
});
}
export { checkIfPasswordIsExposedOnline };
@@ -1,99 +0,0 @@
import { SearchResultCollection } from 'collections/search-result-collection';
import { Ranking } from 'util/data/ranking';
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
class SelectEntryFilter {
constructor(windowInfo, appModel, files, filterOptions) {
this.title = windowInfo.title;
this.useTitle = !!windowInfo.title && !windowInfo.url;
this.url = windowInfo.url;
this.useUrl = !!windowInfo.url;
this.subdomains = true;
this.text = '';
this.appModel = appModel;
this.files = files;
this.filterOptions = filterOptions;
}
getEntries() {
const filter = {
text: this.text,
...this.filterOptions
};
let entries = this.appModel
.getEntriesByFilter(filter, this.files)
.map((e) => [e, this._getEntryRank(e)]);
if (this.useUrl || this.useTitle) {
entries = entries.filter((e) => e[1]);
}
entries = entries.sort((x, y) =>
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
);
entries = entries.map((p) => p[0]);
return new SearchResultCollection(entries, { comparator: 'none' });
}
_getEntryRank(entry) {
let titleRank = 0;
let urlRank = 0;
if (this.useTitle && this.title && entry.title) {
titleRank = Ranking.getStringRank(entry.title.toLowerCase(), this.title.toLowerCase());
if (!titleRank) {
return 0;
}
}
if (this.useUrl && this.url) {
const searchUrlLower = this.url.toLowerCase();
const searchUrlParts = urlPartsRegex.exec(searchUrlLower);
for (const url of entry.getAllUrls()) {
const entryUrlParts = urlPartsRegex.exec(url.toLowerCase());
if (entryUrlParts) {
const [, scheme, domain, path] = entryUrlParts;
const [, searchScheme, searchDomain, searchPath] = searchUrlParts;
if (
domain === searchDomain ||
(this.subdomains && searchDomain.indexOf('.' + domain) > 0)
) {
if (domain === searchDomain) {
urlRank += 20;
} else {
urlRank += 10;
}
if (path === searchPath) {
urlRank += 10;
} else if (path && searchPath) {
if (path.lastIndexOf(searchPath, 0) === 0) {
urlRank += 5;
} else if (searchPath.lastIndexOf(path, 0) === 0) {
urlRank += 3;
}
}
if (scheme === searchScheme) {
urlRank += 1;
}
}
}
}
if (entry.searchText.includes(searchUrlLower)) {
// the url is in some field; include it
urlRank += 5;
}
}
if (this.useTitle && !titleRank) {
return 0;
}
if (this.useUrl && !urlRank) {
return 0;
}
return titleRank + urlRank;
}
}
export { SelectEntryFilter };
-138
View File
@@ -1,138 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Keys } from 'const/keys';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
let allowedKeys;
function getAllowedKeys() {
if (!allowedKeys) {
allowedKeys = {};
for (const [name, code] of Object.entries(Keys)) {
const keyName = name.replace('DOM_VK_', '');
if (/^([0-9A-Z]|F\d{1,2})$/.test(keyName)) {
allowedKeys[code] = keyName;
}
}
}
return allowedKeys;
}
const globalShortcuts = {
copyPassword: { mac: 'Ctrl+Alt+C', all: 'Shift+Alt+C' },
copyUser: { mac: 'Ctrl+Alt+B', all: 'Shift+Alt+B' },
copyUrl: { mac: 'Ctrl+Alt+U', all: 'Shift+Alt+U' },
copyOtp: {},
autoType: { mac: 'Ctrl+Alt+T', all: 'Shift+Alt+T' },
restoreApp: {}
};
const Shortcuts = {
keyEventToShortcut(event) {
const modifiers = [];
if (event.ctrlKey) {
modifiers.push('Ctrl');
}
if (event.altKey) {
modifiers.push('Alt');
}
if (event.shiftKey) {
modifiers.push('Shift');
}
if (Features.isMac && event.metaKey) {
modifiers.push('Meta');
}
const keyName = getAllowedKeys()[event.which];
return {
value: modifiers.join('+') + '+' + (keyName || '…'),
valid: modifiers.length > 0 && !!keyName
};
},
presentShortcut(shortcutValue, formatting) {
if (!shortcutValue) {
return '-';
}
return shortcutValue
.split(/\+/g)
.map((part) => {
switch (part) {
case 'Ctrl':
return this.ctrlShortcutSymbol(formatting);
case 'Alt':
return this.altShortcutSymbol(formatting);
case 'Shift':
return this.shiftShortcutSymbol(formatting);
case 'Meta':
return this.actionShortcutSymbol(formatting);
default:
return part;
}
})
.join('');
},
actionShortcutSymbol(formatting) {
return Features.isMac ? '⌘' : this.formatShortcut(Locale.ctrlKey, formatting);
},
altShortcutSymbol(formatting) {
return Features.isMac ? '⌥' : this.formatShortcut(Locale.altKey, formatting);
},
shiftShortcutSymbol(formatting) {
return Features.isMac ? '⇧' : this.formatShortcut(Locale.shiftKey, formatting);
},
ctrlShortcutSymbol(formatting) {
return Features.isMac ? '⌃' : this.formatShortcut(Locale.ctrlKey, formatting);
},
formatShortcut(shortcut, formatting) {
return formatting ? `${shortcut} + ` : `${shortcut}+`;
},
globalShortcutText(type, formatting) {
return this.presentShortcut(this.globalShortcut(type), formatting);
},
globalShortcut(type) {
const appSettingsShortcut = AppSettingsModel[this.globalShortcutAppSettingsKey(type)];
if (appSettingsShortcut) {
return appSettingsShortcut;
}
const globalShortcut = globalShortcuts[type];
if (globalShortcut) {
if (Features.isMac && globalShortcut.mac) {
return globalShortcut.mac;
}
return globalShortcut.all;
}
return undefined;
},
setGlobalShortcut(type, value) {
if (!globalShortcuts[type]) {
throw new Error('Bad shortcut: ' + type);
}
if (value) {
AppSettingsModel[this.globalShortcutAppSettingsKey(type)] = value;
} else {
delete AppSettingsModel[this.globalShortcutAppSettingsKey(type)];
}
Launcher.setGlobalShortcuts(AppSettingsModel);
},
globalShortcutAppSettingsKey(type) {
return 'globalShortcut' + StringFormat.capFirst(type);
},
screenshotToClipboardShortcut() {
if (Features.isiOS) {
return 'Sleep+Home';
}
if (Features.isMobile) {
return '';
}
if (Features.isMac) {
return 'Command-Shift-Control-4';
}
if (Features.isWindows) {
return 'Alt+PrintScreen';
}
return '';
}
};
export { Shortcuts };
@@ -1,43 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
const LocalStorageKeyName = 'instanceCheck';
const LocalStorageResponseKeyName = 'instanceMaster';
const instanceKey = Date.now().toString();
const SingleInstanceChecker = {
init() {
if (Launcher) {
return;
}
window.addEventListener('storage', SingleInstanceChecker.storageChanged);
SingleInstanceChecker.setKey(LocalStorageKeyName, instanceKey);
},
storageChanged(e) {
if (!e.newValue) {
return;
}
if (e.key === LocalStorageKeyName && e.newValue !== instanceKey) {
SingleInstanceChecker.setKey(
LocalStorageResponseKeyName,
instanceKey + Math.random().toString()
);
} else if (e.key === LocalStorageResponseKeyName && e.newValue.indexOf(instanceKey) < 0) {
window.removeEventListener('storage', SingleInstanceChecker.storageChanged);
Events.emit('second-instance');
}
},
setKey(key, value) {
try {
localStorage.setItem(key, value);
setTimeout(() => {
localStorage.removeItem(key);
}, 100);
} catch (e) {}
}
};
export { SingleInstanceChecker };
-54
View File
@@ -1,54 +0,0 @@
import { Logger } from 'util/logger';
const logger = new Logger('start-profiler');
let lastTs = 0;
const operations = [];
const StartProfiler = {
milestone(name) {
const ts = logger.ts();
const elapsed = ts - lastTs;
lastTs = ts;
operations.push({ name, elapsed });
},
report() {
const networkTime = this.getNetworkTime();
operations.unshift({ name: 'fetching', elapsed: networkTime });
const time = Math.round(performance.now());
this.printReport('App', operations, time);
},
reportAppProfile(data) {
this.printReport('Electron app', data.timings, data.totalTime);
},
printReport(name, operations, totalTime) {
const message =
`${name} started in ${totalTime}ms: ` +
operations.map((op) => `${op.name}=${Math.round(op.elapsed)}ms`).join(', ');
logger.info(message);
},
getNetworkTime() {
let perfEntry;
if (performance.getEntriesByType) {
[perfEntry] = performance.getEntriesByType('navigation');
}
if (!perfEntry || !perfEntry.responseEnd || !perfEntry.fetchStart) {
perfEntry = performance.timing;
}
return perfEntry.responseEnd - perfEntry.fetchStart;
}
};
StartProfiler.milestone('pre-init');
export { StartProfiler };
-306
View File
@@ -1,306 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport';
import { Launcher } from 'comp/launcher';
import { Links } from 'const/links';
import { AppSettingsModel } from 'models/app-settings-model';
import { UpdateModel } from 'models/update-model';
import { SemVer } from 'util/data/semver';
import { Logger } from 'util/logger';
import { SignatureVerifier } from 'util/data/signature-verifier';
const logger = new Logger('updater');
const Updater = {
UpdateInterval: 1000 * 60 * 60 * 24,
MinUpdateTimeout: 500,
MinUpdateSize: 10000,
nextCheckTimeout: null,
updateCheckDate: new Date(0),
enabled: Launcher?.updaterEnabled(),
getAutoUpdateType() {
if (!this.enabled) {
return false;
}
let autoUpdate = AppSettingsModel.autoUpdate;
if (autoUpdate && autoUpdate === true) {
autoUpdate = 'install';
}
return autoUpdate;
},
updateInProgress() {
return (
UpdateModel.status === 'checking' ||
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
);
},
init() {
this.scheduleNextCheck();
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
navigator.serviceWorker
.register('service-worker.js')
.then((reg) => {
logger.info('Service worker registered');
reg.addEventListener('updatefound', () => {
if (reg.active) {
logger.info('Service worker found an update');
UpdateModel.set({ updateStatus: 'ready' });
}
});
})
.catch((e) => {
logger.error('Failed to register a service worker', e);
});
}
},
scheduleNextCheck() {
if (this.nextCheckTimeout) {
clearTimeout(this.nextCheckTimeout);
this.nextCheckTimeout = null;
}
if (!this.getAutoUpdateType()) {
return;
}
let timeDiff = this.MinUpdateTimeout;
const lastCheckDate = UpdateModel.lastCheckDate;
if (lastCheckDate) {
timeDiff = Math.min(
Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout),
this.UpdateInterval
);
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
},
check(startedByUser) {
if (!this.enabled || this.updateInProgress()) {
return;
}
UpdateModel.set({ status: 'checking' });
if (!startedByUser) {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
const diffMs = new Date() - this.updateCheckDate;
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
logger.error(
'Prevented update check; last check was performed at ' + this.updateCheckDate
);
this.scheduleNextCheck();
return;
}
this.updateCheckDate = new Date();
}
logger.info('Checking for update...');
Transport.httpGet({
url: Links.UpdateJson,
json: true,
success: (updateJson) => {
const dt = new Date();
logger.info('Update check: ' + (updateJson.version || 'unknown'));
if (!updateJson.version) {
const errMsg = 'No version info found';
UpdateModel.set({
status: 'error',
lastCheckDate: dt,
lastCheckError: errMsg
});
UpdateModel.save();
this.scheduleNextCheck();
return;
}
const prevLastVersion = UpdateModel.lastVersion;
UpdateModel.set({
status: 'ok',
lastCheckDate: dt,
lastSuccessCheckDate: dt,
lastVersionReleaseDate: new Date(updateJson.date),
lastVersion: updateJson.version,
lastCheckError: null,
lastCheckUpdMin: updateJson.minVersion || null
});
UpdateModel.save();
this.scheduleNextCheck();
if (!this.canAutoUpdate()) {
return;
}
if (
prevLastVersion === UpdateModel.lastVersion &&
UpdateModel.updateStatus === 'ready'
) {
logger.info('Waiting for the user to apply downloaded update');
return;
}
if (!startedByUser && this.getAutoUpdateType() === 'install') {
this.update(startedByUser);
} else if (
SemVer.compareVersions(UpdateModel.lastVersion, RuntimeInfo.version) > 0
) {
UpdateModel.set({ updateStatus: 'found' });
}
},
error: (e) => {
logger.error('Update check error', e);
UpdateModel.set({
status: 'error',
lastCheckDate: new Date(),
lastCheckError: 'Error checking last version'
});
UpdateModel.save();
this.scheduleNextCheck();
}
});
},
canAutoUpdate() {
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
if (minLauncherVersion) {
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
if (cmp < 0) {
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
return false;
}
}
return true;
},
update(startedByUser, successCallback) {
const ver = UpdateModel.lastVersion;
if (!this.enabled) {
logger.info('Updater is disabled');
return;
}
if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
logger.info('You are using the latest version');
return;
}
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
logger.info('Downloading update', ver);
const updateAssetName = this.getUpdateAssetName(ver);
if (!updateAssetName) {
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
return;
}
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
const updateAssetUrl = updateUrlBasePath + updateAssetName;
Transport.httpGet({
url: updateAssetUrl,
file: updateAssetName,
cleanupOldFiles: true,
cache: true,
success: (assetFilePath) => {
logger.info('Downloading update signatures');
Transport.httpGet({
url: updateUrlBasePath + 'Verify.sign.sha256',
text: true,
file: updateAssetName + '.sign',
cleanupOldFiles: true,
cache: true,
success: (assetFileSignaturePath) => {
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
if (err || !valid) {
UpdateModel.set({
updateStatus: 'error',
updateError: err
? 'Error verifying update signature'
: 'Invalid update signature'
});
Launcher.deleteFile(assetFilePath);
Launcher.deleteFile(assetFileSignaturePath);
return;
}
logger.info('Update is ready', assetFilePath);
UpdateModel.set({ updateStatus: 'ready', updateError: null });
if (!startedByUser) {
Events.emit('update-app');
}
if (typeof successCallback === 'function') {
successCallback();
}
});
},
error(e) {
logger.error('Error downloading update signatures', e);
UpdateModel.set({
updateStatus: 'error',
updateError: 'Error downloading update signatures'
});
}
});
},
error(e) {
logger.error('Error downloading update', e);
UpdateModel.set({
updateStatus: 'error',
updateError: 'Error downloading update'
});
}
});
},
verifySignature(assetFilePath, assetName, callback) {
logger.info('Verifying update signature', assetName);
const fs = Launcher.req('fs');
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
const assetSignatureLine = signaturesTxt
.split('\n')
.find((line) => line.endsWith(assetName));
if (!assetSignatureLine) {
logger.error('Signature not found for asset', assetName);
callback('Asset signature not found');
return;
}
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
const fileBytes = fs.readFileSync(assetFilePath);
SignatureVerifier.verify(fileBytes, signature)
.catch((e) => {
logger.error('Error verifying signature', e);
callback('Error verifying signature');
})
.then((valid) => {
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
callback(undefined, valid);
});
},
getUpdateAssetName(ver) {
const platform = Launcher.platform();
const arch = Launcher.arch();
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.win.x64.exe`;
case 'ia32':
return `KeeWeb-${ver}.win.ia32.exe`;
case 'arm64':
return `KeeWeb-${ver}.win.arm64.exe`;
}
break;
case 'darwin':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.mac.x64.dmg`;
case 'arm64':
return `KeeWeb-${ver}.mac.arm64.dmg`;
}
break;
}
return undefined;
},
installAndRestart() {
if (!Launcher) {
return;
}
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
const updateFilePath = Transport.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
}
};
export { Updater };
-69
View File
@@ -1,69 +0,0 @@
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { NativeModules } from 'comp/launcher/native-modules';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
const logger = new Logger('usb-listener');
const UsbListener = {
supported: Features.isDesktop,
attachedYubiKeys: 0,
init() {
if (!this.supported) {
return;
}
Events.on('native-modules-yubikeys', (e) => {
if (e.numYubiKeys !== this.attachedYubiKeys) {
logger.debug(`YubiKeys changed ${this.attachedYubiKeys} => ${e.numYubiKeys}`);
this.attachedYubiKeys = e.numYubiKeys;
Events.emit('usb-devices-changed');
}
});
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
if (enabled) {
this.start();
} else {
this.stop();
}
});
if (AppSettingsModel.enableUsb) {
this.start();
}
},
start() {
logger.info('Starting USB listener');
if (this.usb) {
this.stop();
}
try {
NativeModules.startUsbListener();
} catch (e) {
logger.error('Error starting USB listener', e);
}
},
stop() {
logger.info('Stopping USB listener');
try {
NativeModules.stopUsbListener();
} catch (e) {
logger.error('Error stopping USB listener', e);
}
if (this.attachedYubiKeys) {
this.attachedYubiKeys = 0;
Events.emit('usb-devices-changed');
}
}
};
export { UsbListener };
-291
View File
@@ -1,291 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { NativeModules } from 'comp/launcher/native-modules';
import { Logger } from 'util/logger';
import { UsbListener } from 'comp/app/usb-listener';
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { YubiKeyProductIds, YubiKeyChallengeSize } from 'const/hardware';
import { Locale } from 'util/locale';
const logger = new Logger('yubikey');
const YubiKey = {
ykmanStatus: undefined,
process: null,
aborted: false,
cmd() {
if (this._cmd) {
return this._cmd;
}
const macYkmanPath = '/usr/local/bin/ykman';
if (Launcher.platform() === 'darwin' && Launcher.fileExistsSync(macYkmanPath)) {
this._cmd = macYkmanPath;
} else {
this._cmd = 'ykman';
}
return this._cmd;
},
checkToolStatus() {
if (this.ykmanStatus === 'ok') {
return Promise.resolve(this.ykmanStatus);
}
return new Promise((resolve) => {
this.ykmanStatus = 'checking';
this._cmd = undefined;
Launcher.spawn({
cmd: this.cmd(),
args: ['-v'],
noStdOutLogging: true,
complete: (err, stdout, code) => {
if (err || code !== 0) {
this.ykmanStatus = 'error';
} else {
this.ykmanStatus = 'ok';
}
resolve(this.ykmanStatus);
}
});
});
},
abort() {
logger.info('Aborting');
if (this.process) {
logger.info('Killing the process');
try {
this.process.kill();
} catch {}
}
this.aborted = true;
this.process = null;
},
list(callback) {
NativeModules.getYubiKeys({})
.then((yubiKeys) => {
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
return {
vid,
pid,
serial,
slots,
fullName: this.getKeyFullName(pid, version, serial)
};
});
callback(null, yubiKeys);
})
.catch(callback);
},
getKeyFullName(pid, version, serial) {
let name = 'YubiKey';
if (YubiKeyProductIds.Gen1.includes(pid)) {
name += ' Gen 1';
} else if (YubiKeyProductIds.NEO.includes(pid)) {
name += ' NEO';
} else if (YubiKeyProductIds.YK4.includes(pid)) {
if (version >= '5.1.0') {
name += ' 5';
}
}
return `${name} ${serial}`;
},
listWithYkman(callback) {
this._listWithYkman(callback, true);
},
_listWithYkman(callback, canRetry) {
if (this.process) {
return callback('Already in progress');
}
this.aborted = false;
logger.info('Listing YubiKeys');
if (!UsbListener.attachedYubiKeys) {
return callback(null, []);
}
this.process = Launcher.spawn({
cmd: this.cmd(),
args: ['list'],
noStdOutLogging: true,
complete: (err, stdout) => {
this.process = null;
if (this.aborted) {
return callback('Aborted');
}
if (err) {
return callback(err);
}
const yubiKeysIncludingEmpty = stdout
.trim()
.split(/\n/g)
.map((line) => {
const fullName = line;
const serial = (line.match(/\d{5,}$/g) || [])[0];
return { fullName, serial };
});
const yubiKeys = yubiKeysIncludingEmpty.filter((s) => s.serial);
if (
yubiKeysIncludingEmpty.length === 1 &&
yubiKeys.length === 0 &&
stdout.startsWith('YubiKey') &&
stdout.includes('CCID') &&
!stdout.includes('Serial')
) {
logger.info('The YubiKey is probably stuck');
if (!AppSettingsModel.yubiKeyStuckWorkaround) {
return callback(Locale.yubiKeyStuckError);
}
if (canRetry) {
return this._repairStuckYubiKey(callback);
}
}
if (!yubiKeys.length) {
return callback('No YubiKeys returned by "ykman list"');
}
callback(null, yubiKeys);
}
});
},
_repairStuckYubiKey(callback) {
logger.info('Repairing a stuck YubiKey');
let openTimeout;
const countYubiKeys = UsbListener.attachedYubiKeys;
const onDevicesChangedDuringRepair = () => {
if (UsbListener.attachedYubiKeys === countYubiKeys) {
logger.info('YubiKey was reconnected');
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
clearTimeout(openTimeout);
this.aborted = false;
setTimeout(() => {
this._listWithYkman(callback, false);
}, Timeouts.ExternalDeviceAfterReconnect);
}
};
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
Launcher.spawn({
cmd: this.cmd(),
args: ['config', 'usb', '-e', 'oath', '-f'],
noStdOutLogging: true,
complete: (err) => {
logger.info('Repair complete', err ? 'with error' : 'OK');
if (err) {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
return callback(`YubiKey repair error: ${err}`);
}
openTimeout = setTimeout(() => {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
}, Timeouts.ExternalDeviceReconnect);
}
});
},
getOtpCodes(serial, callback) {
if (this.process) {
return callback('Already in progress');
}
this.aborted = false;
this.process = Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'accounts', 'code'],
noStdOutLogging: true,
throwOnStdErr: true,
complete: (err, stdout) => {
this.process = null;
if (this.aborted) {
return callback('Aborted');
}
if (err) {
return callback(err);
}
const codes = [];
for (const line of stdout.split('\n')) {
const match = line.match(/^(.*?):(.*?)\s+(.*)$/);
if (!match) {
continue;
}
const [, title, user, code] = match;
const needsTouch = !code.match(/^\d+$/);
codes.push({
title,
user,
needsTouch
});
}
callback(null, codes);
}
});
},
getOtp(serial, entry, callback) {
return Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
noStdOutLogging: true,
complete: (err, stdout) => {
if (err) {
return callback(err);
}
const otp = stdout.trim();
callback(null, otp);
}
});
},
calculateChalResp(chalResp, challenge, callback) {
const { vid, pid, serial, slot } = chalResp;
const yubiKey = { vid, pid, serial };
challenge = Buffer.from(challenge);
// https://github.com/Yubico/yubikey-personalization-gui/issues/86
// https://github.com/keepassxreboot/keepassxc/blob/develop/src/keys/drivers/YubiKey.cpp#L318
const padLen = YubiKeyChallengeSize - challenge.byteLength;
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
challenge.copy(paddedChallenge);
NativeModules.yubiKeyChallengeResponse(
yubiKey,
[...paddedChallenge],
slot,
(err, result) => {
if (result) {
result = Buffer.from(result);
}
if (err) {
err.ykError = true;
}
return callback(err, result);
}
);
},
cancelChalResp() {
NativeModules.yubiKeyCancelChallengeResponse();
}
};
export { YubiKey };
-74
View File
@@ -1,74 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
const CopyPaste = {
simpleCopy: !!(Launcher && Launcher.clipboardSupported),
copy(text) {
if (this.simpleCopy) {
Launcher.setClipboardText(text);
const clipboardSeconds = AppSettingsModel.clipboardSeconds;
if (clipboardSeconds > 0) {
const clearClipboard = () => {
if (Launcher.getClipboardText() === text) {
Launcher.clearClipboardText();
}
};
Events.on('main-window-will-close', clearClipboard);
setTimeout(() => {
clearClipboard();
Events.off('main-window-will-close', clearClipboard);
}, clipboardSeconds * 1000);
}
return { success: true, seconds: clipboardSeconds };
} else {
try {
if (document.execCommand('copy')) {
return { success: true };
}
} catch (e) {}
return false;
}
},
createHiddenInput(text) {
const hiddenInput = $('<input/>')
.val(text)
.attr({ type: 'text', 'class': 'hide-by-pos' })
.appendTo(document.body);
hiddenInput[0].selectionStart = 0;
hiddenInput[0].selectionEnd = text.length;
hiddenInput.focus();
hiddenInput.on({
'copy cut paste'() {
setTimeout(() => hiddenInput.blur(), 0);
},
blur() {
hiddenInput.remove();
}
});
},
copyHtml(html) {
const el = document.createElement('div');
el.style.userSelect = 'auto';
el.style.webkitUserSelect = 'auto';
el.style.mozUserSelect = 'auto';
el.innerHTML = html;
document.body.appendChild(el);
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
const result = document.execCommand('copy');
el.remove();
return result;
}
};
export { CopyPaste };
@@ -1,41 +0,0 @@
import { Features } from 'util/features';
const FeatureTester = {
test() {
return Promise.resolve()
.then(() => this.checkWebAssembly())
.then(() => this.checkLocalStorage())
.then(() => this.checkWebCrypto());
},
checkWebAssembly() {
try {
const module = new global.WebAssembly.Module(
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
);
return new global.WebAssembly.Instance(module) instanceof global.WebAssembly.Instance;
} catch (e) {
throw 'WebAssembly is not supported';
}
},
checkLocalStorage() {
if (Features.isDesktop) {
return;
}
try {
localStorage.setItem('_test', '1');
localStorage.removeItem('_test');
} catch (e) {
throw 'LocalStorage is not supported';
}
},
checkWebCrypto() {
if (!global.crypto.subtle) {
throw 'WebCrypto is not supported';
}
}
};
export { FeatureTester };
@@ -1,36 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
const FocusDetector = {
init() {
this.isFocused = true;
this.detectsFocusWithEvents = !Features.isDesktop && !Features.isMobile;
if (this.detectsFocusWithEvents) {
window.addEventListener('focus', () => {
if (!FocusDetector.isFocused) {
FocusDetector.isFocused = true;
Events.emit('main-window-focus');
}
});
window.addEventListener('blur', () => {
if (FocusDetector.isFocused) {
FocusDetector.isFocused = false;
Events.emit('main-window-blur');
}
});
}
},
hasFocus() {
if (this.detectsFocusWithEvents) {
return this.isFocused;
} else if (Launcher) {
return Launcher.isAppFocused();
} else {
return true;
}
}
};
export { FocusDetector };
-24
View File
@@ -1,24 +0,0 @@
import { Events } from 'framework/events';
import { AppSettingsModel } from 'models/app-settings-model';
const IdleTracker = {
actionTime: Date.now(),
init() {
setInterval(this.checkIdle.bind(this), 1000 * 60);
},
checkIdle() {
const idleMinutes = (Date.now() - this.actionTime) / 1000 / 60;
const maxIdleMinutes = AppSettingsModel.idleMinutes;
if (maxIdleMinutes && idleMinutes > maxIdleMinutes) {
Events.emit('before-user-idle');
Events.emit('user-idle');
}
},
regUserAction() {
this.actionTime = Date.now();
}
};
Events.on('power-monitor-resume', () => IdleTracker.checkIdle);
export { IdleTracker };
-139
View File
@@ -1,139 +0,0 @@
import { Events } from 'framework/events';
import { IdleTracker } from 'comp/browser/idle-tracker';
import { Keys } from 'const/keys';
import { FocusManager } from 'comp/app/focus-manager';
const shortcutKeyProp = navigator.platform.indexOf('Mac') >= 0 ? 'metaKey' : 'ctrlKey';
class KeyHandler {
SHORTCUT_ACTION = 1;
SHORTCUT_OPT = 2;
SHORTCUT_SHIFT = 4;
shortcuts = {};
init() {
$(document).bind('keypress', this.keypress.bind(this));
$(document).bind('keydown', this.keydown.bind(this));
this.shortcuts[Keys.DOM_VK_A] = [
{
handler: this.handleAKey,
thisArg: this,
shortcut: this.SHORTCUT_ACTION,
modal: true,
noPrevent: true
}
];
}
onKey(key, handler, thisArg, shortcut, modal, noPrevent) {
let keyShortcuts = this.shortcuts[key];
if (!keyShortcuts) {
this.shortcuts[key] = keyShortcuts = [];
}
keyShortcuts.push({
handler,
thisArg,
shortcut,
modal,
noPrevent
});
}
offKey(key, handler, thisArg) {
if (this.shortcuts[key]) {
this.shortcuts[key] = this.shortcuts[key].filter(
(sh) => sh.handler !== handler || sh.thisArg !== thisArg
);
}
}
isActionKey(e) {
return e[shortcutKeyProp];
}
keydown(e) {
IdleTracker.regUserAction();
const code = e.keyCode || e.which;
const keyShortcuts = this.shortcuts[code];
if (keyShortcuts && keyShortcuts.length) {
for (const sh of keyShortcuts) {
if (FocusManager.modal && sh.modal !== FocusManager.modal && sh.modal !== '*') {
e.stopPropagation();
continue;
}
const isActionKey = this.isActionKey(e);
switch (sh.shortcut) {
case this.SHORTCUT_ACTION:
if (!isActionKey) {
continue;
}
break;
case this.SHORTCUT_OPT:
if (!e.altKey) {
continue;
}
break;
case this.SHORTCUT_SHIFT:
if (!e.shiftKey) {
continue;
}
break;
case this.SHORTCUT_ACTION + this.SHORTCUT_OPT:
if (!e.altKey || !isActionKey) {
continue;
}
break;
default:
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) {
continue;
}
break;
}
sh.handler.call(sh.thisArg, e, code);
if (isActionKey && !sh.noPrevent) {
e.preventDefault();
}
if (e.isImmediatePropagationStopped()) {
break;
}
}
}
}
keypress(e) {
if (
!FocusManager.modal &&
e.which !== Keys.DOM_VK_RETURN &&
e.which !== Keys.DOM_VK_ESCAPE &&
e.which !== Keys.DOM_VK_TAB &&
!e.altKey &&
!e.ctrlKey &&
!e.metaKey
) {
Events.emit('keypress', e);
} else if (FocusManager.modal) {
Events.emit('keypress:' + FocusManager.modal, e);
}
}
reg() {
IdleTracker.regUserAction();
}
handleAKey(e) {
if (
e.target.tagName.toLowerCase() === 'input' &&
['password', 'text'].indexOf(e.target.type) >= 0
) {
e.stopImmediatePropagation();
} else {
e.preventDefault();
}
}
}
const instance = new KeyHandler();
export { instance as KeyHandler };
@@ -1,73 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
const PopupNotifier = {
logger: null,
init() {
this.logger = new Logger('popup-notifier');
if (Launcher) {
window.open = noop;
} else {
const windowOpen = window.open;
window.open = function (...args) {
const win = windowOpen.apply(window, args);
if (win) {
PopupNotifier.deferCheckClosed(win);
Events.emit('popup-opened', win);
} else {
if (!Alerts.alertDisplayed) {
Alerts.error({
header: Locale.authPopupRequired,
body: Locale.authPopupRequiredBody
});
}
}
return win;
};
}
},
deferCheckClosed(win) {
setTimeout(PopupNotifier.checkClosed.bind(PopupNotifier, win), Timeouts.CheckWindowClosed);
},
checkClosed(win) {
if (win.closed) {
setTimeout(
PopupNotifier.triggerClosed.bind(PopupNotifier, win),
Timeouts.CheckWindowClosed
);
} else {
const loc = PopupNotifier.tryGetLocationSearch(win);
if (loc) {
try {
win.close();
} catch {}
PopupNotifier.triggerClosed(win, loc);
return;
}
PopupNotifier.deferCheckClosed(win);
}
},
tryGetLocationSearch(win) {
try {
if (win.location.host === location.host) {
return win.location.search;
}
} catch {}
},
triggerClosed(window, locationSearch) {
Events.emit('popup-closed', { window, locationSearch });
}
};
export { PopupNotifier };
-101
View File
@@ -1,101 +0,0 @@
import * as kdbxweb from 'kdbxweb';
const SecureInput = function () {
this.el = null;
this.minChar = 0x1400 + Math.round(Math.random() * 100);
this.maxLen = 1024;
this.length = 0;
this.pseudoValue = '';
this.salt = new Uint32Array(0);
};
SecureInput.prototype.setElement = function (el) {
this.el = el;
this.el.val(this.pseudoValue);
this.el.on('input', this._input.bind(this));
};
SecureInput.prototype.reset = function () {
this.el = null;
this.length = 0;
this.pseudoValue = '';
if (this.salt) {
for (let i = 0; i < this.salt.length; i++) {
this.salt[i] = 0;
}
}
this.salt = new Uint32Array(0);
};
SecureInput.prototype._input = function () {
const selStart = this.el[0].selectionStart;
const value = this.el.val();
let newPs = '';
const newSalt = new Uint32Array(this.maxLen);
let valIx = 0,
psIx = 0;
while (valIx < value.length) {
const valCh = value.charCodeAt(valIx);
const psCh = this.pseudoValue.charCodeAt(psIx);
const isSpecial = this._isSpecialChar(valCh);
if (psCh === valCh) {
// not changed
newPs += this._getChar(newPs.length);
newSalt[newPs.length - 1] = psCh ^ this.salt[psIx] ^ newPs.charCodeAt(newPs.length - 1);
psIx++;
valIx++;
} else if (isSpecial) {
// deleted
psIx++;
} else {
// inserted or replaced
newPs += this._getChar(newPs.length);
newSalt[newPs.length - 1] = newPs.charCodeAt(newPs.length - 1) ^ valCh;
valIx++;
}
}
this.length = newPs.length;
this.pseudoValue = newPs;
this.salt = newSalt;
this.el.val(newPs);
this.el[0].selectionStart = selStart;
this.el[0].selectionEnd = selStart;
};
SecureInput.prototype._getChar = function (ix) {
return String.fromCharCode(this.minChar + ix);
};
SecureInput.prototype._isSpecialChar = function (ch) {
return ch >= this.minChar && ch <= this.minChar + this.maxLen;
};
Object.defineProperty(SecureInput.prototype, 'value', {
enumerable: true,
get() {
const pseudoValue = this.pseudoValue;
const salt = this.salt;
const len = pseudoValue.length;
let byteLength = 0;
const valueBytes = new Uint8Array(len * 4);
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
let ch;
let bytes;
for (let i = 0; i < len; i++) {
const pseudoCharCode = pseudoValue.charCodeAt(i);
ch = String.fromCharCode(salt[i] ^ pseudoCharCode);
bytes = kdbxweb.ByteUtils.stringToBytes(ch);
for (let j = 0; j < bytes.length; j++) {
valueBytes[byteLength] = bytes[j] ^ saltBytes[byteLength];
byteLength++;
}
}
return new kdbxweb.ProtectedValue(
valueBytes.buffer.slice(0, byteLength),
saltBytes.buffer.slice(0, byteLength)
);
}
});
export { SecureInput };
-19
View File
@@ -1,19 +0,0 @@
import { Events } from 'framework/events';
const ThemeWatcher = {
dark: false,
init() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (mediaQuery && mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', (e) => {
const dark = e.matches;
this.dark = dark;
Events.emit('dark-mode-changed', { dark });
});
}
this.dark = !!mediaQuery.matches;
}
};
export { ThemeWatcher };
-118
View File
@@ -1,118 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
import { StringFormat } from 'util/formatting/string-format';
const logger = new Logger('transport');
const Transport = {
cacheFilePath(fileName) {
return Launcher.getTempPath(fileName);
},
httpGet(config) {
let tmpFile;
const fs = Launcher.req('fs');
if (config.file) {
const baseTempPath = Launcher.getTempPath();
if (config.cleanupOldFiles) {
const allFiles = fs.readdirSync(baseTempPath);
for (const file of allFiles) {
if (
file !== config.file &&
StringFormat.replaceVersion(file, '0') ===
StringFormat.replaceVersion(config.file, '0')
) {
fs.unlinkSync(Launcher.joinPath(baseTempPath, file));
}
}
}
tmpFile = Launcher.joinPath(baseTempPath, config.file);
if (fs.existsSync(tmpFile)) {
try {
if (config.cache && fs.statSync(tmpFile).size > 0) {
logger.info('File already downloaded ' + config.url);
return config.success(tmpFile);
} else {
fs.unlinkSync(tmpFile);
}
} catch (e) {
fs.unlink(tmpFile, noop);
}
}
}
const proto = config.url.split(':')[0];
logger.info('GET ' + config.url);
const opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.resolveProxy(config.url, (proxy) => {
logger.info(
'Request to ' +
config.url +
' ' +
(proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy')
);
if (proxy) {
opts.headers.Host = opts.host;
opts.host = proxy.host;
opts.port = proxy.port;
opts.path = config.url;
}
Launcher.req(proto)
.get(opts, (res) => {
logger.info('Response from ' + config.url + ': ' + res.statusCode);
if (res.statusCode === 200) {
if (config.file) {
const file = fs.createWriteStream(tmpFile);
res.pipe(file);
file.on('finish', () => {
file.close(() => {
config.success(tmpFile);
});
});
file.on('error', (err) => {
config.error(err);
});
} else {
let data = [];
res.on('data', (chunk) => {
data.push(chunk);
});
res.on('end', () => {
data = window.Buffer.concat(data);
if (config.text || config.json) {
data = data.toString('utf8');
}
if (config.json) {
try {
data = JSON.parse(data);
} catch (e) {
config.error('Error parsing JSON: ' + e.message);
}
}
config.success(data);
});
}
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
if (config.noRedirect) {
return config.error('Too many redirects');
}
config.url = res.headers.location;
config.noRedirect = true;
Transport.httpGet(config);
} else {
config.error('HTTP status ' + res.statusCode);
}
})
.on('error', (e) => {
logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile, noop);
}
config.error(e);
});
});
}
};
export { Transport };
@@ -1,248 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger';
import { ProtocolImpl } from './protocol-impl';
import { RuntimeInfo } from 'const/runtime-info';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
const WebConnectionInfo = {
connectionId: 1,
extensionName: 'KeeWeb Connect',
supportsNotifications: true
};
const SupportedExtensions = [
{ alias: 'KWC', name: 'KeeWeb Connect' },
{ alias: 'KPXC', name: 'KeePassXC-Browser' }
];
const SupportedBrowsers = ['Chrome', 'Firefox', 'Edge', 'Other'];
if (Features.isMac) {
SupportedBrowsers.unshift('Safari');
}
const logger = new Logger('browser-extension-connector');
if (!localStorage.debugBrowserExtension) {
logger.level = Logger.Level.Info;
}
const connections = new Map();
const pendingBrowserMessages = [];
let processingBrowserMessage = false;
const BrowserExtensionConnector = {
started: false,
logger,
init(appModel) {
const sendEvent = this.sendEvent.bind(this);
ProtocolImpl.init({ appModel, logger, sendEvent });
this.browserWindowMessage = this.browserWindowMessage.bind(this);
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
this.socketConnected(socketId, connectionInfo)
);
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
this.socketClosed(socketId)
);
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
this.socketRequest(socketId, request)
);
AppSettingsModel.on('change', () => this.appSettingsChanged());
}
if (this.isEnabled()) {
this.start();
}
},
start() {
if (Launcher) {
this.startDesktopAppListener();
} else {
this.startWebMessageListener();
}
this.started = true;
},
stop() {
if (Launcher) {
this.stopDesktopAppListener();
} else {
this.stopWebMessageListener();
}
ProtocolImpl.cleanup();
connections.clear();
this.started = false;
},
appSettingsChanged() {
if (this.isEnabled()) {
if (!this.started) {
this.start();
}
} else if (this.started) {
this.stop();
}
},
isEnabled() {
if (!Launcher) {
return true;
}
for (const ext of SupportedExtensions) {
for (const browser of SupportedBrowsers) {
if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) {
return true;
}
}
}
return false;
},
startWebMessageListener() {
window.addEventListener('message', this.browserWindowMessage);
logger.info('Started');
},
stopWebMessageListener() {
window.removeEventListener('message', this.browserWindowMessage);
},
enable(browser, extension, enabled) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled);
},
async startDesktopAppListener() {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorStart', {
appleTeamId: RuntimeInfo.appleTeamId
});
},
stopDesktopAppListener() {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorStop');
},
browserWindowMessage(e) {
if (e.origin !== location.origin) {
return;
}
if (e.source !== window) {
return;
}
if (e?.data?.kwConnect !== 'request') {
return;
}
logger.debug('Extension -> KeeWeb', e.data);
pendingBrowserMessages.push(e.data);
this.processBrowserMessages();
},
async processBrowserMessages() {
if (!pendingBrowserMessages.length || processingBrowserMessage) {
return;
}
if (!connections.has(WebConnectionInfo.connectionId)) {
connections.set(WebConnectionInfo.connectionId, WebConnectionInfo);
}
processingBrowserMessage = true;
const request = pendingBrowserMessages.shift();
const response = await ProtocolImpl.handleRequest(request, WebConnectionInfo);
processingBrowserMessage = false;
if (response) {
this.sendWebResponse(response);
}
this.processBrowserMessages();
},
sendWebResponse(response) {
logger.debug('KeeWeb -> Extension', response);
response.kwConnect = 'response';
postMessage(response, window.location.origin);
},
sendSocketEvent(data) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
},
sendSocketResult(socketId, data) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
},
sendEvent(data) {
if (!this.isEnabled() || !connections.size) {
return;
}
if (Launcher) {
this.sendSocketEvent(data);
} else {
this.sendWebResponse(data);
}
},
socketConnected(socketId, connectionInfo) {
connections.set(socketId, connectionInfo);
},
socketClosed(socketId) {
connections.delete(socketId);
ProtocolImpl.deleteConnection(socketId);
},
async socketRequest(socketId, request) {
let result;
const connectionInfo = connections.get(socketId);
if (connectionInfo) {
result = await ProtocolImpl.handleRequest(request, connectionInfo);
} else {
const message = `Connection not found: ${socketId}`;
result = ProtocolImpl.errorToResponse({ message }, request);
}
this.sendSocketResult(socketId, result);
},
get sessions() {
return ProtocolImpl.sessions;
},
terminateConnection(connectionId) {
connectionId = +connectionId;
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorCloseSocket', connectionId);
} else {
ProtocolImpl.deleteConnection(connectionId);
}
},
getClientPermissions(clientId) {
return ProtocolImpl.getClientPermissions(clientId);
},
setClientPermissions(clientId, permissions) {
ProtocolImpl.setClientPermissions(clientId, permissions);
}
};
export { BrowserExtensionConnector, SupportedExtensions, SupportedBrowsers };
-940
View File
@@ -1,940 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { box as tweetnaclBox } from 'tweetnacl';
import { PasswordGenerator } from 'util/generators/password-generator';
import { GeneratorPresets } from 'comp/app/generator-presets';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { RuntimeInfo } from 'const/runtime-info';
import { KnownAppVersions } from 'const/known-app-versions';
import { ExtensionConnectView } from 'views/extension/extension-connect-view';
import { ExtensionCreateGroupView } from 'views/extension/extension-create-group-view';
import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-view';
import { RuntimeDataModel } from 'models/runtime-data-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { SelectEntryView } from 'views/select/select-entry-view';
import { SelectEntryFieldView } from 'views/select/select-entry-field-view';
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
const KeeWebAssociationId = 'KeeWeb';
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
const ExtensionGroupIconId = 1;
const DefaultExtensionGroupName = 'Browser';
const ExtensionGroupNames = new Set(['KeePassXC-Browser Passwords', DefaultExtensionGroupName]);
const Errors = {
noOpenFiles: {
message: Locale.extensionErrorNoOpenFiles,
code: '1'
},
userRejected: {
message: Locale.extensionErrorUserRejected,
code: '6'
},
noMatches: {
message: Locale.extensionErrorNoMatches,
code: '15'
}
};
const connectedClients = new Map();
let logger;
let appModel;
let sendEvent;
function setupListeners() {
Events.on('file-opened', () => {
sendEvent({ action: 'database-unlocked' });
});
Events.on('one-file-closed', () => {
if (!appModel.files.hasOpenFiles()) {
sendEvent({ action: 'database-locked' });
}
});
Events.on('all-files-closed', () => {
sendEvent({ action: 'database-locked' });
});
}
function incrementNonce(nonce) {
// from libsodium/utils.c, like it is in KeePassXC
let i = 0;
let c = 1;
for (; i < nonce.length; ++i) {
c += nonce[i];
nonce[i] = c;
c >>= 8;
}
}
function getClient(request) {
if (!request.clientID) {
throw new Error('Empty clientID');
}
const client = connectedClients.get(request.clientID);
if (!client) {
throw new Error(`Client not connected: ${request.clientID}`);
}
return client;
}
function decryptRequest(request) {
const client = getClient(request);
if (!request.nonce) {
throw new Error('Empty nonce');
}
if (!request.message) {
throw new Error('Empty message');
}
const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
const message = kdbxweb.ByteUtils.base64ToBytes(request.message);
const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey);
if (!data) {
throw new Error('Failed to decrypt data');
}
const json = new TextDecoder().decode(data);
const payload = JSON.parse(json);
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
if (!payload) {
throw new Error('Empty request payload');
}
if (payload.action !== request.action) {
throw new Error(`Bad action in decrypted payload`);
}
return payload;
}
function encryptResponse(request, payload) {
logger.debug('KeeWeb -> Extension (decrypted)', payload);
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
incrementNonce(nonceBytes);
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
const client = getClient(request);
payload.nonce = nonce;
const json = JSON.stringify(payload);
const data = new TextEncoder().encode(json);
const encrypted = tweetnaclBox(data, nonceBytes, client.publicKey, client.keys.secretKey);
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
return {
action: request.action,
message,
nonce
};
}
function makeError(def) {
const e = new Error(def.message);
e.code = def.code;
return e;
}
function ensureAtLeastOneFileIsOpen() {
if (!appModel.files.hasOpenFiles()) {
throw makeError(Errors.noOpenFiles);
}
}
async function checkContentRequestPermissions(request) {
if (!appModel.files.hasOpenFiles()) {
if (AppSettingsModel.extensionFocusIfLocked) {
try {
focusKeeWeb();
await appModel.unlockAnyFile(
'extensionUnlockMessage',
Timeouts.KeeWebConnectRequest
);
} catch {
throw makeError(Errors.noOpenFiles);
}
} else {
throw makeError(Errors.noOpenFiles);
}
}
const client = getClient(request);
if (client.permissions) {
return;
}
if (Alerts.alertDisplayed) {
throw new Error(Locale.extensionErrorAlertDisplayed);
}
focusKeeWeb();
const config = RuntimeDataModel.extensionConnectConfig;
const files = appModel.files.map((f) => ({
id: f.id,
name: f.name,
checked: !config || config.allFiles || config.files.includes(f.id)
}));
if (!files.some((f) => f.checked)) {
for (const f of files) {
f.checked = true;
}
}
const extensionConnectView = new ExtensionConnectView({
extensionName: getHumanReadableExtensionName(client),
identityVerified: !Launcher,
files,
allFiles: config?.allFiles ?? true,
askGet: config?.askGet || 'multiple'
});
try {
await alertWithTimeout({
header: Locale.extensionConnectHeader,
icon: 'exchange-alt',
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
view: extensionConnectView,
wide: true,
opaque: true
});
} catch (e) {
client.permissionsDenied = true;
Events.emit('browser-extension-sessions-changed');
throw e;
}
RuntimeDataModel.extensionConnectConfig = extensionConnectView.config;
client.permissions = extensionConnectView.config;
Events.emit('browser-extension-sessions-changed');
}
function alertWithTimeout(config) {
return new Promise((resolve, reject) => {
let inactivityTimer = 0;
const alert = Alerts.alert({
...config,
enter: 'yes',
esc: '',
success: (res) => {
clearTimeout(inactivityTimer);
resolve(res);
},
cancel: () => {
clearTimeout(inactivityTimer);
reject(makeError(Errors.userRejected));
}
});
inactivityTimer = setTimeout(() => {
alert.closeWithResult('');
}, Timeouts.KeeWebConnectRequest);
});
}
function getAvailableFiles(request) {
const client = getClient(request);
if (!client.permissions) {
return;
}
const files = appModel.files.filter(
(file) =>
file.active &&
(client.permissions.allFiles || client.permissions.files.includes(file.id))
);
if (!files.length) {
throw makeError(Errors.noOpenFiles);
}
return files;
}
function getVersion(request) {
return isKeePassXcBrowser(request) ? KnownAppVersions.KeePassXC : RuntimeInfo.version;
}
function isKeeWebConnect(request) {
return getClient(request).connection.extensionName === 'KeeWeb Connect';
}
function isKeePassXcBrowser(request) {
return getClient(request).connection.extensionName === 'KeePassXC-Browser';
}
function getHumanReadableExtensionName(client) {
return client.connection.appName
? `${client.connection.extensionName} (${client.connection.appName})`
: client.connection.extensionName;
}
function focusKeeWeb() {
logger.debug('Focus KeeWeb');
if (Launcher) {
if (!Launcher.isAppFocused()) {
Launcher.showMainWindow();
}
} else {
sendEvent({ action: 'attention-required' });
}
}
async function findEntry(request, returnIfOneMatch, filterOptions) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.url) {
throw new Error('Empty url');
}
const files = getAvailableFiles(request);
const client = getClient(request);
const filter = new SelectEntryFilter(
{ url: payload.url, title: payload.title },
appModel,
files,
filterOptions
);
filter.subdomains = false;
let entries = filter.getEntries();
filter.subdomains = true;
let entry;
if (entries.length) {
if (entries.length === 1 && returnIfOneMatch && client.permissions.askGet === 'multiple') {
entry = entries[0];
}
} else {
entries = filter.getEntries();
if (!entries.length) {
if (AppSettingsModel.extensionFocusIfEmpty) {
filter.useUrl = false;
if (filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
filter.useTitle = true;
entries = filter.getEntries();
if (!entries.length) {
filter.useTitle = false;
}
}
} else {
throw makeError(Errors.noMatches);
}
}
}
if (!entry) {
const extName = getHumanReadableExtensionName(client);
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
const selectEntryView = new SelectEntryView({ filter, topMessage });
focusKeeWeb();
const inactivityTimer = setTimeout(() => {
selectEntryView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const result = await selectEntryView.showAndGetResult();
clearTimeout(inactivityTimer);
entry = result?.entry;
if (!entry) {
throw makeError(Errors.userRejected);
}
}
client.stats.passwordsRead++;
return entry;
}
const ProtocolHandlers = {
'ping'({ data }) {
return { data };
},
'change-public-keys'(request, connection) {
let { publicKey, version, clientID: clientId } = request;
if (connectedClients.has(clientId)) {
throw new Error('Changing keys is not allowed');
}
if (!Launcher) {
// on web there can be only one connected client
connectedClients.clear();
}
const keys = tweetnaclBox.keyPair();
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
const stats = {
connectedDate: new Date(),
passwordsRead: 0,
passwordsWritten: 0
};
connectedClients.set(clientId, { connection, publicKey, version, keys, stats });
Events.emit('browser-extension-sessions-changed');
logger.info('New client key created', clientId, version);
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
incrementNonce(nonceBytes);
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
return {
action: 'change-public-keys',
version: getVersion(request),
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
nonce,
success: 'true',
...(isKeeWebConnect(request) ? { appName: 'KeeWeb' } : undefined)
};
},
async 'get-databasehash'(request) {
decryptRequest(request);
if (request.triggerUnlock) {
await checkContentRequestPermissions(request);
} else {
ensureAtLeastOneFileIsOpen();
}
return encryptResponse(request, {
hash: KeeWebHash,
success: 'true',
version: getVersion(request)
});
},
'generate-password'(request) {
const password = PasswordGenerator.generate(GeneratorPresets.browserExtensionPreset);
return encryptResponse(request, {
version: getVersion(request),
success: 'true',
entries: [{ password }]
});
},
'lock-database'(request) {
decryptRequest(request);
ensureAtLeastOneFileIsOpen();
Events.emit('lock-workspace');
if (Alerts.alertDisplayed) {
focusKeeWeb();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request)
});
},
'associate'(request) {
decryptRequest(request);
ensureAtLeastOneFileIsOpen();
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
id: KeeWebAssociationId
});
},
'test-associate'(request) {
const payload = decryptRequest(request);
// ensureAtLeastOneFileIsOpen();
if (payload.id !== KeeWebAssociationId) {
throw makeError(Errors.noOpenFiles);
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
id: payload.id
});
},
async 'get-logins'(request) {
const entry = await findEntry(request, true);
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
count: 1,
entries: [
{
group: entry.group.title,
login: entry.user || '',
name: entry.title || '',
password: entry.password?.getText() || '',
skipAutoSubmit: 'false',
stringFields: [],
uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes)
}
],
id: ''
});
},
async 'get-totp-by-url'(request) {
const entry = await findEntry(request, true, { otp: true });
entry.initOtpGenerator();
if (!entry.otpGenerator) {
throw makeError(Errors.noMatches);
}
let selectEntryFieldView;
if (entry.needsTouch) {
selectEntryFieldView = new SelectEntryFieldView({
needsTouch: true,
deviceShortName: entry.device.shortName
});
selectEntryFieldView.render();
}
const otpPromise = new Promise((resolve, reject) => {
selectEntryFieldView?.on('result', () => reject(makeError(Errors.userRejected)));
entry.otpGenerator.next((err, otp) => {
if (otp) {
resolve(otp);
} else {
reject(err || makeError(Errors.userRejected));
}
});
});
let totp;
try {
totp = await otpPromise;
} finally {
selectEntryFieldView?.remove();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
totp
});
},
async 'get-any-field'(request) {
const entry = await findEntry(request, false);
const selectEntryFieldView = new SelectEntryFieldView({
entry
});
const inactivityTimer = setTimeout(() => {
selectEntryFieldView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const field = await selectEntryFieldView.showAndGetResult();
clearTimeout(inactivityTimer);
if (!field) {
throw makeError(Errors.userRejected);
}
let value = entry.getAllFields()[field];
if (value.isProtected) {
value = value.getText();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
field,
value
});
},
async 'get-totp'(request) {
decryptRequest(request);
await checkContentRequestPermissions(request);
throw new Error('Not implemented');
},
async 'set-login'(request) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
focusKeeWeb();
if (!payload.url) {
throw new Error('Empty url');
}
const url = new URL(payload.url);
const files = getAvailableFiles(request);
const client = getClient(request);
let selectedGroup;
let entryToUpdate;
if (payload.uuid) {
for (const file of files) {
const entryId = kdbxweb.ByteUtils.bytesToBase64(
kdbxweb.ByteUtils.hexToBytes(payload.uuid)
);
const foundEntry = file.getEntry(file.subId(entryId));
if (foundEntry) {
if (entryToUpdate) {
throw new Error('Two entries with the same ID found');
} else {
entryToUpdate = foundEntry;
selectedGroup = foundEntry.group;
}
}
}
if (!entryToUpdate) {
throw new Error('Updated entry not found');
}
}
if (client.permissions.askSave === 'auto' && client.permissions.saveTo && !selectedGroup) {
const file = files.find((f) => f.id === client.permissions.saveTo.fileId);
selectedGroup = file?.getGroup(client.permissions.saveTo.groupId);
}
if (client.permissions.askSave !== 'auto' || !selectedGroup) {
if (!selectedGroup && RuntimeDataModel.extensionSaveConfig) {
const file = files.find(
(f) => f.id === RuntimeDataModel.extensionSaveConfig.fileId
);
selectedGroup = file?.getGroup(RuntimeDataModel.extensionSaveConfig.groupId);
}
const allGroups = [];
for (const file of files) {
file.forEachGroup((group) => {
const spaces = [];
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
spaces.push(' ', ' ');
}
if (
!selectedGroup &&
group.iconId === ExtensionGroupIconId &&
ExtensionGroupNames.has(group.title)
) {
selectedGroup = group;
}
allGroups.push({
id: group.id,
fileId: file.id,
spaces,
title: group.title,
selected: group.id === selectedGroup?.id
});
});
}
if (!selectedGroup) {
allGroups.splice(1, 0, {
id: '',
fileId: files[0].id,
spaces: [' ', ' '],
title: `${DefaultExtensionGroupName} (${Locale.extensionSaveEntryNewGroup})`,
selected: true
});
}
const saveEntryView = new ExtensionSaveEntryView({
extensionName: getHumanReadableExtensionName(client),
url: payload.url,
user: payload.login,
askSave: RuntimeDataModel.extensionSaveConfig?.askSave || 'always',
update: !!entryToUpdate,
allGroups
});
await alertWithTimeout({
header: Locale.extensionSaveEntryHeader,
icon: 'plus',
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
view: saveEntryView
});
const config = { ...saveEntryView.config };
if (!entryToUpdate) {
if (config.groupId) {
const file = files.find((f) => f.id === config.fileId);
selectedGroup = file.getGroup(config.groupId);
} else {
selectedGroup = appModel.createNewGroupWithName(
files[0].groups[0],
files[0],
DefaultExtensionGroupName
);
selectedGroup.setIcon(ExtensionGroupIconId);
config.groupId = selectedGroup.id;
}
RuntimeDataModel.extensionSaveConfig = config;
client.permissions.saveTo = { fileId: config.fileId, groupId: config.groupId };
}
client.permissions.askSave = config.askSave;
}
const entryFields = {
Title: url.hostname,
UserName: payload.login,
Password: kdbxweb.ProtectedValue.fromString(payload.password || ''),
URL: payload.url
};
if (entryToUpdate) {
for (const [field, value] of Object.entries(entryFields)) {
if (value) {
entryToUpdate.setField(field, value);
}
}
} else {
appModel.createNewEntryWithFields(selectedGroup, entryFields);
}
client.stats.passwordsWritten++;
Events.emit('browser-extension-sessions-changed');
Events.emit('refresh');
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
count: null,
entries: null,
hash: KeeWebHash
});
},
async 'get-database-groups'(request) {
decryptRequest(request);
await checkContentRequestPermissions(request);
const makeGroups = (group) => {
const res = {
name: group.title,
uuid: kdbxweb.ByteUtils.bytesToHex(group.group.uuid.bytes),
children: []
};
for (const subGroup of group.items) {
if (subGroup.matches()) {
res.children.push(makeGroups(subGroup));
}
}
return res;
};
const groups = [];
for (const file of getAvailableFiles(request)) {
for (const group of file.groups) {
groups.push(makeGroups(group));
}
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
groups: { groups }
});
},
async 'create-new-group'(request) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.groupName) {
throw new Error('No groupName');
}
const groupNames = payload.groupName
.split('/')
.map((g) => g.trim())
.filter((g) => g);
if (!groupNames.length) {
throw new Error('Empty group path');
}
const files = getAvailableFiles(request);
for (const file of files) {
for (const rootGroup of file.groups) {
let foundGroup = rootGroup;
const pendingGroups = [...groupNames];
while (pendingGroups.length && foundGroup) {
const title = pendingGroups.shift();
foundGroup = foundGroup.items.find((g) => g.title === title);
}
if (foundGroup) {
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
name: foundGroup.title,
uuid: kdbxweb.ByteUtils.bytesToHex(foundGroup.group.uuid.bytes)
});
}
}
}
const client = getClient(request);
const createGroupView = new ExtensionCreateGroupView({
extensionName: getHumanReadableExtensionName(client),
groupPath: groupNames.join(' / '),
files: files.map((f, ix) => ({ id: f.id, name: f.name, selected: ix === 0 }))
});
await alertWithTimeout({
header: Locale.extensionNewGroupHeader,
icon: 'folder-plus',
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
view: createGroupView
});
const selectedFile = files.find((f) => f.id === createGroupView.selectedFile);
let newGroup = selectedFile.groups[0];
const pendingGroups = [...groupNames];
while (pendingGroups.length) {
const title = pendingGroups.shift();
const item = newGroup.items.find((g) => g.title === title);
if (item) {
newGroup = item;
} else {
newGroup = appModel.createNewGroupWithName(newGroup, selectedFile, title);
}
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
name: newGroup.title,
uuid: kdbxweb.ByteUtils.bytesToHex(newGroup.group.uuid.bytes)
});
}
};
const ProtocolImpl = {
init(vars) {
appModel = vars.appModel;
logger = vars.logger;
sendEvent = vars.sendEvent;
setupListeners();
},
cleanup() {
const wasNotEmpty = connectedClients.size;
connectedClients.clear();
if (wasNotEmpty) {
Events.emit('browser-extension-sessions-changed');
}
},
deleteConnection(connectionId) {
for (const [clientId, client] of connectedClients.entries()) {
if (client.connection.connectionId === connectionId) {
connectedClients.delete(clientId);
}
}
Events.emit('browser-extension-sessions-changed');
},
getClientPermissions(clientId) {
return connectedClients.get(clientId)?.permissions;
},
setClientPermissions(clientId, permissions) {
const client = connectedClients.get(clientId);
if (client?.permissions) {
client.permissions = { ...client.permissions, ...permissions };
}
},
errorToResponse(e, request) {
return {
action: request?.action,
error: e.message || 'Unknown error',
errorCode: e.code || 0
};
},
async handleRequest(request, connectionInfo) {
const appWindowWasFocused = Launcher?.isAppFocused();
let result;
try {
const handler = ProtocolHandlers[request.action];
if (!handler) {
throw new Error(`Handler not found: ${request.action}`);
}
result = await handler(request, connectionInfo);
if (!result) {
throw new Error(`Handler returned an empty result: ${request.action}`);
}
} catch (e) {
if (!e.code) {
logger.error(`Error in handler ${request.action}`, e);
}
result = this.errorToResponse(e, request);
}
if (!appWindowWasFocused && Launcher?.isAppFocused()) {
Launcher.hideApp();
}
return result;
},
get sessions() {
return [...connectedClients.entries()]
.map(([clientId, client]) => ({
clientId,
connectionId: client.connection.connectionId,
appName: client.connection.appName,
extensionName: client.connection.extensionName,
connectedDate: client.stats.connectedDate,
passwordsRead: client.stats.passwordsRead,
passwordsWritten: client.stats.passwordsWritten,
permissions: client.permissions,
permissionsDenied: client.permissionsDenied
}))
.sort((x, y) => y.connectedDate - x.connectedDate);
}
};
export { ProtocolImpl };
-119
View File
@@ -1,119 +0,0 @@
/* eslint-disable import/no-commonjs */
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { DateFormat } from 'comp/i18n/date-format';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
const Templates = {
db: require('templates/export/db.hbs'),
entry: require('templates/export/entry.hbs')
};
const FieldMapping = [
{ name: 'UserName', locStr: 'user' },
{ name: 'Password', locStr: 'password', protect: true },
{ name: 'URL', locStr: 'website' },
{ name: 'Notes', locStr: 'notes' }
];
const KnownFields = { 'Title': true };
for (const { name } of FieldMapping) {
KnownFields[name] = true;
}
function walkGroup(db, group, parents) {
parents = [...parents, group];
if (
group.uuid.equals(db.meta.recycleBinUuid) ||
group.uuid.equals(db.meta.entryTemplatesGroup)
) {
return '';
}
const self = group.entries.map((entry) => walkEntry(db, entry, parents)).join('\n');
const children = group.groups
.map((childGroup) => walkGroup(db, childGroup, parents))
.join('\n');
return self + children;
}
function walkEntry(db, entry, parents) {
const path = parents.map((group) => group.name).join(' / ');
const fields = [];
for (const field of FieldMapping) {
const value = entryField(entry, field.name);
if (value) {
fields.push({
title: StringFormat.capFirst(Locale[field.locStr]),
value,
protect: field.protect
});
}
}
for (const [fieldName, fieldValue] of entry.fields) {
if (!KnownFields[fieldName]) {
const value = entryField(entry, fieldName);
if (value) {
fields.push({
title: fieldName,
value,
protect: fieldValue.isProtected
});
}
}
}
const title = entryField(entry, 'Title');
let expires;
if (entry.times.expires && entry.times.expiryTime) {
expires = DateFormat.dtStr(entry.times.expiryTime);
}
const attachments = [...entry.binaries]
.map(([name, data]) => {
if (data && data.ref) {
data = data.value;
}
if (data) {
const base64 = kdbxweb.ByteUtils.bytesToBase64(data);
data = 'data:application/octet-stream;base64,' + base64;
}
return { name, data };
})
.filter((att) => att.name && att.data);
return Templates.entry({
path,
title,
fields,
tags: entry.tags.join(', '),
created: DateFormat.dtStr(entry.times.creationTime),
modified: DateFormat.dtStr(entry.times.lastModTime),
expires,
attachments
});
}
function entryField(entry, fieldName) {
const value = entry.fields.get(fieldName);
return (value && value.isProtected && value.getText()) || value || '';
}
const KdbxToHtml = {
convert(db, options) {
const content = db.groups.map((group) => walkGroup(db, group, [])).join('\n');
return Templates.db({
name: options.name,
date: DateFormat.dtStr(Date.now()),
appLink: Links.Homepage,
appVersion: RuntimeInfo.version,
contentHtml: content
});
},
entryToHtml(db, entry) {
return walkEntry(db, entry, []);
}
};
export { KdbxToHtml };
-174
View File
@@ -1,174 +0,0 @@
import QrCode from 'jsqrcode';
import { Events } from 'framework/events';
import { Shortcuts } from 'comp/app/shortcuts';
import { Alerts } from 'comp/ui/alerts';
import { Otp } from 'util/data/otp';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
const logger = new Logger('otp-qr-reader');
class OtpQrReader {
alert = null;
fileInput = null;
constructor() {
this.pasteEvent = this.pasteEvent.bind(this);
}
read() {
let screenshotKey = Shortcuts.screenshotToClipboardShortcut();
if (screenshotKey) {
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', screenshotKey);
}
const pasteKey = Features.isMobile
? ''
: Locale.detSetupOtpAlertBodyWith.replace('{}', Shortcuts.actionShortcutSymbol() + 'V');
this.startListenClipoard();
const buttons = [
{ result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
Alerts.buttons.cancel
];
if (Features.isMobile) {
buttons.unshift({ result: 'select', title: Locale.detSetupOtpScanButton });
}
const line3 = Features.isMobile
? Locale.detSetupOtpAlertBody3Mobile
: Locale.detSetupOtpAlertBody3.replace('{}', pasteKey || '');
this.alert = Alerts.alert({
icon: 'qrcode',
header: Locale.detSetupOtpAlert,
body: [
Locale.detSetupOtpAlertBody,
Locale.detSetupOtpAlertBody1,
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
line3,
Locale.detSetupOtpAlertBody4
].join('\n'),
esc: '',
click: '',
enter: '',
buttons,
complete: (res) => {
this.alert = null;
this.stopListenClipboard();
if (res === 'select') {
this.selectFile();
} else if (res === 'manually') {
this.enterManually();
}
}
});
}
selectFile() {
if (!this.fileInput) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('capture', 'camera');
input.setAttribute('accept', 'image/*');
input.setAttribute('class', 'hide-by-pos');
this.fileInput = input;
this.fileInput.onchange = this.fileSelected;
}
this.fileInput.click();
}
fileSelected() {
const file = this.fileInput.files[0];
if (!file || file.type.indexOf('image') < 0) {
return;
}
this.readFile(file);
}
startListenClipoard() {
document.addEventListener('paste', this.pasteEvent);
}
stopListenClipboard() {
document.removeEventListener('paste', this.pasteEvent);
}
pasteEvent(e) {
const item = [...e.clipboardData.items].find(
(item) => item.kind === 'file' && item.type.indexOf('image') !== -1
);
if (!item) {
logger.debug('Paste without file');
return;
}
logger.info('Reading pasted image', item.type);
if (this.alert) {
this.alert.change({
header: Locale.detOtpImageReading
});
}
this.readFile(item.getAsFile());
}
readFile(file) {
const reader = new FileReader();
reader.onload = () => {
logger.debug('Image data loaded');
this.readQr(reader.result);
};
reader.readAsDataURL(file);
}
readQr(imageData) {
const image = new Image();
image.onload = () => {
logger.debug('Image format loaded');
try {
const ts = logger.ts();
const url = new QrCode(image).decode();
logger.info('QR code read', logger.ts(ts));
this.removeAlert();
try {
const otp = Otp.parseUrl(url);
Events.emit('qr-read', otp);
} catch (err) {
logger.error('Error parsing QR code', err);
Alerts.error({
header: Locale.detOtpQrWrong,
body: Locale.detOtpQrWrongBody,
pre: err.toString()
});
}
} catch (e) {
logger.error('Error reading QR code', e);
this.removeAlert();
Alerts.error({
header: Locale.detOtpQrError,
body: Locale.detOtpQrErrorBody
});
}
};
image.onerror = () => {
logger.debug('Image load error');
this.removeAlert();
Alerts.error({
header: Locale.detOtpImageError,
body: Locale.detOtpImageErrorBody
});
};
image.src = imageData;
}
enterManually() {
Events.emit('qr-enter-manually');
}
removeAlert() {
if (this.alert) {
this.alert.closeImmediate();
}
}
}
const instance = new OtpQrReader();
export { instance as OtpQrReader };
-77
View File
@@ -1,77 +0,0 @@
import { SettingsManager } from 'comp/settings/settings-manager';
import { StringFormat } from 'util/formatting/string-format';
const DateFormat = {
months() {
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { month: 'long' });
const months = [];
for (let month = 0; month < 12; month++) {
months.push(format.format(new Date(2008, month)));
}
return months;
},
weekDays() {
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'long' });
const weekdays = [];
for (let day = 1; day < 8; day++) {
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
}
return weekdays;
},
shortWeekDays() {
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'short' });
const weekdays = [];
for (let day = 1; day < 8; day++) {
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
}
return weekdays;
},
dtStr(dt) {
if (typeof dt === 'number') {
dt = new Date(dt);
}
return dt
? new Intl.DateTimeFormat(SettingsManager.activeLocale, {
dateStyle: 'medium',
timeStyle: 'medium'
}).format(dt)
: '';
},
dStr(dt) {
if (typeof dt === 'number') {
dt = new Date(dt);
}
return dt
? new Intl.DateTimeFormat(SettingsManager.activeLocale, {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(dt)
: '';
},
dtStrFs(dt) {
if (typeof dt === 'number') {
dt = new Date(dt);
}
return dt
? dt.getFullYear() +
'-' +
StringFormat.pad(dt.getMonth() + 1, 2) +
'-' +
StringFormat.pad(dt.getDate(), 2) +
'T' +
StringFormat.pad(dt.getHours(), 2) +
'-' +
StringFormat.pad(dt.getMinutes(), 2) +
'-' +
StringFormat.pad(dt.getSeconds(), 2)
: '';
}
};
export { DateFormat };
-7
View File
@@ -1,7 +0,0 @@
let Launcher;
if (window.process && window.process.versions && window.process.versions.electron) {
Launcher = require('./launcher-electron').Launcher;
}
export { Launcher };
@@ -1,330 +0,0 @@
import { Events } from 'framework/events';
import { StartProfiler } from 'comp/app/start-profiler';
import { RuntimeInfo } from 'const/runtime-info';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
const logger = new Logger('launcher');
const Launcher = {
name: 'electron',
version: window.process.versions.electron,
autoTypeSupported: true,
thirdPartyStoragesSupported: true,
clipboardSupported: true,
req: window.require,
platform() {
return process.platform;
},
arch() {
return process.arch;
},
electron() {
return this.req('electron');
},
remoteApp() {
return this.electron().remote.app;
},
remReq(mod) {
return this.electron().remote.require(mod);
},
openLink(href) {
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
this.electron().shell.openExternal(href);
}
},
devTools: true,
openDevTools() {
this.electron().remote.getCurrentWindow().webContents.openDevTools({ mode: 'bottom' });
},
getSaveFileName(defaultPath, callback) {
if (defaultPath) {
const homePath = this.remReq('electron').app.getPath('userDesktop');
defaultPath = this.joinPath(homePath, defaultPath);
}
this.remReq('electron')
.dialog.showSaveDialog({
title: Locale.launcherSave,
defaultPath,
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
})
.then((res) => callback(res.filePath));
},
getUserDataPath(fileName) {
if (!this.userDataPath) {
this.userDataPath = this.remoteApp().getPath('userData');
}
return this.joinPath(this.userDataPath, fileName || '');
},
getTempPath(fileName) {
let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
const fs = this.req('fs');
if (!fs.existsSync(tempPath)) {
fs.mkdirSync(tempPath);
}
if (fileName) {
tempPath = this.joinPath(tempPath, fileName);
}
return tempPath;
},
getDocumentsPath(fileName) {
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
},
getAppPath(fileName) {
const dirname = this.req('path').dirname;
const appPath = __dirname.endsWith('app.asar') ? __dirname : this.remoteApp().getAppPath();
return this.joinPath(dirname(appPath), fileName || '');
},
getWorkDirPath(fileName) {
return this.joinPath(process.cwd(), fileName || '');
},
joinPath(...parts) {
return this.req('path').join(...parts);
},
writeFile(path, data, callback) {
this.req('fs').writeFile(path, window.Buffer.from(data), callback);
},
readFile(path, encoding, callback) {
this.req('fs').readFile(path, encoding, (err, contents) => {
const data = typeof contents === 'string' ? contents : new Uint8Array(contents);
callback(data, err);
});
},
fileExists(path, callback) {
const fs = this.req('fs');
fs.access(path, fs.constants.F_OK, (err) => callback(!err));
},
fileExistsSync(path) {
const fs = this.req('fs');
return !fs.accessSync(path, fs.constants.F_OK);
},
deleteFile(path, callback) {
this.req('fs').unlink(path, callback || noop);
},
statFile(path, callback) {
this.req('fs').stat(path, (err, stats) => callback(stats, err));
},
mkdir(dir, callback) {
const fs = this.req('fs');
const path = this.req('path');
const stack = [];
const collect = function (dir, stack, callback) {
fs.exists(dir, (exists) => {
if (exists) {
return callback();
}
stack.unshift(dir);
const newDir = path.dirname(dir);
if (newDir === dir || !newDir || newDir === '.' || newDir === '/') {
return callback();
}
collect(newDir, stack, callback);
});
};
const create = function (stack, callback) {
if (!stack.length) {
return callback();
}
fs.mkdir(stack.shift(), (err) => (err ? callback(err) : create(stack, callback)));
};
collect(dir, stack, () => create(stack, callback));
},
parsePath(fileName) {
const path = this.req('path');
return {
path: fileName,
dir: path.dirname(fileName),
file: path.basename(fileName)
};
},
createFsWatcher(path) {
return this.req('fs').watch(path, { persistent: false });
},
loadConfig(name) {
return this.remoteApp().loadConfig(name);
},
saveConfig(name, data) {
return this.remoteApp().saveConfig(name, data);
},
preventExit(e) {
e.returnValue = false;
return false;
},
exit() {
this.exitRequested = true;
this.requestExit();
},
requestExit() {
const app = this.remoteApp();
app.setHookBeforeQuitEvent(false);
if (this.pendingUpdateFile) {
app.restartAndUpdate(this.pendingUpdateFile);
} else {
app.quit();
}
},
requestRestartAndUpdate(updateFilePath) {
this.pendingUpdateFile = updateFilePath;
this.requestExit();
},
cancelRestart() {
this.pendingUpdateFile = undefined;
},
setClipboardText(text) {
return this.electron().clipboard.writeText(text);
},
getClipboardText() {
return this.electron().clipboard.readText();
},
clearClipboardText() {
const { clipboard } = this.electron();
clipboard.clear();
if (process.platform === 'linux') {
clipboard.clear('selection');
}
},
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
return !!this.pendingUpdateFile;
},
minimizeApp() {
this.remoteApp().minimizeApp({
restore: Locale.menuRestoreApp.replace('{}', 'KeeWeb'),
quit: Locale.menuQuitApp.replace('{}', 'KeeWeb')
});
},
canDetectOsSleep() {
return process.platform !== 'linux';
},
updaterEnabled() {
return process.platform !== 'linux';
},
getMainWindow() {
return this.remoteApp().getMainWindow();
},
resolveProxy(url, callback) {
const window = this.getMainWindow();
const session = window.webContents.session;
session.resolveProxy(url).then((proxy) => {
const match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
callback(proxy);
});
},
hideApp() {
const app = this.remoteApp();
if (this.platform() === 'darwin') {
app.hide();
} else {
app.minimizeThenHideIfInTray();
}
},
isAppFocused() {
return !!this.electron().remote.BrowserWindow.getFocusedWindow();
},
showMainWindow() {
this.remoteApp().showAndFocusMainWindow();
},
spawn(config) {
const ts = logger.ts();
const { ipcRenderer } = this.electron();
let { complete } = config;
delete config.complete;
ipcRenderer
.invoke('spawnProcess', config)
.then((res) => {
if (res.err) {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
complete?.(res.err);
} else {
const code = res.code;
const stdout = res.stdout || '';
const stderr = res.stderr || '';
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
if (code !== 0) {
logger.error(msg + '\n' + stdout + '\n' + stderr);
} else {
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
}
complete?.(code !== 0 ? 'Exit code ' + code : null, stdout, code);
}
complete = null;
})
.catch((err) => {
complete?.(err);
});
},
checkOpenFiles() {
this.readyToOpenFiles = true;
if (this.pendingFileToOpen) {
this.openFile(this.pendingFileToOpen);
delete this.pendingFileToOpen;
}
},
openFile(file) {
if (this.readyToOpenFiles) {
Events.emit('launcher-open-file', file);
} else {
this.pendingFileToOpen = file;
}
},
setGlobalShortcuts(appSettings) {
this.remoteApp().setGlobalShortcuts(appSettings);
},
minimizeMainWindow() {
this.getMainWindow().minimize();
},
maximizeMainWindow() {
this.getMainWindow().maximize();
},
restoreMainWindow() {
this.getMainWindow().restore();
},
mainWindowMaximized() {
return this.getMainWindow().isMaximized();
}
};
Events.on('launcher-exit-request', () => {
setTimeout(() => Launcher.exit(), 0);
});
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
window.launcherOpen = (file) => Launcher.openFile(file);
if (window.launcherOpenedFile) {
logger.info('Open file request', window.launcherOpenedFile);
Launcher.openFile(window.launcherOpenedFile);
delete window.launcherOpenedFile;
}
Events.on('app-ready', () =>
setTimeout(() => {
Launcher.checkOpenFiles();
Launcher.remoteApp().setAboutPanelOptions({
applicationVersion: RuntimeInfo.version,
version: RuntimeInfo.commit
});
}, 0)
);
if (process.platform === 'darwin') {
Launcher.remoteApp().setHookBeforeQuitEvent(true);
}
Launcher.remoteApp().on('remote-app-event', (e) => {
if (window.debugRemoteAppEvents) {
logger.debug('remote-app-event', e.name);
}
Events.emit(e.name, e.data);
});
export { Launcher };
-250
View File
@@ -1,250 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
let NativeModules;
if (Launcher) {
const logger = new Logger('native-module-connector');
let hostRunning = false;
let hostStartPromise;
let callId = 0;
let promises = {};
let ykChalRespCallbacks = {};
const { ipcRenderer } = Launcher.electron();
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
const handlers = {
yubikeys(numYubiKeys) {
Events.emit('native-modules-yubikeys', { numYubiKeys });
},
log(...args) {
logger.info('Message from host', ...args);
},
result({ callId, result, error }) {
const promise = promises[callId];
if (promise) {
delete promises[callId];
if (error) {
logger.error('Received an error', promise.cmd, error);
promise.reject(error);
} else {
promise.resolve(result);
}
}
},
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
const callback = ykChalRespCallbacks[callbackId];
if (callback) {
const willBeCalledAgain = error && error.touchRequested;
if (!willBeCalledAgain) {
delete ykChalRespCallbacks[callbackId];
}
callback(error, result);
}
}
};
NativeModules = {
startHost() {
if (hostRunning) {
return Promise.resolve();
}
if (hostStartPromise) {
return hostStartPromise;
}
logger.debug('Starting native module host');
hostStartPromise = this.callNoWait('start').then(() => {
hostStartPromise = undefined;
hostRunning = true;
if (this.usbListenerRunning) {
return this.call('startUsbListener');
}
});
return hostStartPromise;
},
hostError(e) {
logger.error('Host error', e);
},
hostDisconnect() {
logger.error('Host disconnected');
},
hostExit(code, sig) {
logger.error(`Host exited with code ${code} and signal ${sig}`);
hostRunning = false;
const err = new Error('Native module host crashed');
for (const promise of Object.values(promises)) {
promise.reject(err);
}
promises = {};
for (const callback of Object.values(ykChalRespCallbacks)) {
callback(err);
}
ykChalRespCallbacks = {};
if (code !== 0) {
this.autoRestartHost();
}
},
hostCallback(message) {
const { cmd, args } = message;
// logger.debug('Callback', cmd, args);
if (handlers[cmd]) {
handlers[cmd](...args);
} else {
logger.error('No callback', cmd);
}
},
log(name, level, ...args) {
if (!name) {
return;
}
const logger = new Logger(name);
logger[level](...args);
},
autoRestartHost() {
setTimeout(() => {
try {
this.startHost();
} catch (e) {
logger.error('Native module host failed to auto-restart', e);
}
}, Timeouts.NativeModuleHostRestartTime);
},
call(cmd, ...args) {
return this.startHost().then(() => this.callNoWait(cmd, ...args));
},
callNoWait(cmd, ...args) {
return new Promise((resolve, reject) => {
callId++;
if (callId === Number.MAX_SAFE_INTEGER) {
callId = 1;
}
// logger.debug('Call', cmd, args, callId);
promises[callId] = { cmd, resolve, reject };
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
});
},
startUsbListener() {
this.call('startUsbListener');
this.usbListenerRunning = true;
},
stopUsbListener() {
this.usbListenerRunning = false;
if (hostRunning) {
this.call('stopUsbListener');
}
},
getYubiKeys(config) {
return this.call('getYubiKeys', config);
},
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
ykChalRespCallbacks[callId] = callback;
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
},
yubiKeyCancelChallengeResponse() {
if (hostRunning) {
this.call('yubiKeyCancelChallengeResponse');
}
},
argon2(password, salt, options) {
return this.call('argon2', password, salt, options);
},
hardwareCryptoDeleteKey: async () => {
await ipcRenderer.invoke('hardwareCryptoDeleteKey');
},
hardwareEncrypt: async (value) => {
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
return new kdbxweb.ProtectedValue(data, salt);
},
hardwareDecrypt: async (value, touchIdPrompt) => {
const { data, salt } = await ipcRenderer.invoke(
'hardwareDecrypt',
value.dataAndSalt(),
touchIdPrompt
);
return new kdbxweb.ProtectedValue(data, salt);
},
kbdGetActiveWindow(options) {
return this.call('kbdGetActiveWindow', options);
},
kbdGetActivePid() {
return this.call('kbdGetActivePid');
},
kbdShowWindow(win) {
return this.call('kbdShowWindow', win);
},
kbdText(str) {
return this.call('kbdText', str);
},
kbdTextAsKeys(str, mods) {
return this.call('kbdTextAsKeys', str, mods);
},
kbdKeyPress(code, modifiers) {
return this.call('kbdKeyPress', code, modifiers);
},
kbdShortcut(code, modifiers) {
return this.call('kbdShortcut', code, modifiers);
},
kbdKeyMoveWithModifier(down, modifiers) {
return this.call('kbdKeyMoveWithModifier', down, modifiers);
},
kbdKeyPressWithCharacter(character, code, modifiers) {
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
},
kbdEnsureModifierNotPressed() {
return this.call('kbdEnsureModifierNotPressed');
}
};
global.NativeModules = NativeModules;
}
export { NativeModules };
@@ -1,193 +0,0 @@
import { Events } from 'framework/events';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
const logger = new Logger('settings-manager');
const SettingsManager = {
neutralLocale: null,
activeLocale: 'en-US',
activeTheme: null,
allLocales: {
'en-US': 'English',
'de-DE': 'Deutsch',
'fr-FR': 'Français'
},
allThemes: {
dark: 'setGenThemeDark',
light: 'setGenThemeLight',
sd: 'setGenThemeSd',
sl: 'setGenThemeSl',
fb: 'setGenThemeFb',
bl: 'setGenThemeBl',
db: 'setGenThemeDb',
lb: 'setGenThemeLb',
te: 'setGenThemeTe',
lt: 'setGenThemeLt',
dc: 'setGenThemeDc',
hc: 'setGenThemeHc'
},
// changing something here? don't forget about desktop/app.js
autoSwitchedThemes: [
{
name: 'setGenThemeDefault',
dark: 'dark',
light: 'light'
},
{
name: 'setGenThemeSol',
dark: 'sd',
light: 'sl'
},
{
name: 'setGenThemeBlue',
dark: 'fb',
light: 'bl'
},
{
name: 'setGenThemeBrown',
dark: 'db',
light: 'lb'
},
{
name: 'setGenThemeTerminal',
dark: 'te',
light: 'lt'
},
{
name: 'setGenThemeHighContrast',
dark: 'dc',
light: 'hc'
}
],
customLocales: {},
init() {
Events.on('dark-mode-changed', () => this.darkModeChanged());
},
setBySettings() {
this.setTheme(AppSettingsModel.theme);
this.setFontSize(AppSettingsModel.fontSize);
const locale = AppSettingsModel.locale;
try {
if (locale) {
this.setLocale(AppSettingsModel.locale);
} else {
this.setLocale(this.getBrowserLocale());
}
} catch (ex) {}
},
getDefaultTheme() {
return 'dark';
},
setTheme(theme) {
if (!theme) {
if (this.activeTheme) {
return;
}
theme = this.getDefaultTheme();
}
for (const cls of document.body.classList) {
if (/^th-/.test(cls)) {
document.body.classList.remove(cls);
}
}
if (AppSettingsModel.autoSwitchTheme) {
theme = this.selectDarkOrLightTheme(theme);
}
document.body.classList.add(this.getThemeClass(theme));
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
}
this.activeTheme = theme;
logger.debug('Theme changed', theme);
Events.emit('theme-applied');
},
getThemeClass(theme) {
return 'th-' + theme;
},
selectDarkOrLightTheme(theme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === theme || config.dark === theme) {
return ThemeWatcher.dark ? config.dark : config.light;
}
}
return theme;
},
darkModeChanged() {
if (AppSettingsModel.autoSwitchTheme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
logger.debug('Setting theme triggered by system settings change', newTheme);
this.setTheme(newTheme);
break;
}
}
}
},
setFontSize(fontSize) {
const defaultFontSize = Features.isMobile ? 14 : 12;
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
},
setLocale(loc) {
if (!loc || loc === this.activeLocale) {
return;
}
let localeValues;
if (loc !== 'en-US') {
if (this.customLocales[loc]) {
localeValues = this.customLocales[loc];
} else {
localeValues = require('../../locales/' + loc + '.json');
}
}
if (!this.neutralLocale) {
this.neutralLocale = { ...Locale };
}
Object.assign(Locale, this.neutralLocale, localeValues);
this.activeLocale = loc;
Events.emit('set-locale', loc);
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
const localeValuesForDesktopApp = {};
for (const [key, value] of Object.entries(Locale)) {
if (key.startsWith('sysMenu')) {
localeValuesForDesktopApp[key] = value;
}
}
ipcRenderer.invoke('setLocale', {
locale: loc,
...localeValuesForDesktopApp
});
}
},
getBrowserLocale() {
const language = (navigator.languages && navigator.languages[0]) || navigator.language;
if (language && language.startsWith('en')) {
return 'en-US';
}
return language;
}
};
export { SettingsManager };
@@ -1,38 +0,0 @@
import { Launcher } from 'comp/launcher';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
const logger = new Logger('settings');
const SettingsStore = {
load(key) {
let loadPromise;
if (Launcher) {
loadPromise = Launcher.loadConfig(key);
} else {
loadPromise = Promise.resolve().then(() => {
return localStorage[StringFormat.camelCase(key)];
});
}
return loadPromise
.then((data) => (data ? JSON.parse(data) : null))
.catch((err) => {
logger.error(`Error loading ${key}`, err);
});
},
save(key, data) {
if (Launcher) {
return Launcher.saveConfig(key, JSON.stringify(data)).catch((err) => {
logger.error(`Error saving ${key}`, err);
});
}
return Promise.resolve().then(() => {
if (typeof localStorage !== 'undefined') {
localStorage[StringFormat.camelCase(key)] = JSON.stringify(data);
}
});
}
};
export { SettingsStore };
-122
View File
@@ -1,122 +0,0 @@
import { Locale } from 'util/locale';
import { ModalView } from 'views/modal-view';
const Alerts = {
alertDisplayed: false,
buttons: {
ok: {
result: 'yes',
get title() {
return Locale.alertOk;
}
},
yes: {
result: 'yes',
get title() {
return Locale.alertYes;
}
},
allow: {
result: 'yes',
get title() {
return Locale.alertAllow;
}
},
no: {
result: '',
get title() {
return Locale.alertNo;
}
},
cancel: {
result: '',
get title() {
return Locale.alertCancel;
}
},
deny: {
result: '',
get title() {
return Locale.alertDeny;
}
}
},
alert(config) {
if (config.skipIfAlertDisplayed && Alerts.alertDisplayed) {
return null;
}
Alerts.alertDisplayed = true;
const view = new ModalView(config);
view.render();
view.once('result', (res, check) => {
if (res && config.success) {
config.success(res, check);
}
if (!res && config.cancel) {
config.cancel();
}
if (config.complete) {
config.complete(res, check);
}
});
view.on('will-close', () => {
Alerts.alertDisplayed = false;
});
return view;
},
notImplemented() {
this.alert({
header: Locale.notImplemented,
body: '',
icon: 'exclamation-triangle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: ''
});
},
info(config) {
this.alert({
header: '',
body: '',
icon: 'info',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
error(config) {
this.alert({
header: '',
body: '',
icon: 'exclamation-circle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
yesno(config) {
this.alert({
header: '',
body: '',
icon: 'question',
buttons: [this.buttons.yes, this.buttons.no],
esc: '',
click: '',
enter: 'yes',
...config
});
}
};
export { Alerts };
-53
View File
@@ -1,53 +0,0 @@
// Secrets are not really secrets and are supposed to be embedded in the app code according to
// the Google's guide: https://developers.google.com/identity/protocols/oauth2#installed
// The process results in a client ID and, in some cases, a client secret,
// which you embed in the source code of your application.
// (In this context, the client secret is obviously not treated as a secret.)
const DropboxApps = {
AppFolder: { id: 'qp7ctun6qt5n9d6', secret: '07s5r4ck1uvlj6a' },
FullDropbox: { id: 'eor7hvv6u6oslq9', secret: 'ez04o1iwf6yprq3' }
};
const GDriveApps = {
Local: {
id: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
secret: 'yAtyfc9TIQ9GyQgQmo3i0HAP'
},
Production: {
id: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com',
secret: '42HeSBybXDZjvweotq4o4CkJ'
},
Desktop: {
id: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com',
secret: 'nTSCiqXtUNmURIIdASaC1TJK'
}
};
const OneDriveApps = {
Local: {
id: 'b97c53d5-db5b-4124-aab9-d39195293815'
},
Production: {
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923'
},
Desktop: {
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
secret: 'aOMJaktJEAs_Tmh]fx4iQ[Zd3mp3KK7-'
}
};
const MsTeamsApps = {
Local: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
},
Production: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
},
Desktop: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635',
secret: 'F02~HYaWs-~7MndJcVRtv9~h-50Brk_9ho'
}
};
export { DropboxApps, GDriveApps, OneDriveApps, MsTeamsApps };
-23
View File
@@ -1,23 +0,0 @@
const Colors = {
AllColors: ['yellow', 'green', 'red', 'orange', 'blue', 'violet'],
ColorsValues: {
yellow: 'ffff00',
green: '00ff00',
red: 'ff0000',
orange: 'ff8800',
blue: '0000ff',
violet: 'ff00ff'
},
BgColors: {
yellow: 'ffff88',
green: '88ff88',
red: 'ff8888',
orange: 'ffcc88',
blue: '8888ff',
violet: 'ff88ff'
}
};
export { Colors };
-99
View File
@@ -1,99 +0,0 @@
const DefaultAppSettings = {
theme: null, // UI theme
autoSwitchTheme: false, // automatically switch between light and dark theme
locale: null, // user interface language
expandGroups: true, // show entries from all subgroups
listViewWidth: null, // width of the entry list representation
menuViewWidth: null, // width of the left menu
tagsViewHeight: null, // tags menu section height
autoUpdate: 'install', // auto-update options: "install", "check", ""
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
autoSave: true, // auto-save open files
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
idleMinutes: 15, // app lock timeout after inactivity, minutes
minimizeOnClose: false, // minimise the app instead of closing
minimizeOnFieldCopy: false, // minimise the app on copy
tableView: false, // view entries as a table instead of list
colorfulIcons: false, // use colorful custom icons instead of grayscale
useMarkdown: true, // use Markdown in Notes field
directAutotype: true, // if only one matching entry is found, select that one automatically
autoTypeTitleFilterEnabled: true, // enable the title filtering in auto-type by default
titlebarStyle: 'default', // window titlebar style
lockOnMinimize: true, // lock the app when it's minimized
lockOnCopy: false, // lock the app after a password was copied
lockOnAutoType: false, // lock the app after performing auto-type
lockOnOsLock: false, // lock the app when the computer is locked
helpTipCopyShown: false, // disable the tooltip about copying fields
templateHelpShown: false, // disable the tooltip about entry templates
skipOpenLocalWarn: false, // disable the warning about opening a local file
hideEmptyFields: false, // hide empty fields in entries
skipHttpsWarning: false, // disable the non-HTTPS warning
demoOpened: false, // hide the demo button inside the More... menu
fontSize: 0, // font size: 0, 1, 2
tableViewColumns: null, // columns displayed in the table view
generatorPresets: null, // presets used in the password generator
generatorHidePassword: false, // hide password in the generator
cacheConfigSettings: false, // cache config settings and use them if the config can't be loaded
allowIframes: false, // allow displaying the app in IFrames
useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
auditPasswords: true, // enable password audit
auditPasswordEntropy: true, // show warnings for weak passwords
excludePinsFromAudit: true, // exclude PIN codes from audit
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
disableOfflineStorage: false, // don't cache loaded files in offline storage
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
yubiKeyMatchEntries: true, // show matching one-time codes in entries
yubiKeyShowChalResp: true, // show YubiKey challenge-response option
yubiKeyRememberChalResp: false, // remember YubiKey challenge-response codes while the app is open
yubiKeyStuckWorkaround: false, // enable the workaround for stuck YubiKeys
canOpen: true, // can select and open new files
canOpenDemo: true, // can open a demo file
canOpenSettings: true, // can go to settings
canCreate: true, // can create new files
canImportXml: true, // can import files from XML
canImportCsv: true, // can import files from CSV
canRemoveLatest: true, // can remove files from the recent file list
canExportXml: true, // can export files as XML
canExportHtml: true, // can export files as HTML
canSaveTo: true, // can save existing files to filesystem
canOpenStorage: true, // can open files from cloud storage providers
canOpenGenerator: true, // can open password generator
canOpenOtpDevice: true, // can open OTP codes from USB tokens
dropbox: true, // enable Dropbox integration
dropboxFolder: null, // default folder path
dropboxAppKey: null, // custom Dropbox app key
dropboxSecret: null, // custom Dropbox app secret
webdav: true, // enable WebDAV integration
webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put"
webdavStatReload: false, // WebDAV: reload the file instead of relying on Last-Modified
gdrive: true, // enable Google Drive integration
gdriveClientId: null, // custom Google Drive client id
gdriveClientSecret: null, // custom Google Drive client secret
onedrive: true, // enable OneDrive integration
onedriveClientId: null, // custom OneDrive client id
onedriveClientSecret: null, // custom OneDrive client secret
onedriveTenantId: null, // custom OneDrive tenant id
msteams: false, // enable Microsoft Teams integration
msteamsClientId: null, // custom Microsoft Teams client id
msteamsClientSecret: null, // custom Microsoft Teams client secret
msteamsTenantId: null // custom Microsoft Teams tenant id
};
export { DefaultAppSettings };
-12
View File
@@ -1,12 +0,0 @@
const BuiltInFields = [
'Title',
'Password',
'UserName',
'URL',
'Notes',
'TOTP Seed',
'TOTP Settings',
'_etm_template_uuid'
];
export { BuiltInFields };
-14
View File
@@ -1,14 +0,0 @@
// https://support.yubico.com/support/solutions/articles/15000028104-yubikey-usb-id-values
// https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h#L274
const YubiKeyVendorId = 0x1050;
const YubiKeyProductIds = {
Gen1: [0x0010],
NEO: [0x0110, 0x0112, 0x0113, 0x0114, 0x0115, 0x0116],
YK4: [0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407]
};
const YubiKeyChallengeSize = 64;
export { YubiKeyVendorId, YubiKeyProductIds, YubiKeyChallengeSize };
-73
View File
@@ -1,73 +0,0 @@
const IconMap = [
'key',
'globe',
'exclamation-triangle',
'server',
'thumbtack',
'comments',
'puzzle-piece',
'edit',
'plug',
'address-card',
'paperclip',
'camera',
'wifi',
'link',
'battery-three-quarters',
'barcode',
'certificate',
'bullseye',
'desktop',
'envelope',
'cog',
'clipboard',
'paper-plane',
'newspaper',
'bolt',
'inbox',
'save',
'hdd',
'dot-circle',
'user-lock',
'terminal',
'print',
'project-diagram',
'flag-checkered',
'wrench',
'laptop',
'archive',
'credit-card',
'windows',
'clock',
'search',
'flask',
'gamepad',
'trash',
'sticky-note',
'ban',
'question-circle',
'cube',
'folder-o',
'folder-open-o',
'database',
'unlock-alt',
'lock',
'check',
'pencil-alt',
'image',
'book',
'list-alt',
'user-secret',
'utensils',
'home',
'star',
'linux',
'map-pin',
'apple',
'wikipedia-w',
'dollar-sign',
'signature',
'mobile'
];
export { IconMap };
-2
View File
@@ -1,2 +0,0 @@
export const KeeWebLogo =
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';
-119
View File
@@ -1,119 +0,0 @@
const Keys = {
DOM_VK_CANCEL: 3,
DOM_VK_HELP: 6,
DOM_VK_BACK_SPACE: 8,
DOM_VK_TAB: 9,
DOM_VK_CLEAR: 12,
DOM_VK_RETURN: 13,
DOM_VK_ENTER: 14,
DOM_VK_SHIFT: 16,
DOM_VK_CONTROL: 17,
DOM_VK_ALT: 18,
DOM_VK_PAUSE: 19,
DOM_VK_CAPS_LOCK: 20,
DOM_VK_ESCAPE: 27,
DOM_VK_SPACE: 32,
DOM_VK_PAGE_UP: 33,
DOM_VK_PAGE_DOWN: 34,
DOM_VK_END: 35,
DOM_VK_HOME: 36,
DOM_VK_LEFT: 37,
DOM_VK_UP: 38,
DOM_VK_RIGHT: 39,
DOM_VK_DOWN: 40,
DOM_VK_PRINTSCREEN: 44,
DOM_VK_INSERT: 45,
DOM_VK_DELETE: 46,
DOM_VK_0: 48,
DOM_VK_1: 49,
DOM_VK_2: 50,
DOM_VK_3: 51,
DOM_VK_4: 52,
DOM_VK_5: 53,
DOM_VK_6: 54,
DOM_VK_7: 55,
DOM_VK_8: 56,
DOM_VK_9: 57,
DOM_VK_SEMICOLON: 59,
DOM_VK_EQUALS: 61,
DOM_VK_A: 65,
DOM_VK_B: 66,
DOM_VK_C: 67,
DOM_VK_D: 68,
DOM_VK_E: 69,
DOM_VK_F: 70,
DOM_VK_G: 71,
DOM_VK_H: 72,
DOM_VK_I: 73,
DOM_VK_J: 74,
DOM_VK_K: 75,
DOM_VK_L: 76,
DOM_VK_M: 77,
DOM_VK_N: 78,
DOM_VK_O: 79,
DOM_VK_P: 80,
DOM_VK_Q: 81,
DOM_VK_R: 82,
DOM_VK_S: 83,
DOM_VK_T: 84,
DOM_VK_U: 85,
DOM_VK_V: 86,
DOM_VK_W: 87,
DOM_VK_X: 88,
DOM_VK_Y: 89,
DOM_VK_Z: 90,
DOM_VK_CONTEXT_MENU: 93,
DOM_VK_NUMPAD0: 96,
DOM_VK_NUMPAD1: 97,
DOM_VK_NUMPAD2: 98,
DOM_VK_NUMPAD3: 99,
DOM_VK_NUMPAD4: 100,
DOM_VK_NUMPAD5: 101,
DOM_VK_NUMPAD6: 102,
DOM_VK_NUMPAD7: 103,
DOM_VK_NUMPAD8: 104,
DOM_VK_NUMPAD9: 105,
DOM_VK_MULTIPLY: 106,
DOM_VK_ADD: 107,
DOM_VK_SEPARATOR: 108,
DOM_VK_SUBTRACT: 109,
DOM_VK_DECIMAL: 110,
DOM_VK_DIVIDE: 111,
DOM_VK_F1: 112,
DOM_VK_F2: 113,
DOM_VK_F3: 114,
DOM_VK_F4: 115,
DOM_VK_F5: 116,
DOM_VK_F6: 117,
DOM_VK_F7: 118,
DOM_VK_F8: 119,
DOM_VK_F9: 120,
DOM_VK_F10: 121,
DOM_VK_F11: 122,
DOM_VK_F12: 123,
DOM_VK_F13: 124,
DOM_VK_F14: 125,
DOM_VK_F15: 126,
DOM_VK_F16: 127,
DOM_VK_F17: 128,
DOM_VK_F18: 129,
DOM_VK_F19: 130,
DOM_VK_F20: 131,
DOM_VK_F21: 132,
DOM_VK_F22: 133,
DOM_VK_F23: 134,
DOM_VK_F24: 135,
DOM_VK_NUM_LOCK: 144,
DOM_VK_SCROLL_LOCK: 145,
DOM_VK_COMMA: 188,
DOM_VK_PERIOD: 190,
DOM_VK_SLASH: 191,
DOM_VK_BACK_QUOTE: 192,
DOM_VK_OPEN_BRACKET: 219,
DOM_VK_BACK_SLASH: 220,
DOM_VK_CLOSE_BRACKET: 221,
DOM_VK_QUOTE: 222,
DOM_VK_META: 224
};
export { Keys };
-5
View File
@@ -1,5 +0,0 @@
const KnownAppVersions = {
KeePassXC: '2.6.4'
};
export { KnownAppVersions };
-39
View File
@@ -1,39 +0,0 @@
const Links = {
Homepage: 'https://keeweb.info',
Repo: 'https://github.com/keeweb/keeweb',
Desktop: 'https://github.com/keeweb/keeweb/releases/latest',
WebApp: 'https://app.keeweb.info',
BetaWebApp: 'https://beta.keeweb.info',
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE',
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/',
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
UpdateJson: 'https://app.keeweb.info/update.json',
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
Translation: 'https://keeweb.oneskyapp.com/',
Donation: 'https://opencollective.com/keeweb#support',
Plugins: 'https://plugins.keeweb.info',
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
HaveIBeenPwned: 'https://haveibeenpwned.com',
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
ExtensionHelpForOtherBrowsers:
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
KWCForChrome:
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
KWCForEdge:
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094',
KPXCForChrome:
'https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk',
KPXCForFirefox: 'https://addons.mozilla.org/firefox/addon/keepassxc-browser/',
KPXCForEdge:
'https://microsoftedge.microsoft.com/addons/detail/keepassxcbrowser/pdffhmdngciaglkoonimfcmckehcpafo'
};
export { Links };
-10
View File
@@ -1,10 +0,0 @@
const RuntimeInfo = {
version: '@@VERSION',
beta: !!'@@BETA',
buildDate: '@@DATE',
commit: '@@COMMIT',
devMode: '@@DEVMODE',
appleTeamId: '@@APPLE_TEAM_ID'
};
export { RuntimeInfo };
-25
View File
@@ -1,25 +0,0 @@
const Timeouts = {
CopyTip: 1500,
AutoHideHint: 3000,
FileChangeSync: 3000,
BeforeAutoLock: 300,
CheckWindowClosed: 300,
OtpFadeDuration: 10000,
AutoTypeAfterHide: 100,
AutoTypeWindowFocusAfterBlur: 500,
DropDownClickWait: 500,
RedrawInactiveWindow: 50,
PopupWaitTime: 1000,
AutoUpdatePluginsAfterStart: 500,
LinkDownloadRevoke: 10 * 1000 * 60,
DefaultHttpRequest: 60000,
ExternalDeviceReconnect: 3000,
ExternalDeviceAfterReconnect: 1000,
FieldLabelDoubleClick: 300,
NativeModuleHostRestartTime: 3000,
FastAnimation: 100,
AutoTypeCopyPaste: 300,
KeeWebConnectRequest: 60000
};
export { Timeouts };
-251
View File
@@ -1,251 +0,0 @@
import EventEmitter from 'events';
const SymbolEvents = Symbol('events');
const SymbolArray = Symbol('array');
function emitSet(target, value, prevValue) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
const updates = { added: [], removed: [] };
if (prevValue) {
emitter.emit('remove', prevValue, target);
updates.removed.push(prevValue);
}
if (value) {
emitter.emit('add', value, target);
updates.added.push(value);
}
emitter.emit('change', updates, target);
}
}
function emitRemoved(target, removed) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
for (const item of removed) {
emitter.emit('remove', item, target);
}
emitter.emit('change', { added: [], removed }, target);
}
}
function checkType(target, value) {
const modelClass = target.constructor.model;
if (!modelClass) {
throw new Error(`Model type not defined for ${target.constructor.name}`);
}
if (!(value instanceof modelClass)) {
const valueType = value && value.constructor ? value.constructor.name : typeof value;
throw new Error(`Attempt to write ${valueType} into ${target.constructor.name}`);
}
}
const ProxyDef = {
set(target, property, value) {
const numProp = parseInt(property);
if (isNaN(numProp)) {
target[property] = value;
return true;
}
checkType(target, value);
const array = target[SymbolArray];
const prevValue = array[property];
if (prevValue !== value) {
array[property] = value;
emitSet(target, value, prevValue);
}
return true;
},
get(target, property) {
if (typeof property !== 'string') {
return target[property];
}
const numProp = parseInt(property);
if (isNaN(numProp)) {
return target[property];
}
return target[SymbolArray][property];
}
};
class Collection {
constructor(items) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
const properties = {
[SymbolEvents]: { value: emitter },
[SymbolArray]: { value: [] }
};
Object.defineProperties(this, properties);
if (items) {
this.push(...items);
}
return new Proxy(this, ProxyDef);
}
get length() {
return this[SymbolArray].length;
}
set length(value) {
const array = this[SymbolArray];
let removed;
if (value < array.length) {
removed = array.slice(value);
}
array.length = value;
if (removed) {
emitRemoved(this, removed);
}
}
push(...items) {
if (items.length) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
this[SymbolArray].push(...items);
this[SymbolEvents].paused = false;
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
this[SymbolEvents].emit('change', { added: items, removed: [] }, this);
}
}
pop() {
this[SymbolEvents].paused = true;
const item = this[SymbolArray].pop();
this[SymbolEvents].paused = false;
if (item) {
this[SymbolEvents].emit('remove', item, this);
this[SymbolEvents].emit('change', { added: [], removed: [item] }, this);
}
return item;
}
shift() {
this[SymbolEvents].paused = true;
const item = this[SymbolArray].shift();
this[SymbolEvents].paused = false;
if (item) {
this[SymbolEvents].emit('remove', item, this);
this[SymbolEvents].emit('change', { added: [], removed: [item] }, this);
}
return item;
}
unshift(...items) {
if (items.length) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
this[SymbolArray].unshift(...items);
this[SymbolEvents].paused = false;
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
this[SymbolEvents].emit('change', { added: items, removed: [] }, this);
}
}
splice(start, deleteCount, ...items) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
const removed = this[SymbolArray].splice(start, deleteCount, ...items);
this[SymbolEvents].paused = false;
for (const item of removed) {
this[SymbolEvents].emit('remove', item, this);
}
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
if (removed.length || items.length) {
this[SymbolEvents].emit('change', { added: items, removed }, this);
}
}
on(eventName, listener) {
this[SymbolEvents].on(eventName, listener);
}
once(eventName, listener) {
this[SymbolEvents].once(eventName, listener);
}
off(eventName, listener) {
this[SymbolEvents].off(eventName, listener);
}
get(id) {
return this.find((model) => model.id === id);
}
remove(idOrModel) {
for (let i = 0; i < this.length; i++) {
while (i < this.length && (this[i].id === idOrModel || this[i] === idOrModel)) {
this.splice(i, 1);
}
}
}
sort() {
return this[SymbolArray].sort(this.comparator);
}
fill() {
throw new Error('Not implemented');
}
copyWithin() {
throw new Error('Not implemented');
}
toJSON() {
return this[SymbolArray].concat();
}
}
const ProxiedArrayMethods = [
Symbol.iterator,
'concat',
'entries',
'every',
'filter',
'find',
'findIndex',
'flat',
'flatMap',
'forEach',
'includes',
'indexOf',
'join',
'keys',
'lastIndexOf',
'map',
'reduce',
'reduceRight',
'reverse',
'slice',
'some',
'values'
];
for (const method of ProxiedArrayMethods) {
Object.defineProperty(Collection.prototype, method, {
value: function proxyMethod(...args) {
return this[SymbolArray][method](...args);
}
});
}
export { Collection };
-12
View File
@@ -1,12 +0,0 @@
import EventEmitter from 'events';
class Events extends EventEmitter {
constructor() {
super();
this.setMaxListeners(1000);
}
}
const instance = new Events();
export { instance as Events };
-126
View File
@@ -1,126 +0,0 @@
import EventEmitter from 'events';
import { Logger } from 'util/logger';
const SymbolEvents = Symbol('events');
const SymbolDefaults = Symbol('defaults');
const SymbolExtensions = Symbol('extensions');
function emitPropChange(target, property, value, prevValue) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
emitter.emit('change:' + property, target, value, prevValue);
if (!emitter.noChange) {
emitter.emit('change', target, { [property]: value });
}
}
}
const ProxyDef = {
deleteProperty(target, property) {
if (Object.prototype.hasOwnProperty.call(target, property)) {
const defaults = target[SymbolDefaults];
const value = defaults[property];
const prevValue = target[property];
if (prevValue !== value) {
if (Object.prototype.hasOwnProperty.call(defaults, property)) {
target[property] = value;
} else {
delete target[property];
}
emitPropChange(target, property, value, prevValue);
}
return true;
}
return true;
},
set(target, property, value, receiver) {
if (Object.prototype.hasOwnProperty.call(target, property) || target[SymbolExtensions]) {
if (target[property] !== value) {
const prevValue = target[property];
target[property] = value;
emitPropChange(target, property, value, prevValue);
}
return true;
} else {
new Logger(receiver.constructor.name).warn(
`Unknown property: ${property}`,
new Error().stack
);
}
return false;
}
};
class Model {
constructor(data) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
const properties = {
[SymbolEvents]: { value: emitter }
};
for (const [propName, defaultValue] of Object.entries(this[SymbolDefaults])) {
properties[propName] = {
configurable: true,
enumerable: true,
writable: true,
value: defaultValue
};
}
Object.defineProperties(this, properties);
const object = new Proxy(this, ProxyDef);
if (data) {
object.set(data, { silent: true });
}
return object;
}
set(props, { silent } = {}) {
const emitter = this[SymbolEvents];
if (silent) {
emitter.paused = true;
}
emitter.noChange = true;
for (const [prop, value] of Object.entries(props)) {
this[prop] = value;
}
emitter.noChange = false;
if (silent) {
emitter.paused = false;
} else {
emitter.emit('change', this, props);
}
}
on(eventName, listener) {
this[SymbolEvents].on(eventName, listener);
}
once(eventName, listener) {
this[SymbolEvents].once(eventName, listener);
}
off(eventName, listener) {
this[SymbolEvents].off(eventName, listener);
}
emit(eventName, ...args) {
this[SymbolEvents].emit(eventName, ...args);
}
static defineModelProperties(properties, options) {
this.prototype[SymbolDefaults] = { ...this.prototype[SymbolDefaults], ...properties };
if (options && options.extensions) {
this.prototype[SymbolExtensions] = true;
}
}
static set(properties) {
this.prototype[SymbolDefaults] = properties;
}
}
export { Model };
-48
View File
@@ -1,48 +0,0 @@
import { Events } from 'framework/events';
import { AppSettingsModel } from 'models/app-settings-model';
import { Locale } from 'util/locale';
import { Tip } from 'util/ui/tip';
import { Timeouts } from 'const/timeouts';
const Copyable = {
hideFieldCopyTip() {
if (this.fieldCopyTip) {
this.fieldCopyTip.hide();
this.fieldCopyTip = null;
}
},
fieldCopied(e) {
this.hideFieldCopyTip();
const fieldLabel = e.source.labelEl;
const clipboardTime = e.copyRes.seconds;
const msg = clipboardTime
? Locale.detFieldCopiedTime.replace('{}', clipboardTime)
: Locale.detFieldCopied;
let tip;
if (!this.isHidden()) {
tip = Tip.createTip(fieldLabel[0], {
title: msg,
placement: 'right',
fast: true,
force: true,
noInit: true
});
this.fieldCopyTip = tip;
tip.show();
}
setTimeout(() => {
if (tip) {
tip.hide();
}
this.fieldCopyTip = null;
if (e.source.model.name === '$Password' && AppSettingsModel.lockOnCopy) {
setTimeout(() => {
Events.emit('lock-workspace');
}, Timeouts.BeforeAutoLock);
}
}, Timeouts.CopyTip);
}
};
export { Copyable };
-69
View File
@@ -1,69 +0,0 @@
import { Events } from 'framework/events';
const Resizable = {
listenDrag(dragView) {
this.listenTo(dragView, 'dragstart', this.dragStart);
this.listenTo(dragView, 'drag', this.drag);
this.listenTo(dragView, 'autosize', this.autoSize);
},
dragStart(e) {
this._dragInfo = this.getDragInfo(e.coord);
},
drag(e) {
const dragInfo = this._dragInfo;
let size = dragInfo.startSize + e.offset;
size = Math.max(dragInfo.min, Math.min(dragInfo.max, size));
this.$el[dragInfo.prop](size);
this.emit('view-resize', size);
Events.emit('page-geometry', { source: 'resizable' });
},
autoSize(e) {
const dragInfo = this.getDragInfo(e.coord);
if (dragInfo.auto !== undefined) {
this.$el.css(dragInfo.prop, dragInfo.auto);
} else {
this.$el.css(dragInfo.prop, '');
}
this.fixSize(dragInfo);
this.emit('view-resize', null);
Events.emit('page-geometry', { source: 'resizable' });
},
fixSize(cfg) {
const size = this.$el[cfg.prop]();
const newSize = Math.max(cfg.min, Math.min(cfg.max, size));
if (newSize !== size) {
this.$el[cfg.prop](size);
}
},
// TODO: check size on window resize
// checkSize: function() {
// if (this.maxWidth) {
// this.fixSize(this.getDragInfo('x'));
// }
// if (this.maxHeight) {
// this.fixSize(this.getDragInfo('y'));
// }
// },
getDragInfo(coord) {
const prop = coord === 'x' ? 'Width' : 'Height';
const propLower = prop.toLowerCase();
const min = this.getSizeProp('min' + prop);
const max = this.getSizeProp('max' + prop);
const auto = this.getSizeProp('auto' + prop);
const startSize = this.$el[propLower]();
return { startSize, prop: propLower, min, max, auto };
},
getSizeProp(prop) {
const member = this[prop];
return typeof member === 'function' ? member.call(this) : member;
}
};
export { Resizable };
-61
View File
@@ -1,61 +0,0 @@
import baron from 'baron';
import { Events } from 'framework/events';
import { Features } from 'util/features';
const isEnabled = !Features.isMobile;
const SymbolRemoveScrollListenerAdded = Symbol('removeScrollAdded');
const Scrollable = {
createScroll(opts) {
// opts.cssGuru = true;
if (isEnabled) {
if (this.scroll) {
this.removeScroll();
}
this.scroll = baron(opts);
if (!this[SymbolRemoveScrollListenerAdded]) {
this.once('remove', () => this.removeScroll);
this[SymbolRemoveScrollListenerAdded] = true;
}
}
this.scroller = this.$el.find('.scroller');
this.scrollerBar = this.$el.find('.scroller__bar');
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
},
removeScroll() {
if (this.scroll) {
try {
this.scroll.dispose();
} catch {}
this.scroll = null;
}
},
pageResized() {
// TODO: check size on window resize
// if (this.checkSize && (!e || e.source === 'window')) {
// this.checkSize();
// }
if (this.scroll) {
this.scroll.update();
requestAnimationFrame(() => {
if (this.scroll) {
this.scroll.update();
const barHeight = Math.round(this.scrollerBar.height());
const wrapperHeight = Math.round(this.scrollerBarWrapper.height());
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
}
});
}
},
initScroll() {
if (isEnabled) {
this.listenTo(Events, 'page-geometry', this.pageResized);
}
}
};
export { Scrollable };
-290
View File
@@ -1,290 +0,0 @@
import morphdom from 'morphdom';
import EventEmitter from 'events';
import { Tip } from 'util/ui/tip';
import { KeyHandler } from 'comp/browser/key-handler';
import { FocusManager } from 'comp/app/focus-manager';
import { Logger } from 'util/logger';
const DoesNotBubble = {
mouseenter: true,
mouseleave: true,
blur: true,
focus: true
};
const DefaultTemplateOptions = {
allowProtoPropertiesByDefault: true,
allowedProtoProperties: { length: true, active: true }
};
class View extends EventEmitter {
parent = undefined;
template = undefined;
events = {};
model = undefined;
options = {};
views = {};
hidden = undefined;
removed = false;
modal = undefined;
eventListeners = {};
elementEventListeners = [];
debugLogger = localStorage.debugView ? new Logger('view', this.constructor.name) : undefined;
constructor(model = undefined, options = {}) {
super();
this.model = model;
this.options = options;
this.setMaxListeners(100);
}
render(templateData) {
if (this.removed) {
return;
}
let ts;
if (this.debugLogger) {
this.debugLogger.debug('Render start');
ts = this.debugLogger.ts();
}
if (this.el) {
Tip.destroyTips(this.el);
}
this.renderElement(templateData);
Tip.createTips(this.el);
this.debugLogger?.debug('Render finished', this.debugLogger.ts(ts));
return this;
}
renderElement(templateData) {
const html = this.template(templateData, DefaultTemplateOptions);
if (this.el) {
const mountRoot = this.options.ownParent ? this.el.firstChild : this.el;
morphdom(mountRoot, html);
this.bindElementEvents();
} else {
let parent = this.options.parent || this.parent;
if (parent) {
if (typeof parent === 'string') {
parent = document.querySelector(parent);
}
if (!parent) {
throw new Error(`Error rendering ${this.constructor.name}: parent not found`);
}
if (this.options.replace) {
Tip.destroyTips(parent);
parent.innerHTML = '';
}
const el = document.createElement('div');
el.innerHTML = html;
const root = el.firstChild;
if (this.options.ownParent) {
if (root) {
parent.appendChild(root);
}
this.el = parent;
} else {
this.el = root;
parent.appendChild(this.el);
}
if (this.modal) {
FocusManager.setModal(this.modal);
}
this.bindEvents();
} else {
throw new Error(
`Error rendering ${this.constructor.name}: I don't know how to insert the view`
);
}
this.$el = $(this.el); // legacy
}
}
bindEvents() {
const eventsMap = {};
for (const [eventDef, method] of Object.entries(this.events)) {
const spaceIx = eventDef.indexOf(' ');
let event, selector;
if (spaceIx > 0) {
event = eventDef.substr(0, spaceIx);
selector = eventDef.substr(spaceIx + 1);
if (DoesNotBubble[event]) {
this.elementEventListeners.push({ event, selector, method, els: [] });
continue;
}
} else {
event = eventDef;
}
if (!eventsMap[event]) {
eventsMap[event] = [];
}
eventsMap[event].push({ selector, method });
}
for (const [event, handlers] of Object.entries(eventsMap)) {
this.debugLogger?.debug('Bind', 'view', event, handlers);
const listener = (e) => this.eventListener(e, handlers);
this.eventListeners[event] = listener;
this.el.addEventListener(event, listener);
}
this.bindElementEvents();
}
unbindEvents() {
for (const [event, listener] of Object.entries(this.eventListeners)) {
this.el.removeEventListener(event, listener);
}
this.unbindElementEvents();
}
bindElementEvents() {
if (!this.elementEventListeners.length) {
return;
}
this.unbindElementEvents();
for (const cfg of this.elementEventListeners) {
const els = this.el.querySelectorAll(cfg.selector);
this.debugLogger?.debug('Bind', 'element', cfg.event, cfg.selector, els.length);
cfg.listener = (e) => this.eventListener(e, [cfg]);
for (const el of els) {
el.addEventListener(cfg.event, cfg.listener);
cfg.els.push(el);
}
}
}
unbindElementEvents() {
if (!this.elementEventListeners.length) {
return;
}
for (const cfg of this.elementEventListeners) {
for (const el of cfg.els) {
el.removeEventListener(cfg.event, cfg.listener);
}
cfg.els = [];
}
}
eventListener(e, handlers) {
this.debugLogger?.debug('Listener fired', e.type);
for (const { selector, method } of handlers) {
if (selector) {
const closest = e.target.closest(selector);
if (!closest || !this.el.contains(closest)) {
continue;
}
}
if (!this[method]) {
this.debugLogger?.debug('Method not defined', method);
continue;
}
this.debugLogger?.debug('Handling event', e.type, method);
this[method](e);
}
}
remove() {
if (this.modal && FocusManager.modal === this.modal) {
FocusManager.setModal(null);
}
this.emit('remove');
this.removeInnerViews();
Tip.hideTips(this.el);
this.el.remove();
this.removed = true;
this.debugLogger?.debug('Remove');
}
removeInnerViews() {
if (this.views) {
for (const view of Object.values(this.views)) {
if (view) {
if (view instanceof Array) {
view.forEach((v) => v.remove());
} else {
view.remove();
}
}
}
this.views = {};
}
}
listenTo(model, event, callback) {
const boundCallback = callback.bind(this);
model.on(event, boundCallback);
this.once('remove', () => model.off(event, boundCallback));
}
hide() {
Tip.hideTips(this.el);
return this.toggle(false);
}
show() {
return this.toggle(true);
}
toggle(visible) {
this.debugLogger?.debug(visible ? 'Show' : 'Hide');
if (visible === undefined) {
visible = this.hidden;
}
if (this.hidden === !visible) {
this.debugLogger?.debug('Toggle: noop', visible);
return;
}
this.hidden = !visible;
if (this.modal) {
if (visible) {
FocusManager.setModal(this.modal);
} else if (FocusManager.modal === this.modal) {
FocusManager.setModal(null);
}
}
if (this.el) {
this.el.classList.toggle('show', !!visible);
this.el.classList.toggle('hide', !visible);
if (!visible) {
Tip.hideTips(this.el);
}
}
this.emit(visible ? 'show' : 'hide');
}
isHidden() {
return !!this.hidden;
}
isVisible() {
return !this.hidden;
}
afterPaint(callback) {
requestAnimationFrame(() => requestAnimationFrame(callback));
}
onKey(key, handler, shortcut, modal, noPrevent) {
KeyHandler.onKey(key, handler, this, shortcut, modal, noPrevent);
this.once('remove', () => KeyHandler.offKey(key, handler, this));
}
off(event, listener) {
if (listener === undefined) {
return super.removeAllListeners(event);
} else {
return super.off(event, listener);
}
}
}
export { View, DefaultTemplateOptions };
-4
View File
@@ -1,4 +0,0 @@
import Handlebars from 'hbs';
// inspired by https://stackoverflow.com/questions/22103989/adding-offset-to-index-when-looping-through-items-in-handlebars/39588001#39588001
Handlebars.registerHelper('add', (lvalue, rvalue) => parseInt(lvalue) + parseInt(rvalue));
-28
View File
@@ -1,28 +0,0 @@
import Handlebars from 'hbs';
Handlebars.registerHelper('cmp', function (lvalue, rvalue, op, options) {
let cond;
switch (op) {
case '<':
cond = lvalue < rvalue;
break;
case '>':
cond = lvalue > rvalue;
break;
case '>=':
cond = lvalue >= rvalue;
break;
case '<=':
cond = lvalue <= rvalue;
break;
case '===':
case '==':
cond = lvalue === rvalue;
break;
case '!==':
case '!=':
cond = lvalue !== rvalue;
break;
}
return cond ? options.fn(this) : options.inverse(this);
});

Some files were not shown because too many files have changed in this diff Show More