Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd73ad740 | ||
|
|
0ceb5875fa | ||
|
|
e5bd2dcf85 | ||
|
|
e7625fbe23 | ||
|
|
878afe6af7 | ||
|
|
26aad741ff | ||
|
|
0cdf3ab847 | ||
|
|
7f95d2d4a4 | ||
|
|
9990002744 | ||
|
|
9d3ae2531b | ||
|
|
f503671bf0 | ||
|
|
cc99f830fa | ||
|
|
7aaff0464d | ||
|
|
8cbf042b08 | ||
|
|
a3df687c17 | ||
|
|
a5e631d7d0 | ||
|
|
34b7c9c6fc | ||
|
|
304d32552d | ||
|
|
b638b3ce46 | ||
|
|
bf2e74f32b | ||
|
|
2629b59035 | ||
|
|
789422861c | ||
|
|
f4ecc1780c | ||
|
|
1e2a8e0294 | ||
|
|
e2a4846bf6 | ||
|
|
037f45441d | ||
|
|
a2829d12f0 | ||
|
|
31b1b63f03 | ||
|
|
803866f370 | ||
|
|
fae73e4f98 | ||
|
|
e781e5a184 | ||
|
|
1bcb920f38 | ||
|
|
9d3d3ab446 | ||
|
|
80a956a602 | ||
|
|
b8cad5bad7 | ||
|
|
97e0916b8c | ||
|
|
63789c42ba | ||
|
|
e2879ace1a | ||
|
|
e6909f919d | ||
|
|
9dc3c65e78 | ||
|
|
3727fdcfc7 | ||
|
|
8dcd0a7fbf | ||
|
|
c397832062 | ||
|
|
aa240e5915 | ||
|
|
410d6aa9c5 | ||
|
|
0d288881f9 | ||
|
|
241ea7ade8 | ||
|
|
2ff97d9f3e | ||
|
|
c1b1796d6c | ||
|
|
863ac3859d | ||
|
|
f50337dc14 | ||
|
|
15a9db8704 | ||
|
|
ecadb9ffca | ||
|
|
5d11e29cd0 | ||
|
|
6dd5d97970 | ||
|
|
72b6aed3bc | ||
|
|
943446bd39 | ||
|
|
d2e9de710f | ||
|
|
622a1ababe | ||
|
|
fa049c840f | ||
|
|
c703afd074 | ||
|
|
6f3ef907e3 | ||
|
|
043693d4a4 | ||
|
|
2321d42e67 | ||
|
|
a451b4ec78 | ||
|
|
86f9af9576 | ||
|
|
0654349021 | ||
|
|
ef89003f04 | ||
|
|
a459a4f20d | ||
|
|
dea266ef4d | ||
|
|
34e954ee23 | ||
|
|
fb7c8da4b0 | ||
|
|
c2ae28e450 | ||
|
|
376a1b3865 | ||
|
|
36436de1cf | ||
|
|
6888df45ba | ||
|
|
80f65aeb0e | ||
|
|
01dd0295c2 | ||
|
|
a337b0a131 | ||
|
|
b5f5d54ff2 | ||
|
|
17536ba0b6 | ||
|
|
a61600118f | ||
|
|
1445ef273f | ||
|
|
e62adb4ddf | ||
|
|
ce9277ca95 | ||
|
|
7c6935cf35 | ||
|
|
a86de235c9 | ||
|
|
cbb3e89646 | ||
|
|
ff30286472 | ||
|
|
6edff27c5d | ||
|
|
75812fef67 | ||
|
|
96c8d07a57 | ||
|
|
e7e23bb319 | ||
|
|
a53a1e4c81 | ||
|
|
b0e4e96a61 | ||
|
|
2d48166d30 | ||
|
|
d0fa63045e | ||
|
|
4aa6bc1d95 | ||
|
|
7467a16d9b | ||
|
|
6f3fa8328d | ||
|
|
86cd6e9468 | ||
|
|
dd4d71a713 | ||
|
|
657fedaf26 | ||
|
|
f474b62966 | ||
|
|
efb7789806 | ||
|
|
489a2f31f1 | ||
|
|
a3b76c410d | ||
|
|
3811518fb9 | ||
|
|
69d942edad | ||
|
|
2ae7023e2d | ||
|
|
6a153fa2f4 | ||
|
|
23d30de921 | ||
|
|
2bbc61c2f5 | ||
|
|
ca33f4255f | ||
|
|
975f620698 | ||
|
|
b827dbb557 | ||
|
|
b621ee7ffb | ||
|
|
6fe581333d | ||
|
|
10d2455b9a | ||
|
|
d79a6178b7 | ||
|
|
864061c839 | ||
|
|
6c37774795 | ||
|
|
9dd0cba100 | ||
|
|
44de83b497 | ||
|
|
bb878a7004 | ||
|
|
7a5a25241c | ||
|
|
a9305839a9 | ||
|
|
73b7868ee4 | ||
|
|
4c1d451d24 | ||
|
|
0532ce2901 | ||
|
|
1fa31c449b | ||
|
|
329a4e76d2 | ||
|
|
39c06fa368 | ||
|
|
15c40708e1 | ||
|
|
bef16e8114 | ||
|
|
2d3db5583c | ||
|
|
a457d98fe4 | ||
|
|
9c99db6579 | ||
|
|
29ebef03ed | ||
|
|
04c6147ae7 | ||
|
|
83fe3d269b | ||
|
|
b241813f95 | ||
|
|
aaf304aa99 | ||
|
|
3d26658ecc | ||
|
|
e6da3c4281 | ||
|
|
fbeba063cc | ||
|
|
6805b0a6b5 | ||
|
|
1e75ac210d | ||
|
|
c51b3c6b6c | ||
|
|
c9e75b2097 | ||
|
|
b8f74fe737 | ||
|
|
0afc1ec8cf | ||
|
|
be37b586e8 |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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
@@ -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/
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"quoteProps": "preserve",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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).
|
||||
@@ -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>
|
||||
@@ -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.
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"import/no-commonjs": "error"
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,5 +0,0 @@
|
||||
const DragDropInfo = {
|
||||
dragObject: null
|
||||
};
|
||||
|
||||
export { DragDropInfo };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,12 +0,0 @@
|
||||
const BuiltInFields = [
|
||||
'Title',
|
||||
'Password',
|
||||
'UserName',
|
||||
'URL',
|
||||
'Notes',
|
||||
'TOTP Seed',
|
||||
'TOTP Settings',
|
||||
'_etm_template_uuid'
|
||||
];
|
||||
|
||||
export { BuiltInFields };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,2 +0,0 @@
|
||||
export const KeeWebLogo =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';
|
||||
@@ -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 };
|
||||
@@ -1,5 +0,0 @@
|
||||
const KnownAppVersions = {
|
||||
KeePassXC: '2.6.4'
|
||||
};
|
||||
|
||||
export { KnownAppVersions };
|
||||
@@ -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 };
|
||||
@@ -1,10 +0,0 @@
|
||||
const RuntimeInfo = {
|
||||
version: '@@VERSION',
|
||||
beta: !!'@@BETA',
|
||||
buildDate: '@@DATE',
|
||||
commit: '@@COMMIT',
|
||||
devMode: '@@DEVMODE',
|
||||
appleTeamId: '@@APPLE_TEAM_ID'
|
||||
};
|
||||
|
||||
export { RuntimeInfo };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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));
|
||||
@@ -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
Reference in New Issue
Block a user