mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
Merge bc5875f101 into 6bd8360261
This commit is contained in:
commit
ca8a56d19d
39 changed files with 1223 additions and 690 deletions
131
release-tool.py
131
release-tool.py
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
import ctypes
|
import ctypes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import lzma
|
import lzma
|
||||||
import os
|
import os
|
||||||
|
|
@ -33,7 +35,8 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
from urllib.request import urlretrieve
|
from urllib import request
|
||||||
|
from xml import sax
|
||||||
|
|
||||||
|
|
||||||
###########################################################################################
|
###########################################################################################
|
||||||
|
|
@ -447,6 +450,7 @@ class Check(Command):
|
||||||
if checkout:
|
if checkout:
|
||||||
_git_checkout(git_ref, cwd=src_dir)
|
_git_checkout(git_ref, cwd=src_dir)
|
||||||
logger.debug('Attempting to find "%s" version string in source files...', version)
|
logger.debug('Attempting to find "%s" version string in source files...', version)
|
||||||
|
cls.check_version_in_vcpkg_manifest(version, src_dir)
|
||||||
cls.check_version_in_cmake(version, src_dir)
|
cls.check_version_in_cmake(version, src_dir)
|
||||||
cls.check_changelog(version, src_dir)
|
cls.check_changelog(version, src_dir)
|
||||||
cls.check_app_stream_info(version, src_dir)
|
cls.check_app_stream_info(version, src_dir)
|
||||||
|
|
@ -460,32 +464,32 @@ class Check(Command):
|
||||||
raise Error(f'Source directory "{src_dir}" does not exist!')
|
raise Error(f'Source directory "{src_dir}" does not exist!')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_git_repository(cwd):
|
def check_git_repository(cwd=None):
|
||||||
if _run(['git', 'rev-parse', '--is-inside-work-tree'], check=False, cwd=cwd).returncode != 0:
|
if _run(['git', 'rev-parse', '--is-inside-work-tree'], check=False, cwd=cwd).returncode != 0:
|
||||||
raise Error('Not a valid Git repository: %s', cwd)
|
raise Error('Not a valid Git repository: %s', cwd)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_release_exists(tag_name, cwd):
|
def check_release_exists(tag_name, cwd=None):
|
||||||
if not _run(['git', 'tag', '--list', tag_name], check=False, cwd=cwd).stdout:
|
if not _run(['git', 'tag', '--list', tag_name], check=False, cwd=cwd).stdout:
|
||||||
raise Error('Release tag does not exists: %s', tag_name)
|
raise Error('Release tag does not exists: %s', tag_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_release_does_not_exist(tag_name, cwd):
|
def check_release_does_not_exist(tag_name, cwd=None):
|
||||||
if _run(['git', 'tag', '--list', tag_name], check=False, cwd=cwd).stdout:
|
if _run(['git', 'tag', '--list', tag_name], check=False, cwd=cwd).stdout:
|
||||||
raise Error('Release tag already exists: %s', tag_name)
|
raise Error('Release tag already exists: %s', tag_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_working_tree_clean(cwd):
|
def check_working_tree_clean(cwd=None):
|
||||||
if not _git_working_dir_clean(cwd=cwd):
|
if not _git_working_dir_clean(cwd=cwd):
|
||||||
raise Error('Current working tree is not clean! Please commit or unstage any changes.')
|
raise Error('Current working tree is not clean! Please commit or unstage any changes.')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_branch_exists(branch, cwd):
|
def check_branch_exists(branch, cwd=None):
|
||||||
if _run(['git', 'rev-parse', branch], check=False, cwd=cwd).returncode != 0:
|
if _run(['git', 'rev-parse', branch], check=False, cwd=cwd).returncode != 0:
|
||||||
raise Error(f'Branch or tag "{branch}" does not exist!')
|
raise Error(f'Branch or tag "{branch}" does not exist!')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_version_in_cmake(version, cwd):
|
def check_version_in_cmake(version, cwd=None):
|
||||||
cmakelists = Path('CMakeLists.txt')
|
cmakelists = Path('CMakeLists.txt')
|
||||||
if cwd:
|
if cwd:
|
||||||
cmakelists = Path(cwd) / cmakelists
|
cmakelists = Path(cwd) / cmakelists
|
||||||
|
|
@ -500,7 +504,17 @@ class Check(Command):
|
||||||
raise Error(f'Version number in {cmakelists} not updated! Expected: %s, found: %s.', version, cmake_version)
|
raise Error(f'Version number in {cmakelists} not updated! Expected: %s, found: %s.', version, cmake_version)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_changelog(version, cwd):
|
def check_version_in_vcpkg_manifest(version, cwd=None):
|
||||||
|
manifest = Path('vcpkg.json')
|
||||||
|
if cwd:
|
||||||
|
manifest = Path(cwd) / manifest
|
||||||
|
manifest_json = json.load(manifest.open('r'))
|
||||||
|
if version != manifest_json['version-string']:
|
||||||
|
raise Error(f'Version number in {manifest} not updated! Expected: %s, found: %s.',
|
||||||
|
version, manifest_json['version-string'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_changelog(version, cwd=None):
|
||||||
changelog = Path('CHANGELOG.md')
|
changelog = Path('CHANGELOG.md')
|
||||||
if cwd:
|
if cwd:
|
||||||
changelog = Path(cwd) / changelog
|
changelog = Path(cwd) / changelog
|
||||||
|
|
@ -511,12 +525,21 @@ class Check(Command):
|
||||||
raise Error(f'{changelog} has not been updated to the "%s" release.', version)
|
raise Error(f'{changelog} has not been updated to the "%s" release.', version)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_app_stream_info(version, cwd):
|
def check_app_stream_info(version, cwd=None):
|
||||||
appstream = Path('share/linux/org.keepassxc.KeePassXC.appdata.xml')
|
appstream = Path('share/linux/org.keepassxc.KeePassXC.appdata.xml')
|
||||||
if cwd:
|
if cwd:
|
||||||
appstream = Path(cwd) / appstream
|
appstream = Path(cwd) / appstream
|
||||||
if not appstream.is_file():
|
if not appstream.is_file():
|
||||||
raise Error('File not found: %s', appstream)
|
raise Error('File not found: %s', appstream)
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = sax.make_parser()
|
||||||
|
parser.setContentHandler(sax.handler.ContentHandler())
|
||||||
|
parser.parse(appstream)
|
||||||
|
except sax.SAXParseException as e:
|
||||||
|
raise Error(f'{appstream} is not well-formed. Error: %s at line %s, column %s',
|
||||||
|
e.getMessage(), e.getLineNumber(), e.getColumnNumber())
|
||||||
|
|
||||||
regex = re.compile(rf'^\s*<release version="{version}" date=".+?">')
|
regex = re.compile(rf'^\s*<release version="{version}" date=".+?">')
|
||||||
with appstream.open('r', encoding='utf-8') as f:
|
with appstream.open('r', encoding='utf-8') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
|
|
@ -572,8 +595,7 @@ class Tag(Command):
|
||||||
# Update translations
|
# Update translations
|
||||||
if not skip_translations:
|
if not skip_translations:
|
||||||
i18n = I18N(self._arg_parser)
|
i18n = I18N(self._arg_parser)
|
||||||
i18n.run_tx_pull(src_dir, i18n.derive_resource_name(tx_resource, cwd=src_dir), tx_min_perc,
|
i18n.run_tx_pull(src_dir, tx_resource, tx_min_perc, commit=True, yes=yes)
|
||||||
commit=True, yes=yes)
|
|
||||||
|
|
||||||
changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ',
|
changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ',
|
||||||
(Path(src_dir) / 'CHANGELOG.md').read_text("UTF-8"), re.MULTILINE | re.DOTALL)
|
(Path(src_dir) / 'CHANGELOG.md').read_text("UTF-8"), re.MULTILINE | re.DOTALL)
|
||||||
|
|
@ -808,7 +830,7 @@ class Build(Command):
|
||||||
if _run(['which', toolname], cwd=None, check=False, **(docker_args or {})).returncode != 0:
|
if _run(['which', toolname], cwd=None, check=False, **(docker_args or {})).returncode != 0:
|
||||||
logger.info(f'Downloading {toolname}...')
|
logger.info(f'Downloading {toolname}...')
|
||||||
outfile = bin_dir / toolname
|
outfile = bin_dir / toolname
|
||||||
urlretrieve(url, outfile)
|
request.urlretrieve(url, outfile)
|
||||||
outfile.chmod(outfile.stat().st_mode | stat.S_IEXEC)
|
outfile.chmod(outfile.stat().st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
def build_linux(self, version, src_dir, output_dir, *, install_prefix, parallelism, cmake_opts, use_system_deps,
|
def build_linux(self, version, src_dir, output_dir, *, install_prefix, parallelism, cmake_opts, use_system_deps,
|
||||||
|
|
@ -1048,7 +1070,7 @@ class GPGSign(Command):
|
||||||
class I18N(Command):
|
class I18N(Command):
|
||||||
"""Update translation files and pull from or push to Transifex."""
|
"""Update translation files and pull from or push to Transifex."""
|
||||||
|
|
||||||
TRANSIFEX_RESOURCE = 'keepassxc.share-translations-keepassxc-en-ts--{}'
|
TRANSIFEX_RESOURCE = 'share-translations-keepassxc-en-ts--{}'
|
||||||
TRANSIFEX_PULL_PERC = 60
|
TRANSIFEX_PULL_PERC = 60
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1070,6 +1092,15 @@ class I18N(Command):
|
||||||
pull.add_argument('-y', '--yes', help='Don\'t ask before pulling translations.', action='store_true')
|
pull.add_argument('-y', '--yes', help='Don\'t ask before pulling translations.', action='store_true')
|
||||||
pull.add_argument('tx_args', help='Additional arguments to pass to tx subcommand.', nargs=argparse.REMAINDER)
|
pull.add_argument('tx_args', help='Additional arguments to pass to tx subcommand.', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
list_translators = subparsers.add_parser('tx-list-translators',
|
||||||
|
help='Print a HTML-formatted list of translation contributors.')
|
||||||
|
list_translators.add_argument('-o', '--org', help='Transifex org name.', default='keepassxc')
|
||||||
|
list_translators.add_argument('-p', '--project', help='Transifex project name.', default='keepassxc')
|
||||||
|
list_translators.add_argument('-r', '--resource', help='Transifex resource name.',
|
||||||
|
choices=['master', 'develop'])
|
||||||
|
list_translators.add_argument('-b', '--member-blacklist', nargs='+', help='Transifex users to ignore',
|
||||||
|
default=['phoerious', 'droidmonkey'])
|
||||||
|
|
||||||
lupdate = subparsers.add_parser('lupdate', help='Update source translation file from C++ sources.')
|
lupdate = subparsers.add_parser('lupdate', help='Update source translation file from C++ sources.')
|
||||||
lupdate.add_argument('-d', '--build-dir', help='Build directory for looking up lupdate binary.')
|
lupdate.add_argument('-d', '--build-dir', help='Build directory for looking up lupdate binary.')
|
||||||
lupdate.add_argument('-c', '--commit', help='Commit changes.', action='store_true')
|
lupdate.add_argument('-c', '--commit', help='Commit changes.', action='store_true')
|
||||||
|
|
@ -1112,12 +1143,15 @@ class I18N(Command):
|
||||||
self.check_transifex_cmd_exists()
|
self.check_transifex_cmd_exists()
|
||||||
self.check_transifex_config_exists(src_dir)
|
self.check_transifex_config_exists(src_dir)
|
||||||
|
|
||||||
kwargs['resource'] = self.derive_resource_name(kwargs['resource'], cwd=src_dir)
|
if 'tx_args' in kwargs:
|
||||||
kwargs['tx_args'] = kwargs['tx_args'][1:]
|
kwargs['tx_args'] = kwargs['tx_args'][1:]
|
||||||
if subcmd == 'tx-push':
|
if subcmd == 'tx-push':
|
||||||
self.run_tx_push(src_dir, **kwargs)
|
self.run_tx_push(src_dir, **kwargs)
|
||||||
elif subcmd == 'tx-pull':
|
elif subcmd == 'tx-pull':
|
||||||
self.run_tx_pull(src_dir, **kwargs)
|
self.run_tx_pull(src_dir, **kwargs)
|
||||||
|
elif subcmd == 'tx-list-translators':
|
||||||
|
self.run_tx_list_translators(src_dir, kwargs['org'], kwargs['project'], kwargs['resource'],
|
||||||
|
kwargs['member_blacklist'])
|
||||||
|
|
||||||
elif subcmd == 'lupdate':
|
elif subcmd == 'lupdate':
|
||||||
kwargs['lupdate_args'] = kwargs['lupdate_args'][1:]
|
kwargs['lupdate_args'] = kwargs['lupdate_args'][1:]
|
||||||
|
|
@ -1137,6 +1171,7 @@ class I18N(Command):
|
||||||
|
|
||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
def run_tx_push(self, src_dir, resource, yes, tx_args):
|
def run_tx_push(self, src_dir, resource, yes, tx_args):
|
||||||
|
resource = 'keepassxc.' + self.derive_resource_name(resource, cwd=src_dir)
|
||||||
sys.stderr.write('\nAbout to push the ' + fmt.bold('"en"') +
|
sys.stderr.write('\nAbout to push the ' + fmt.bold('"en"') +
|
||||||
' source file from the current branch to Transifex:\n')
|
' source file from the current branch to Transifex:\n')
|
||||||
sys.stderr.write(f' {fmt.bold(_git_get_branch(cwd=src_dir))}'
|
sys.stderr.write(f' {fmt.bold(_git_get_branch(cwd=src_dir))}'
|
||||||
|
|
@ -1151,6 +1186,7 @@ class I18N(Command):
|
||||||
|
|
||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
def run_tx_pull(self, src_dir, resource, min_perc, commit=False, yes=False, tx_args=None):
|
def run_tx_pull(self, src_dir, resource, min_perc, commit=False, yes=False, tx_args=None):
|
||||||
|
resource = 'keepassxc.' + self.derive_resource_name(resource, cwd=src_dir)
|
||||||
sys.stderr.write('\nAbout to pull translations for ' + fmt.bold(f'"{resource}"') + '.\n')
|
sys.stderr.write('\nAbout to pull translations for ' + fmt.bold(f'"{resource}"') + '.\n')
|
||||||
if not yes and not _yes_no_prompt('Continue?'):
|
if not yes and not _yes_no_prompt('Continue?'):
|
||||||
logger.error('Pull aborted.')
|
logger.error('Pull aborted.')
|
||||||
|
|
@ -1164,6 +1200,71 @@ class I18N(Command):
|
||||||
if commit:
|
if commit:
|
||||||
_git_commit_files(files, 'Update translations.', cwd=src_dir)
|
_git_commit_files(files, 'Update translations.', cwd=src_dir)
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
def run_tx_list_translators(self, src_dir, org, project, resource, member_blacklist):
|
||||||
|
txrc = Path.home() / '.transifexrc'
|
||||||
|
if not txrc.exists():
|
||||||
|
raise Error('No Transifex config found. Run tx init first.')
|
||||||
|
|
||||||
|
org = f'o:{org}'
|
||||||
|
project = f'{org}:p:{project}'
|
||||||
|
resource = f'{project}:r:{self.derive_resource_name(resource, cwd=src_dir)}'
|
||||||
|
|
||||||
|
token = [l for l in open(txrc, 'r') if l.startswith('token')][0].split('=', 1)[1].strip()
|
||||||
|
member_blacklist = [f'u:{m}' for m in member_blacklist]
|
||||||
|
|
||||||
|
def get_url(url):
|
||||||
|
req = request.Request(url)
|
||||||
|
req.add_header('Content-Type', 'application/vnd.api+json')
|
||||||
|
req.add_header('Authorization', f'Bearer {token}')
|
||||||
|
with request.urlopen(req) as resp:
|
||||||
|
return json.load(resp)
|
||||||
|
|
||||||
|
logger.info('Fetching languages...',)
|
||||||
|
languages_json = get_url(f'https://rest.api.transifex.com/projects/{project}/languages')
|
||||||
|
languages = {}
|
||||||
|
for lang in languages_json['data']:
|
||||||
|
languages[lang['id']] = lang['attributes']['name']
|
||||||
|
|
||||||
|
logger.info('Fetching language stats...')
|
||||||
|
language_stats_json = get_url('https://rest.api.transifex.com/resource_language_stats?'
|
||||||
|
f'filter[project]={project}&filter[resource]={resource}')
|
||||||
|
for s in language_stats_json['data']:
|
||||||
|
completion = s['attributes']['translated_strings'] / s['attributes']['total_strings']
|
||||||
|
if completion < .6:
|
||||||
|
languages.pop(s['relationships']['language']['data']['id'])
|
||||||
|
|
||||||
|
logger.info('Fetching language members...')
|
||||||
|
members_json = get_url(f'https://rest.api.transifex.com/team_memberships?filter[organization]={org}')
|
||||||
|
members = defaultdict(set)
|
||||||
|
for member in members_json['data']:
|
||||||
|
print('.', end='', file=sys.stderr)
|
||||||
|
sys.stderr.flush()
|
||||||
|
if member['relationships']['user']['data']['id'] in member_blacklist:
|
||||||
|
continue
|
||||||
|
lid = member['relationships']['language']['data']['id']
|
||||||
|
if lid not in languages:
|
||||||
|
continue
|
||||||
|
user = get_url(member['relationships']['user']['links']['related'])['data']['attributes']['username']
|
||||||
|
members[lid].add(user)
|
||||||
|
print(file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
print('<ul>')
|
||||||
|
for lang in sorted(languages, key=lambda x: languages[x]):
|
||||||
|
if not members[lang]:
|
||||||
|
continue
|
||||||
|
lines = [f' <li><strong>{languages[lang]}:</strong> ']
|
||||||
|
for i, m in enumerate(sorted(members[lang], key=lambda x: x.lower())):
|
||||||
|
if len(lines[-1]) + len(m) >= 120:
|
||||||
|
lines.append(' ')
|
||||||
|
lines[-1] += m
|
||||||
|
if i < len(members[lang]) - 1:
|
||||||
|
lines[-1] += ', '
|
||||||
|
lines[-1] += '</li>'
|
||||||
|
print('\n'.join(lines))
|
||||||
|
print('</ul>')
|
||||||
|
logger.info('Done. Please add the list to the About dialog and commit the changes.')
|
||||||
|
|
||||||
def run_lupdate(self, src_dir, build_dir=None, commit=False, lupdate_args=None):
|
def run_lupdate(self, src_dir, build_dir=None, commit=False, lupdate_args=None):
|
||||||
path = _get_bin_path(build_dir)
|
path = _get_bin_path(build_dir)
|
||||||
self.check_lupdate_exists(path)
|
self.check_lupdate_exists(path)
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ namespace
|
||||||
{"f14", Qt::Key_F14},
|
{"f14", Qt::Key_F14},
|
||||||
{"f15", Qt::Key_F15},
|
{"f15", Qt::Key_F15},
|
||||||
{"f16", Qt::Key_F16}};
|
{"f16", Qt::Key_F16}};
|
||||||
|
constexpr int s_minWaitDelay = 100; // 100 ms
|
||||||
|
constexpr int s_maxWaitDelay = 10000; // 10 seconds
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AutoType* AutoType::m_instance = nullptr;
|
AutoType* AutoType::m_instance = nullptr;
|
||||||
|
|
@ -312,6 +314,9 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
|
||||||
// Restore executor mode
|
// Restore executor mode
|
||||||
m_executor->mode = mode;
|
m_executor->mode = mode;
|
||||||
|
|
||||||
|
// Initial Auto-Type delay to allow window to come to foreground
|
||||||
|
Tools::wait(qBound(s_minWaitDelay, config()->get(Config::AutoTypeStartDelay).toInt(), s_maxWaitDelay));
|
||||||
|
|
||||||
// Grab the current active window after everything settles
|
// Grab the current active window after everything settles
|
||||||
if (window == 0) {
|
if (window == 0) {
|
||||||
window = m_plugin->activeWindow();
|
window = m_plugin->activeWindow();
|
||||||
|
|
@ -543,16 +548,16 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
|
||||||
}
|
}
|
||||||
|
|
||||||
const int maxTypeDelay = 500;
|
const int maxTypeDelay = 500;
|
||||||
const int maxWaitDelay = 10000;
|
|
||||||
const int maxRepetition = 100;
|
const int maxRepetition = 100;
|
||||||
|
|
||||||
int currentTypingDelay = qBound(0, config()->get(Config::AutoTypeDelay).toInt(), maxTypeDelay);
|
int currentTypingDelay = qBound(0, config()->get(Config::AutoTypeDelay).toInt(), maxTypeDelay);
|
||||||
int cumulativeDelay = qBound(0, config()->get(Config::AutoTypeStartDelay).toInt(), maxWaitDelay);
|
// Take into account the initial delay which is added before any actions are performed
|
||||||
|
int cumulativeDelay = qBound(s_minWaitDelay, config()->get(Config::AutoTypeStartDelay).toInt(), s_maxWaitDelay);
|
||||||
|
|
||||||
// Initial actions include start delay and initial inter-key delay
|
// Initial actions include start delay and initial inter-key delay
|
||||||
QList<QSharedPointer<AutoTypeAction>> actions;
|
QList<QSharedPointer<AutoTypeAction>> actions;
|
||||||
actions << QSharedPointer<AutoTypeBegin>::create();
|
actions << QSharedPointer<AutoTypeBegin>::create();
|
||||||
actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
|
actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
|
||||||
actions << QSharedPointer<AutoTypeDelay>::create(cumulativeDelay);
|
|
||||||
|
|
||||||
// Replace escaped braces with a template for easier regex
|
// Replace escaped braces with a template for easier regex
|
||||||
QString sequence = entrySequence;
|
QString sequence = entrySequence;
|
||||||
|
|
@ -631,12 +636,12 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
|
||||||
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
|
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
|
||||||
} else if (placeholder == "delay") {
|
} else if (placeholder == "delay") {
|
||||||
// Mid typing delay (wait), repeat represents the desired delay in milliseconds
|
// Mid typing delay (wait), repeat represents the desired delay in milliseconds
|
||||||
if (repeat > maxWaitDelay) {
|
if (repeat > s_maxWaitDelay) {
|
||||||
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
|
error = tr("Very long delay detected, max is %1: %2").arg(s_maxWaitDelay).arg(fullPlaceholder);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
cumulativeDelay += repeat;
|
cumulativeDelay += repeat;
|
||||||
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
|
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, s_maxWaitDelay));
|
||||||
} else if (placeholder == "clearfield") {
|
} else if (placeholder == "clearfield") {
|
||||||
// Platform-specific field clearing
|
// Platform-specific field clearing
|
||||||
actions << QSharedPointer<AutoTypeClearField>::create();
|
actions << QSharedPointer<AutoTypeClearField>::create();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
* Copyright (C) 2013 Francois Ferrand
|
* Copyright (C) 2013 Francois Ferrand
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -53,55 +53,72 @@ void BrowserAccessControlDialog::setEntries(const QList<Entry*>& entriesToConfir
|
||||||
QUrl url(urlString);
|
QUrl url(urlString);
|
||||||
m_ui->siteLabel->setText(m_ui->siteLabel->text().arg(
|
m_ui->siteLabel->setText(m_ui->siteLabel->text().arg(
|
||||||
url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment)));
|
url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment)));
|
||||||
|
m_ui->siteLabel->setToolTip(urlString);
|
||||||
|
|
||||||
m_ui->rememberDecisionCheckBox->setVisible(!httpAuth);
|
m_ui->rememberDecisionCheckBox->setVisible(!httpAuth);
|
||||||
m_ui->rememberDecisionCheckBox->setChecked(false);
|
m_ui->rememberDecisionCheckBox->setChecked(false);
|
||||||
|
|
||||||
m_ui->itemsTable->setRowCount(entriesToConfirm.count());
|
m_ui->itemsTable->setRowCount(entriesToConfirm.count());
|
||||||
m_ui->itemsTable->setColumnCount(2);
|
m_ui->itemsTable->setColumnCount(3);
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (const auto& entry : entriesToConfirm) {
|
for (const auto& entry : entriesToConfirm) {
|
||||||
addEntryToList(entry, row);
|
addEntryToList(entry, row);
|
||||||
++row;
|
++row;
|
||||||
}
|
}
|
||||||
m_ui->itemsTable->resizeColumnsToContents();
|
|
||||||
m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||||
|
m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||||
m_ui->itemsTable->selectAll();
|
m_ui->itemsTable->selectAll();
|
||||||
m_ui->allowButton->setFocus();
|
m_ui->allowButton->setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserAccessControlDialog::addEntryToList(Entry* entry, int row)
|
void BrowserAccessControlDialog::addEntryToList(const Entry* entry, int row)
|
||||||
{
|
{
|
||||||
auto item = new QTableWidgetItem();
|
const auto titleItem = new QTableWidgetItem();
|
||||||
item->setText(entry->resolveMultiplePlaceholders(entry->title()) + " - "
|
const auto entryTitle = entry->resolveMultiplePlaceholders(entry->title());
|
||||||
+ entry->resolveMultiplePlaceholders(entry->username()));
|
const auto entryUrl = entry->resolveMultiplePlaceholders(entry->url());
|
||||||
item->setData(Qt::UserRole, row);
|
titleItem->setText(entryTitle);
|
||||||
item->setFlags(item->flags() | Qt::ItemIsSelectable);
|
titleItem->setToolTip(entryUrl);
|
||||||
m_ui->itemsTable->setItem(row, 0, item);
|
titleItem->setData(Qt::UserRole, row);
|
||||||
|
titleItem->setFlags(titleItem->flags() | Qt::ItemIsSelectable);
|
||||||
|
m_ui->itemsTable->setItem(row, 0, titleItem);
|
||||||
|
|
||||||
|
const auto usernameItem = new QTableWidgetItem();
|
||||||
|
const auto entryUsername = entry->resolveMultiplePlaceholders(entry->username());
|
||||||
|
usernameItem->setText(entryUsername);
|
||||||
|
usernameItem->setData(Qt::UserRole, row);
|
||||||
|
m_ui->itemsTable->setItem(row, 1, usernameItem);
|
||||||
|
|
||||||
auto disableButton = new QPushButton();
|
auto disableButton = new QPushButton();
|
||||||
disableButton->setIcon(icons()->icon("entry-delete"));
|
disableButton->setIcon(icons()->icon("entry-delete"));
|
||||||
disableButton->setToolTip(tr("Disable for this site"));
|
disableButton->setToolTip(tr("Disable for this site"));
|
||||||
|
|
||||||
connect(disableButton, &QAbstractButton::pressed, [&, item, disableButton] {
|
connect(disableButton, &QAbstractButton::pressed, [&, titleItem, usernameItem, disableButton] {
|
||||||
auto font = item->font();
|
auto font = titleItem->font();
|
||||||
if (item->flags() == Qt::NoItemFlags) {
|
if (titleItem->flags() == Qt::NoItemFlags) {
|
||||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
titleItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||||
item->setSelected(true);
|
usernameItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||||
|
titleItem->setSelected(true);
|
||||||
|
usernameItem->setSelected(true);
|
||||||
|
|
||||||
font.setStrikeOut(false);
|
font.setStrikeOut(false);
|
||||||
item->setFont(font);
|
titleItem->setFont(font);
|
||||||
|
usernameItem->setFont(font);
|
||||||
|
|
||||||
disableButton->setIcon(icons()->icon("entry-delete"));
|
disableButton->setIcon(icons()->icon("entry-delete"));
|
||||||
disableButton->setToolTip(tr("Disable for this site"));
|
disableButton->setToolTip(tr("Disable for this site"));
|
||||||
m_ui->rememberDecisionCheckBox->setEnabled(true);
|
m_ui->rememberDecisionCheckBox->setEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
item->setFlags(Qt::NoItemFlags);
|
titleItem->setFlags(Qt::NoItemFlags);
|
||||||
item->setSelected(false);
|
usernameItem->setFlags(Qt::NoItemFlags);
|
||||||
|
titleItem->setSelected(false);
|
||||||
|
usernameItem->setSelected(false);
|
||||||
|
|
||||||
font.setStrikeOut(true);
|
font.setStrikeOut(true);
|
||||||
item->setFont(font);
|
titleItem->setFont(font);
|
||||||
|
usernameItem->setFont(font);
|
||||||
|
|
||||||
disableButton->setIcon(icons()->icon("entry-restore"));
|
disableButton->setIcon(icons()->icon("entry-restore"));
|
||||||
disableButton->setToolTip(tr("Undo"));
|
disableButton->setToolTip(tr("Undo"));
|
||||||
|
|
@ -112,7 +129,7 @@ void BrowserAccessControlDialog::addEntryToList(Entry* entry, int row)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
m_ui->itemsTable->setCellWidget(row, 1, disableButton);
|
m_ui->itemsTable->setCellWidget(row, 2, disableButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BrowserAccessControlDialog::remember() const
|
bool BrowserAccessControlDialog::remember() const
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
* Copyright (C) 2013 Francois Ferrand
|
* Copyright (C) 2013 Francois Ferrand
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -55,7 +55,7 @@ private slots:
|
||||||
void selectionChanged();
|
void selectionChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addEntryToList(Entry* entry, int row);
|
void addEntryToList(const Entry* entry, int row);
|
||||||
bool areAllDisabled() const;
|
bool areAllDisabled() const;
|
||||||
QList<QTableWidgetItem*> getAllItems() const;
|
QList<QTableWidgetItem*> getAllItems() const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<string>%1 is requesting access to the following entries:</string>
|
<string>%1 is requesting access to the following entries:</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignCenter</set>
|
<set>Qt::AlignLeft</set>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -79,7 +79,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
|
||||||
|
|
||||||
// Credential private key
|
// Credential private key
|
||||||
const auto alg = getAlgorithmFromPublicKey(credentialCreationOptions);
|
const auto alg = getAlgorithmFromPublicKey(credentialCreationOptions);
|
||||||
const auto privateKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
|
const auto privateKey = buildCredentialPrivateKey(alg, testingVariables);
|
||||||
if (privateKey.cborEncodedPublicKey.isEmpty() && privateKey.privateKeyPem.isEmpty()) {
|
if (privateKey.cborEncodedPublicKey.isEmpty() && privateKey.privateKeyPem.isEmpty()) {
|
||||||
// Key creation failed
|
// Key creation failed
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -103,6 +103,9 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
|
||||||
|
|
||||||
// Additions for extension side functions
|
// Additions for extension side functions
|
||||||
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
|
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
|
||||||
|
|
||||||
|
// PublicKey
|
||||||
|
responseObject["publicKey"] = browserMessageBuilder()->getBase64FromArray(privateKey.spkiPublicKey);
|
||||||
responseObject["publicKeyAlgorithm"] = alg;
|
responseObject["publicKeyAlgorithm"] = alg;
|
||||||
|
|
||||||
// PublicKeyCredential
|
// PublicKeyCredential
|
||||||
|
|
@ -122,14 +125,16 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
|
||||||
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
|
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
|
||||||
const QString& credentialId,
|
const QString& credentialId,
|
||||||
const QString& userHandle,
|
const QString& userHandle,
|
||||||
const QString& privateKeyPem)
|
const QString& privateKeyPem,
|
||||||
|
const bool beFlag,
|
||||||
|
const bool bsFlag)
|
||||||
{
|
{
|
||||||
if (!passkeyUtils()->checkCredentialAssertionOptions(assertionOptions)) {
|
if (!passkeyUtils()->checkCredentialAssertionOptions(assertionOptions)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto authenticatorData =
|
const auto authenticatorData = buildAuthenticatorData(
|
||||||
buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString());
|
assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString(), beFlag, bsFlag);
|
||||||
const auto clientDataJson = assertionOptions["clientDataJson"].toString();
|
const auto clientDataJson = assertionOptions["clientDataJson"].toString();
|
||||||
const auto clientDataArray = clientDataJson.toUtf8();
|
const auto clientDataArray = clientDataJson.toUtf8();
|
||||||
|
|
||||||
|
|
@ -168,8 +173,12 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential
|
||||||
result.append(rpIdHash);
|
result.append(rpIdHash);
|
||||||
|
|
||||||
// Use default flags
|
// Use default flags
|
||||||
const auto flags = setFlagsFromJson(QJsonObject(
|
const auto flags = setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
|
||||||
{{"ED", !extensions.isEmpty()}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
|
{"AT", true},
|
||||||
|
{"BS", DEFAULT_BS_FLAG},
|
||||||
|
{"BE", DEFAULT_BE_FLAG},
|
||||||
|
{"UV", true},
|
||||||
|
{"UP", true}}));
|
||||||
result.append(flags);
|
result.append(flags);
|
||||||
|
|
||||||
// Signature counter (not supported, always 0
|
// Signature counter (not supported, always 0
|
||||||
|
|
@ -201,7 +210,10 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a short version of the attestation object for webauthn.get
|
// Build a short version of the attestation object for webauthn.get
|
||||||
QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QString& extensions)
|
QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId,
|
||||||
|
const QString& extensions,
|
||||||
|
const bool beFlag,
|
||||||
|
const bool bsFlag)
|
||||||
{
|
{
|
||||||
QByteArray result;
|
QByteArray result;
|
||||||
|
|
||||||
|
|
@ -209,7 +221,7 @@ QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QS
|
||||||
result.append(rpIdHash);
|
result.append(rpIdHash);
|
||||||
|
|
||||||
const auto flags = setFlagsFromJson(QJsonObject(
|
const auto flags = setFlagsFromJson(QJsonObject(
|
||||||
{{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
|
{{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", bsFlag}, {"BE", beFlag}, {"UV", true}, {"UP", true}}));
|
||||||
result.append(flags);
|
result.append(flags);
|
||||||
|
|
||||||
// Signature counter (not supported, always 0
|
// Signature counter (not supported, always 0
|
||||||
|
|
@ -224,8 +236,7 @@ QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QS
|
||||||
}
|
}
|
||||||
|
|
||||||
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
|
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
|
||||||
AttestationKeyPair
|
AttestationKeyPair BrowserPasskeys::buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables)
|
||||||
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
|
|
||||||
{
|
{
|
||||||
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
|
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
|
||||||
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
|
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
|
||||||
|
|
@ -234,21 +245,31 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
|
||||||
|
|
||||||
QByteArray firstPart;
|
QByteArray firstPart;
|
||||||
QByteArray secondPart;
|
QByteArray secondPart;
|
||||||
|
QByteArray spki;
|
||||||
QByteArray pem;
|
QByteArray pem;
|
||||||
|
|
||||||
if (!predefinedFirst.isEmpty() && !predefinedSecond.isEmpty()) {
|
if (!testingVariables.first.isEmpty() && !testingVariables.second.isEmpty()) {
|
||||||
firstPart = browserMessageBuilder()->getArrayFromBase64(predefinedFirst);
|
firstPart = browserMessageBuilder()->getArrayFromBase64(testingVariables.first);
|
||||||
secondPart = browserMessageBuilder()->getArrayFromBase64(predefinedSecond);
|
secondPart = browserMessageBuilder()->getArrayFromBase64(testingVariables.second);
|
||||||
} else {
|
} else {
|
||||||
if (alg == WebAuthnAlgorithms::ES256) {
|
if (alg == WebAuthnAlgorithms::ES256) {
|
||||||
try {
|
try {
|
||||||
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"));
|
// Use predefined data if found (only for testing private key creation)
|
||||||
|
const auto keyData = !testingVariables.data.isEmpty()
|
||||||
|
? Botan::BigInt(testingVariables.data.toStdString())
|
||||||
|
: Botan::BigInt(0);
|
||||||
|
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"), keyData);
|
||||||
const auto& publicPoint = privateKey.public_point();
|
const auto& publicPoint = privateKey.public_point();
|
||||||
auto x = publicPoint.get_affine_x();
|
auto x = publicPoint.get_affine_x();
|
||||||
auto y = publicPoint.get_affine_y();
|
auto y = publicPoint.get_affine_y();
|
||||||
firstPart = bigIntToQByteArray(x);
|
firstPart = bigIntToQByteArray(x);
|
||||||
secondPart = bigIntToQByteArray(y);
|
secondPart = bigIntToQByteArray(y);
|
||||||
|
|
||||||
|
auto publicKey =
|
||||||
|
Botan::ECDSA_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
|
||||||
|
auto publicKeySpki = publicKey.subject_public_key();
|
||||||
|
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
|
||||||
|
|
||||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||||||
pem = QByteArray::fromStdString(privateKeyPem);
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
|
|
@ -263,6 +284,10 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
|
||||||
firstPart = bigIntToQByteArray(modulus);
|
firstPart = bigIntToQByteArray(modulus);
|
||||||
secondPart = bigIntToQByteArray(exponent);
|
secondPart = bigIntToQByteArray(exponent);
|
||||||
|
|
||||||
|
auto publicKey = Botan::RSA_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
|
||||||
|
auto publicKeySpki = publicKey.subject_public_key();
|
||||||
|
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
|
||||||
|
|
||||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||||||
pem = QByteArray::fromStdString(privateKeyPem);
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
|
|
@ -271,17 +296,22 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
|
||||||
}
|
}
|
||||||
} else if (alg == WebAuthnAlgorithms::EDDSA) {
|
} else if (alg == WebAuthnAlgorithms::EDDSA) {
|
||||||
try {
|
try {
|
||||||
Botan::Ed25519_PrivateKey key(*randomGen()->getRng());
|
Botan::Ed25519_PrivateKey privateKey(*randomGen()->getRng());
|
||||||
auto publicKey = key.get_public_key();
|
auto publicKeyBits = privateKey.get_public_key();
|
||||||
#ifdef WITH_XC_BOTAN3
|
#ifdef WITH_XC_BOTAN3
|
||||||
auto privateKey = key.raw_private_key_bits();
|
auto privateKeyBits = privateKey.raw_private_key_bits();
|
||||||
#else
|
#else
|
||||||
auto privateKey = key.get_private_key();
|
auto privateKeyBits = privateKey.get_private_key();
|
||||||
#endif
|
#endif
|
||||||
firstPart = browserMessageBuilder()->getQByteArray(publicKey.data(), publicKey.size());
|
firstPart = browserMessageBuilder()->getQByteArray(publicKeyBits.data(), publicKeyBits.size());
|
||||||
secondPart = browserMessageBuilder()->getQByteArray(privateKey.data(), privateKey.size());
|
secondPart = browserMessageBuilder()->getQByteArray(privateKeyBits.data(), privateKeyBits.size());
|
||||||
|
|
||||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(key);
|
auto publicKey =
|
||||||
|
Botan::Ed25519_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
|
||||||
|
auto publicKeySpki = publicKey.subject_public_key();
|
||||||
|
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
|
||||||
|
|
||||||
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||||||
pem = QByteArray::fromStdString(privateKeyPem);
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
|
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
|
||||||
|
|
@ -299,6 +329,7 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
|
||||||
AttestationKeyPair attestationKeyPair;
|
AttestationKeyPair attestationKeyPair;
|
||||||
attestationKeyPair.cborEncodedPublicKey = result;
|
attestationKeyPair.cborEncodedPublicKey = result;
|
||||||
attestationKeyPair.privateKeyPem = pem;
|
attestationKeyPair.privateKeyPem = pem;
|
||||||
|
attestationKeyPair.spkiPublicKey = spki;
|
||||||
return attestationKeyPair;
|
return attestationKeyPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
#include <botan/asn1_obj.h>
|
#include <botan/asn1_obj.h>
|
||||||
#include <botan/bigint.h>
|
#include <botan/bigint.h>
|
||||||
|
|
||||||
|
#define DEFAULT_BE_FLAG true
|
||||||
|
#define DEFAULT_BS_FLAG true
|
||||||
#define ID_BYTES 32
|
#define ID_BYTES 32
|
||||||
#define HASH_BYTES 32
|
#define HASH_BYTES 32
|
||||||
#define RSA_BITS 2048
|
#define RSA_BITS 2048
|
||||||
|
|
@ -61,6 +63,7 @@ struct AttestationKeyPair
|
||||||
{
|
{
|
||||||
QByteArray cborEncodedPublicKey;
|
QByteArray cborEncodedPublicKey;
|
||||||
QByteArray privateKeyPem;
|
QByteArray privateKeyPem;
|
||||||
|
QByteArray spkiPublicKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Predefined variables used for testing the class
|
// Predefined variables used for testing the class
|
||||||
|
|
@ -69,6 +72,7 @@ struct TestingVariables
|
||||||
QString credentialId;
|
QString credentialId;
|
||||||
QString first;
|
QString first;
|
||||||
QString second;
|
QString second;
|
||||||
|
QString data;
|
||||||
};
|
};
|
||||||
|
|
||||||
class BrowserPasskeys : public QObject
|
class BrowserPasskeys : public QObject
|
||||||
|
|
@ -81,11 +85,13 @@ public:
|
||||||
static BrowserPasskeys* instance();
|
static BrowserPasskeys* instance();
|
||||||
|
|
||||||
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& credentialCreationOptions,
|
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& credentialCreationOptions,
|
||||||
const TestingVariables& predefinedVariables = {});
|
const TestingVariables& testingVariables = {});
|
||||||
QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
|
QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
|
||||||
const QString& credentialId,
|
const QString& credentialId,
|
||||||
const QString& userHandle,
|
const QString& userHandle,
|
||||||
const QString& privateKeyPem);
|
const QString& privateKeyPem,
|
||||||
|
const bool beFlag = DEFAULT_BE_FLAG,
|
||||||
|
const bool bsFlag = DEFAULT_BE_FLAG);
|
||||||
|
|
||||||
static const QString AAGUID;
|
static const QString AAGUID;
|
||||||
|
|
||||||
|
|
@ -110,11 +116,12 @@ private:
|
||||||
const QString& extensions,
|
const QString& extensions,
|
||||||
const QString& credentialId,
|
const QString& credentialId,
|
||||||
const QByteArray& cborEncodedPublicKey,
|
const QByteArray& cborEncodedPublicKey,
|
||||||
const TestingVariables& predefinedVariables = {});
|
const TestingVariables& testingVariables = {});
|
||||||
QByteArray buildAuthenticatorData(const QString& rpId, const QString& extensions);
|
QByteArray buildAuthenticatorData(const QString& rpId,
|
||||||
AttestationKeyPair buildCredentialPrivateKey(int alg,
|
const QString& extensions,
|
||||||
const QString& predefinedFirst = QString(),
|
const bool beFlag = DEFAULT_BE_FLAG,
|
||||||
const QString& predefinedSecond = QString());
|
const bool bsFlag = DEFAULT_BE_FLAG);
|
||||||
|
AttestationKeyPair buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables = {});
|
||||||
QByteArray
|
QByteArray
|
||||||
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
|
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
|
||||||
QJsonObject parseAuthData(const QByteArray& authData) const;
|
QJsonObject parseAuthData(const QByteArray& authData) const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
|
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
|
||||||
* Copyright (C) 2013 Francois Ferrand
|
* Copyright (C) 2013 Francois Ferrand
|
||||||
*
|
*
|
||||||
|
|
@ -774,8 +774,16 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
|
||||||
const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
|
const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
|
||||||
const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
|
const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
|
||||||
|
|
||||||
auto publicKeyCredential =
|
// Get BE and BS flags if present
|
||||||
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
|
const auto beFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BE)
|
||||||
|
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == TRUE_STR
|
||||||
|
: DEFAULT_BE_FLAG;
|
||||||
|
const auto bsFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BS)
|
||||||
|
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == TRUE_STR
|
||||||
|
: DEFAULT_BS_FLAG;
|
||||||
|
|
||||||
|
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(
|
||||||
|
assertionOptions, credentialId, userHandle, privateKeyPem, beFlag, bsFlag);
|
||||||
if (publicKeyCredential.isEmpty()) {
|
if (publicKeyCredential.isEmpty()) {
|
||||||
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
|
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
|
||||||
}
|
}
|
||||||
|
|
@ -855,6 +863,8 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
|
||||||
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
|
||||||
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
|
||||||
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
|
||||||
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BE, TRUE_STR);
|
||||||
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, TRUE_STR);
|
||||||
entry->addTag(tr("Passkey"));
|
entry->addTag(tr("Passkey"));
|
||||||
|
|
||||||
entry->endUpdate();
|
entry->endUpdate();
|
||||||
|
|
@ -1012,8 +1022,8 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& group : rootGroup->groupsRecursive(true)) {
|
for (const auto& group : rootGroup->groupsRecursive(true)) {
|
||||||
if (group->isRecycled()
|
const auto groupOptionHideEntry = group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY);
|
||||||
|| group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable) {
|
if (group->isRecycled() || groupOptionHideEntry == Group::Enable) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1028,7 +1038,8 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
|
||||||
|
|
||||||
for (auto* entry : group->entries()) {
|
for (auto* entry : group->entries()) {
|
||||||
if (entry->isRecycled()
|
if (entry->isRecycled()
|
||||||
|| (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
|
|| (groupOptionHideEntry == Group::Inherit
|
||||||
|
&& entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
|
||||||
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
|
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -149,7 +149,6 @@ void CustomData::copyDataFrom(const CustomData* other)
|
||||||
|
|
||||||
m_data = other->m_data;
|
m_data = other->m_data;
|
||||||
|
|
||||||
updateLastModified();
|
|
||||||
emit reset();
|
emit reset();
|
||||||
emitModified();
|
emitModified();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -46,6 +46,8 @@ const QString EntryAttributes::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX
|
||||||
const QString EntryAttributes::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
|
const QString EntryAttributes::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
|
||||||
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START = QStringLiteral("-----BEGIN PRIVATE KEY-----");
|
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START = QStringLiteral("-----BEGIN PRIVATE KEY-----");
|
||||||
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END = QStringLiteral("-----END PRIVATE KEY-----");
|
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END = QStringLiteral("-----END PRIVATE KEY-----");
|
||||||
|
const QString EntryAttributes::KPEX_PASSKEY_FLAG_BE = QStringLiteral("KPEX_PASSKEY_FLAG_BE");
|
||||||
|
const QString EntryAttributes::KPEX_PASSKEY_FLAG_BS = QStringLiteral("KPEX_PASSKEY_FLAG_BS");
|
||||||
|
|
||||||
// For compatibility with StrongBox
|
// For compatibility with StrongBox
|
||||||
const QString EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
|
const QString EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -75,6 +75,8 @@ public:
|
||||||
static const QString KPEX_PASSKEY_USER_HANDLE;
|
static const QString KPEX_PASSKEY_USER_HANDLE;
|
||||||
static const QString KPEX_PASSKEY_PRIVATE_KEY_START;
|
static const QString KPEX_PASSKEY_PRIVATE_KEY_START;
|
||||||
static const QString KPEX_PASSKEY_PRIVATE_KEY_END;
|
static const QString KPEX_PASSKEY_PRIVATE_KEY_END;
|
||||||
|
static const QString KPEX_PASSKEY_FLAG_BE;
|
||||||
|
static const QString KPEX_PASSKEY_FLAG_BS;
|
||||||
|
|
||||||
static bool isDefaultAttribute(const QString& key);
|
static bool isDefaultAttribute(const QString& key);
|
||||||
static bool isPasskeyAttribute(const QString& key);
|
static bool isPasskeyAttribute(const QString& key);
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,48 @@ namespace
|
||||||
return entry.take();
|
return entry.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Group* createGroup(Group* rootGroup, const QString& folderName)
|
||||||
|
{
|
||||||
|
Group* currentParentGroup = rootGroup;
|
||||||
|
Group* result = nullptr;
|
||||||
|
const auto groups = folderName.split("/", Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
// Returns the group name based on depth
|
||||||
|
const auto getGroupName = [&](const int depth) {
|
||||||
|
QString groupName;
|
||||||
|
for (int i = 0; i < depth + 1; ++i) {
|
||||||
|
groupName.append((i == 0 ? "" : "/") + groups[i]);
|
||||||
|
}
|
||||||
|
return groupName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new group(s) always when the path is not found
|
||||||
|
for (int i = 0; i < groups.length(); ++i) {
|
||||||
|
const auto groupName = getGroupName(i);
|
||||||
|
const auto tempGroup = rootGroup->findGroupByPath(groupName);
|
||||||
|
|
||||||
|
if (!tempGroup) {
|
||||||
|
const auto newGroup = new Group();
|
||||||
|
newGroup->setName(groups[i]);
|
||||||
|
newGroup->setUuid(QUuid::createUuid());
|
||||||
|
newGroup->setParent(currentParentGroup);
|
||||||
|
currentParentGroup = newGroup;
|
||||||
|
|
||||||
|
if (groupName == folderName) {
|
||||||
|
result = newGroup;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupName == folderName) {
|
||||||
|
result = tempGroup;
|
||||||
|
}
|
||||||
|
currentParentGroup = tempGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
|
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
|
||||||
{
|
{
|
||||||
auto folderField = QString("folders");
|
auto folderField = QString("folders");
|
||||||
|
|
@ -277,12 +319,12 @@ namespace
|
||||||
// Create groups from folders and store a temporary map of id -> uuid
|
// Create groups from folders and store a temporary map of id -> uuid
|
||||||
QMap<QString, Group*> folderMap;
|
QMap<QString, Group*> folderMap;
|
||||||
for (const auto& folder : vault.value(folderField).toArray()) {
|
for (const auto& folder : vault.value(folderField).toArray()) {
|
||||||
auto group = new Group();
|
const auto folderId = folder.toObject().value("id").toString();
|
||||||
group->setUuid(QUuid::createUuid());
|
const auto folderName = folder.toObject().value("name").toString();
|
||||||
group->setName(folder.toObject().value("name").toString());
|
|
||||||
group->setParent(db->rootGroup());
|
|
||||||
|
|
||||||
folderMap.insert(folder.toObject().value("id").toString(), group);
|
if (const auto group = createGroup(db->rootGroup(), folderName)) {
|
||||||
|
folderMap.insert(folderId, group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString folderId;
|
QString folderId;
|
||||||
|
|
|
||||||
|
|
@ -153,8 +153,6 @@ void Application::bootstrap(const QString& uiLanguage)
|
||||||
{
|
{
|
||||||
Bootstrap::bootstrap(uiLanguage);
|
Bootstrap::bootstrap(uiLanguage);
|
||||||
|
|
||||||
applyFontSize();
|
|
||||||
|
|
||||||
osUtils->registerNativeEventFilter();
|
osUtils->registerNativeEventFilter();
|
||||||
MessageBox::initializeButtonDefs();
|
MessageBox::initializeButtonDefs();
|
||||||
|
|
||||||
|
|
@ -200,6 +198,7 @@ void Application::applyTheme()
|
||||||
stylesheetFile.close();
|
stylesheetFile.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyFontSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::applyFontSize()
|
void Application::applyFontSize()
|
||||||
|
|
|
||||||
|
|
@ -392,6 +392,9 @@ void EntryPreviewWidget::updateEntryGeneralTab()
|
||||||
m_ui->entryNotesTextEdit->setFont(Font::defaultFont());
|
m_ui->entryNotesTextEdit->setFont(Font::defaultFont());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_ui->entryNotesTextEdit->setTabStopDistance(
|
||||||
|
QFontMetrics(m_ui->entryNotesTextEdit->font()).horizontalAdvance(QString(4, ' ')));
|
||||||
|
|
||||||
m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl().toHtmlEscaped());
|
m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl().toHtmlEscaped());
|
||||||
const QString url = m_currentEntry->url();
|
const QString url = m_currentEntry->url();
|
||||||
if (!url.isEmpty()) {
|
if (!url.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@
|
||||||
<widget class="QLabel" name="entryTotpLabel">
|
<widget class="QLabel" name="entryTotpLabel">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>10</pointsize>
|
|
||||||
<bold>true</bold>
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
|
|
@ -434,9 +433,6 @@
|
||||||
<property name="readOnly">
|
<property name="readOnly">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="tabStopDistance">
|
|
||||||
<double>10.000000000000000</double>
|
|
||||||
</property>
|
|
||||||
<property name="blendIn" stdset="0">
|
<property name="blendIn" stdset="0">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
||||||
|
|
@ -2031,7 +2031,6 @@ void MainWindow::initViewMenu()
|
||||||
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
|
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
|
||||||
} else {
|
} else {
|
||||||
kpxcApp->applyTheme();
|
kpxcApp->applyTheme();
|
||||||
kpxcApp->applyFontSize();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -351,25 +351,31 @@ void EditEntryWidget::updateBrowser()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto changeValue = [&](const QString& option, const bool newValue) {
|
||||||
|
// If value is false and no customData exists, make no edits
|
||||||
|
if (!m_customData->hasKey(option) && !newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If customData exists, set the value
|
||||||
|
m_customData->set(option, (newValue ? TRUE_STR : FALSE_STR));
|
||||||
|
};
|
||||||
|
|
||||||
// Only update the custom data if no group level settings are used (checkbox is enabled)
|
// Only update the custom data if no group level settings are used (checkbox is enabled)
|
||||||
if (m_browserUi->hideEntryCheckbox->isEnabled()) {
|
if (m_browserUi->hideEntryCheckbox->isEnabled()) {
|
||||||
auto hide = m_browserUi->hideEntryCheckbox->isChecked();
|
changeValue(BrowserService::OPTION_HIDE_ENTRY, m_browserUi->hideEntryCheckbox->isChecked());
|
||||||
m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? TRUE_STR : FALSE_STR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_browserUi->skipAutoSubmitCheckbox->isEnabled()) {
|
if (m_browserUi->skipAutoSubmitCheckbox->isEnabled()) {
|
||||||
auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked();
|
changeValue(BrowserService::OPTION_SKIP_AUTO_SUBMIT, m_browserUi->skipAutoSubmitCheckbox->isChecked());
|
||||||
m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? TRUE_STR : FALSE_STR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_browserUi->onlyHttpAuthCheckbox->isEnabled()) {
|
if (m_browserUi->onlyHttpAuthCheckbox->isEnabled()) {
|
||||||
auto onlyHttpAuth = m_browserUi->onlyHttpAuthCheckbox->isChecked();
|
changeValue(BrowserService::OPTION_ONLY_HTTP_AUTH, m_browserUi->onlyHttpAuthCheckbox->isChecked());
|
||||||
m_customData->set(BrowserService::OPTION_ONLY_HTTP_AUTH, (onlyHttpAuth ? TRUE_STR : FALSE_STR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_browserUi->notHttpAuthCheckbox->isEnabled()) {
|
if (m_browserUi->notHttpAuthCheckbox->isEnabled()) {
|
||||||
auto notHttpAuth = m_browserUi->notHttpAuthCheckbox->isChecked();
|
changeValue(BrowserService::OPTION_NOT_HTTP_AUTH, m_browserUi->notHttpAuthCheckbox->isChecked());
|
||||||
m_customData->set(BrowserService::OPTION_NOT_HTTP_AUTH, (notHttpAuth ? TRUE_STR : FALSE_STR));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -943,6 +949,9 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
||||||
m_mainUi->notesEdit->setFont(Font::defaultFont());
|
m_mainUi->notesEdit->setFont(Font::defaultFont());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_mainUi->notesEdit->setTabStopDistance(
|
||||||
|
QFontMetrics(m_mainUi->notesEdit->font()).horizontalAdvance(QString(4, ' ')));
|
||||||
|
|
||||||
m_advancedUi->attachmentsWidget->setReadOnly(m_history);
|
m_advancedUi->attachmentsWidget->setReadOnly(m_history);
|
||||||
m_advancedUi->addAttributeButton->setEnabled(!m_history);
|
m_advancedUi->addAttributeButton->setEnabled(!m_history);
|
||||||
m_advancedUi->editAttributeButton->setEnabled(false);
|
m_advancedUi->editAttributeButton->setEnabled(false);
|
||||||
|
|
@ -1037,55 +1046,26 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
||||||
setupBrowser();
|
setupBrowser();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto hideEntriesCheckBoxEnabled = true;
|
|
||||||
auto skipAutoSubmitCheckBoxEnabled = true;
|
|
||||||
auto onlyHttpAuthCheckBoxEnabled = true;
|
|
||||||
auto notHttpAuthCheckBoxEnabled = true;
|
|
||||||
auto hideEntries = false;
|
|
||||||
auto skipAutoSubmit = false;
|
|
||||||
auto onlyHttpAuth = false;
|
|
||||||
auto notHttpAuth = false;
|
|
||||||
|
|
||||||
const auto group = m_entry->group();
|
const auto group = m_entry->group();
|
||||||
if (group) {
|
m_browserUi->messageWidget->showMessage(
|
||||||
hideEntries = group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable;
|
tr("Some Browser Integration settings are overridden by group settings."), MessageWidget::Information);
|
||||||
skipAutoSubmit = group->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == Group::Enable;
|
m_browserUi->messageWidget->setVisible(false);
|
||||||
onlyHttpAuth = group->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Enable;
|
|
||||||
notHttpAuth = group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Enable;
|
|
||||||
|
|
||||||
hideEntriesCheckBoxEnabled =
|
auto updateCheckBoxValue = [&](QCheckBox* checkBox, const QString& option) {
|
||||||
group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Inherit;
|
const auto optionEnabledInGroup = group ? group->resolveBrowserOptionEnabled(option) : false;
|
||||||
skipAutoSubmitCheckBoxEnabled =
|
const auto optionInherited = group ? group->resolveCustomDataTriState(option) == Group::Inherit : true;
|
||||||
group->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == Group::Inherit;
|
|
||||||
onlyHttpAuthCheckBoxEnabled =
|
|
||||||
group->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Inherit;
|
|
||||||
notHttpAuthCheckBoxEnabled =
|
|
||||||
group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show information about group level settings
|
if (!optionInherited) {
|
||||||
if (!hideEntriesCheckBoxEnabled || !skipAutoSubmitCheckBoxEnabled || !onlyHttpAuthCheckBoxEnabled
|
m_browserUi->messageWidget->setVisible(true);
|
||||||
|| !notHttpAuthCheckBoxEnabled) {
|
}
|
||||||
m_browserUi->messageWidget->showMessage(
|
|
||||||
tr("Some Browser Integration settings are overridden by group settings."), MessageWidget::Information);
|
|
||||||
m_browserUi->messageWidget->setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable checkboxes based on group level settings
|
updateBrowserIntegrationCheckbox(checkBox, optionInherited, optionEnabledInGroup, option);
|
||||||
updateBrowserIntegrationCheckbox(
|
};
|
||||||
m_browserUi->hideEntryCheckbox, hideEntriesCheckBoxEnabled, hideEntries, BrowserService::OPTION_HIDE_ENTRY);
|
|
||||||
updateBrowserIntegrationCheckbox(m_browserUi->skipAutoSubmitCheckbox,
|
updateCheckBoxValue(m_browserUi->hideEntryCheckbox, BrowserService::OPTION_HIDE_ENTRY);
|
||||||
skipAutoSubmitCheckBoxEnabled,
|
updateCheckBoxValue(m_browserUi->skipAutoSubmitCheckbox, BrowserService::OPTION_SKIP_AUTO_SUBMIT);
|
||||||
skipAutoSubmit,
|
updateCheckBoxValue(m_browserUi->onlyHttpAuthCheckbox, BrowserService::OPTION_ONLY_HTTP_AUTH);
|
||||||
BrowserService::OPTION_SKIP_AUTO_SUBMIT);
|
updateCheckBoxValue(m_browserUi->notHttpAuthCheckbox, BrowserService::OPTION_NOT_HTTP_AUTH);
|
||||||
updateBrowserIntegrationCheckbox(m_browserUi->onlyHttpAuthCheckbox,
|
|
||||||
onlyHttpAuthCheckBoxEnabled,
|
|
||||||
onlyHttpAuth,
|
|
||||||
BrowserService::OPTION_ONLY_HTTP_AUTH);
|
|
||||||
updateBrowserIntegrationCheckbox(m_browserUi->notHttpAuthCheckbox,
|
|
||||||
notHttpAuthCheckBoxEnabled,
|
|
||||||
notHttpAuth,
|
|
||||||
BrowserService::OPTION_NOT_HTTP_AUTH);
|
|
||||||
|
|
||||||
m_browserUi->addURLButton->setEnabled(!m_history);
|
m_browserUi->addURLButton->setEnabled(!m_history);
|
||||||
m_browserUi->removeURLButton->setEnabled(false);
|
m_browserUi->removeURLButton->setEnabled(false);
|
||||||
|
|
@ -1534,7 +1514,7 @@ void EditEntryWidget::updateAutoTypeEnabled()
|
||||||
m_autoTypeUi->inheritSequenceButton->setEnabled(!m_history && autoTypeEnabled);
|
m_autoTypeUi->inheritSequenceButton->setEnabled(!m_history && autoTypeEnabled);
|
||||||
m_autoTypeUi->customSequenceButton->setEnabled(!m_history && autoTypeEnabled);
|
m_autoTypeUi->customSequenceButton->setEnabled(!m_history && autoTypeEnabled);
|
||||||
m_autoTypeUi->sequenceEdit->setEnabled(autoTypeEnabled && m_autoTypeUi->customSequenceButton->isChecked());
|
m_autoTypeUi->sequenceEdit->setEnabled(autoTypeEnabled && m_autoTypeUi->customSequenceButton->isChecked());
|
||||||
m_autoTypeUi->openHelpButton->setEnabled(autoTypeEnabled && m_autoTypeUi->customSequenceButton->isChecked());
|
m_autoTypeUi->openHelpButton->setEnabled(autoTypeEnabled);
|
||||||
|
|
||||||
m_autoTypeUi->assocView->setEnabled(autoTypeEnabled);
|
m_autoTypeUi->assocView->setEnabled(autoTypeEnabled);
|
||||||
m_autoTypeUi->assocAddButton->setEnabled(!m_history);
|
m_autoTypeUi->assocAddButton->setEnabled(!m_history);
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,6 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="openHelpButton">
|
<widget class="QToolButton" name="openHelpButton">
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Open Auto-Type help webpage</string>
|
<string>Open Auto-Type help webpage</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
||||||
|
|
@ -1,386 +1,383 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>EditEntryWidgetMain</class>
|
<class>EditEntryWidgetMain</class>
|
||||||
<widget class="QScrollArea" name="EditEntryWidgetMain">
|
<widget class="QScrollArea" name="EditEntryWidgetMain">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>400</width>
|
<width>400</width>
|
||||||
<height>523</height>
|
<height>523</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Edit Entry</string>
|
<string>Edit Entry</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::NoFrame</enum>
|
<enum>QFrame::NoFrame</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Plain</enum>
|
<enum>QFrame::Plain</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="horizontalScrollBarPolicy">
|
<property name="horizontalScrollBarPolicy">
|
||||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeAdjustPolicy">
|
<property name="sizeAdjustPolicy">
|
||||||
<enum>QAbstractScrollArea::AdjustToContents</enum>
|
<enum>QAbstractScrollArea::AdjustToContents</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="widgetResizable">
|
<property name="widgetResizable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="container">
|
<widget class="QWidget" name="container">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>400</width>
|
<width>400</width>
|
||||||
<height>523</height>
|
<height>523</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
|
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="topMargin">
|
<property name="topMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="rightMargin">
|
<property name="rightMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="horizontalSpacing">
|
<property name="horizontalSpacing">
|
||||||
<number>10</number>
|
<number>10</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="verticalSpacing">
|
<property name="verticalSpacing">
|
||||||
<number>8</number>
|
<number>8</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLineEdit" name="titleEdit">
|
<widget class="QLineEdit" name="titleEdit">
|
||||||
<property name="accessibleName">
|
<property name="accessibleName">
|
||||||
<string>Title field</string>
|
<string>Title field</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="usernameLabel">
|
<widget class="QLabel" name="usernameLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Username:</string>
|
<string>&Username:</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
<property name="buddy">
|
||||||
<cstring>usernameComboBox</cstring>
|
<cstring>usernameComboBox</cstring>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="PasswordWidget" name="passwordEdit" native="true">
|
<widget class="PasswordWidget" name="passwordEdit" native="true">
|
||||||
<property name="focusPolicy">
|
<property name="focusPolicy">
|
||||||
<enum>Qt::StrongFocus</enum>
|
<enum>Qt::StrongFocus</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="accessibleName">
|
||||||
<string>Password field</string>
|
<string>Password field</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="8" column="1">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPlainTextEdit" name="notesEdit">
|
<widget class="QPlainTextEdit" name="notesEdit">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>1</verstretch>
|
<verstretch>1</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
<height>100</height>
|
<height>100</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="accessibleName">
|
||||||
<string>Notes field</string>
|
<string>Notes field</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="tabStopDistance">
|
</widget>
|
||||||
<double>10.000000000000000</double>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</widget>
|
</item>
|
||||||
</item>
|
<item row="0" column="0">
|
||||||
</layout>
|
<widget class="QLabel" name="titleLabel">
|
||||||
</item>
|
<property name="text">
|
||||||
<item row="0" column="0">
|
<string>&Title:</string>
|
||||||
<widget class="QLabel" name="titleLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>&Title:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>titleEdit</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>titleEdit</cstring>
|
</item>
|
||||||
</property>
|
<item row="2" column="0">
|
||||||
</widget>
|
<widget class="QLabel" name="passwordLabel">
|
||||||
</item>
|
<property name="text">
|
||||||
<item row="2" column="0">
|
<string>&Password:</string>
|
||||||
<widget class="QLabel" name="passwordLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>&Password:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>passwordEdit</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>passwordEdit</cstring>
|
</item>
|
||||||
</property>
|
<item row="1" column="1">
|
||||||
</widget>
|
<widget class="QComboBox" name="usernameComboBox">
|
||||||
</item>
|
<property name="focusPolicy">
|
||||||
<item row="1" column="1">
|
<enum>Qt::StrongFocus</enum>
|
||||||
<widget class="QComboBox" name="usernameComboBox">
|
</property>
|
||||||
<property name="focusPolicy">
|
<property name="accessibleName">
|
||||||
<enum>Qt::StrongFocus</enum>
|
<string>Username field</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
</widget>
|
||||||
<string>Username field</string>
|
</item>
|
||||||
</property>
|
<item row="5" column="1">
|
||||||
</widget>
|
<widget class="TagsEdit" name="tagsList" native="true">
|
||||||
</item>
|
<property name="focusPolicy">
|
||||||
<item row="5" column="1">
|
<enum>Qt::StrongFocus</enum>
|
||||||
<widget class="TagsEdit" name="tagsList" native="true">
|
</property>
|
||||||
<property name="focusPolicy">
|
<property name="accessibleName">
|
||||||
<enum>Qt::StrongFocus</enum>
|
<string>Tags list</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
</widget>
|
||||||
<string>Tags list</string>
|
</item>
|
||||||
</property>
|
<item row="7" column="1">
|
||||||
</widget>
|
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0">
|
||||||
</item>
|
<property name="spacing">
|
||||||
<item row="7" column="1">
|
<number>8</number>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0">
|
</property>
|
||||||
<property name="spacing">
|
<item>
|
||||||
<number>8</number>
|
<widget class="QCheckBox" name="expireCheck">
|
||||||
</property>
|
<property name="toolTip">
|
||||||
<item>
|
<string>Toggle expiration</string>
|
||||||
<widget class="QCheckBox" name="expireCheck">
|
</property>
|
||||||
<property name="toolTip">
|
<property name="accessibleName">
|
||||||
<string>Toggle expiration</string>
|
<string>Toggle expiration</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="text">
|
||||||
<string>Toggle expiration</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
</widget>
|
||||||
<string/>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</widget>
|
<widget class="QDateTimeEdit" name="expireDatePicker">
|
||||||
</item>
|
<property name="enabled">
|
||||||
<item>
|
<bool>false</bool>
|
||||||
<widget class="QDateTimeEdit" name="expireDatePicker">
|
</property>
|
||||||
<property name="enabled">
|
<property name="accessibleName">
|
||||||
<bool>false</bool>
|
<string>Expiration field</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="calendarPopup">
|
||||||
<string>Expiration field</string>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="calendarPopup">
|
</widget>
|
||||||
<bool>true</bool>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</widget>
|
<widget class="QPushButton" name="expirePresets">
|
||||||
</item>
|
<property name="sizePolicy">
|
||||||
<item>
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
<widget class="QPushButton" name="expirePresets">
|
<horstretch>0</horstretch>
|
||||||
<property name="sizePolicy">
|
<verstretch>0</verstretch>
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
</sizepolicy>
|
||||||
<horstretch>0</horstretch>
|
</property>
|
||||||
<verstretch>0</verstretch>
|
<property name="toolTip">
|
||||||
</sizepolicy>
|
<string>Expiration Presets</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="accessibleName">
|
||||||
<string>Expiration Presets</string>
|
<string>Expiration presets</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="text">
|
||||||
<string>Expiration presets</string>
|
<string>Presets</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
</widget>
|
||||||
<string>Presets</string>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</widget>
|
</item>
|
||||||
</item>
|
<item row="3" column="0">
|
||||||
</layout>
|
<widget class="QLabel" name="urlLabel">
|
||||||
</item>
|
<property name="text">
|
||||||
<item row="3" column="0">
|
<string>UR&L:</string>
|
||||||
<widget class="QLabel" name="urlLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>UR&L:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>urlEdit</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>urlEdit</cstring>
|
</item>
|
||||||
</property>
|
<item row="3" column="1">
|
||||||
</widget>
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
</item>
|
<property name="spacing">
|
||||||
<item row="3" column="1">
|
<number>8</number>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
</property>
|
||||||
<property name="spacing">
|
<item>
|
||||||
<number>8</number>
|
<widget class="URLEdit" name="urlEdit">
|
||||||
</property>
|
<property name="accessibleName">
|
||||||
<item>
|
<string>Url field</string>
|
||||||
<widget class="URLEdit" name="urlEdit">
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="placeholderText">
|
||||||
<string>Url field</string>
|
<string notr="true">https://example.com</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="placeholderText">
|
</widget>
|
||||||
<string notr="true">https://example.com</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</widget>
|
<widget class="QToolButton" name="fetchFaviconButton">
|
||||||
</item>
|
<property name="toolTip">
|
||||||
<item>
|
<string>Download favicon for URL</string>
|
||||||
<widget class="QToolButton" name="fetchFaviconButton">
|
</property>
|
||||||
<property name="toolTip">
|
<property name="accessibleName">
|
||||||
<string>Download favicon for URL</string>
|
<string>Download favicon for URL</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
</widget>
|
||||||
<string>Download favicon for URL</string>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</widget>
|
</item>
|
||||||
</item>
|
<item row="8" column="0">
|
||||||
</layout>
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
</item>
|
<item>
|
||||||
<item row="8" column="0">
|
<widget class="QLabel" name="notesLabel">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<property name="text">
|
||||||
<item>
|
<string>&Notes:</string>
|
||||||
<widget class="QLabel" name="notesLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>&Notes:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>notesEdit</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>notesEdit</cstring>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</widget>
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
</item>
|
<property name="topMargin">
|
||||||
<item>
|
<number>6</number>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
</property>
|
||||||
<property name="topMargin">
|
<item>
|
||||||
<number>6</number>
|
<spacer name="horizontalSpacer">
|
||||||
</property>
|
<property name="orientation">
|
||||||
<item>
|
<enum>Qt::Horizontal</enum>
|
||||||
<spacer name="horizontalSpacer">
|
</property>
|
||||||
<property name="orientation">
|
<property name="sizeHint" stdset="0">
|
||||||
<enum>Qt::Horizontal</enum>
|
<size>
|
||||||
</property>
|
<width>5</width>
|
||||||
<property name="sizeHint" stdset="0">
|
<height>20</height>
|
||||||
<size>
|
</size>
|
||||||
<width>5</width>
|
</property>
|
||||||
<height>20</height>
|
</spacer>
|
||||||
</size>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</spacer>
|
<widget class="QToolButton" name="revealNotesButton">
|
||||||
</item>
|
<property name="toolTip">
|
||||||
<item>
|
<string>Toggle notes visibility</string>
|
||||||
<widget class="QToolButton" name="revealNotesButton">
|
</property>
|
||||||
<property name="toolTip">
|
<property name="accessibleName">
|
||||||
<string>Toggle notes visibility</string>
|
<string>Toggle notes visibility</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="accessibleName">
|
<property name="iconSize">
|
||||||
<string>Toggle notes visibility</string>
|
<size>
|
||||||
</property>
|
<width>14</width>
|
||||||
<property name="iconSize">
|
<height>14</height>
|
||||||
<size>
|
</size>
|
||||||
<width>14</width>
|
</property>
|
||||||
<height>14</height>
|
<property name="checkable">
|
||||||
</size>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkable">
|
</widget>
|
||||||
<bool>true</bool>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</widget>
|
</item>
|
||||||
</item>
|
<item>
|
||||||
</layout>
|
<spacer name="verticalSpacer">
|
||||||
</item>
|
<property name="orientation">
|
||||||
<item>
|
<enum>Qt::Vertical</enum>
|
||||||
<spacer name="verticalSpacer">
|
</property>
|
||||||
<property name="orientation">
|
<property name="sizeHint" stdset="0">
|
||||||
<enum>Qt::Vertical</enum>
|
<size>
|
||||||
</property>
|
<width>20</width>
|
||||||
<property name="sizeHint" stdset="0">
|
<height>40</height>
|
||||||
<size>
|
</size>
|
||||||
<width>20</width>
|
</property>
|
||||||
<height>40</height>
|
</spacer>
|
||||||
</size>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</spacer>
|
</item>
|
||||||
</item>
|
<item row="5" column="0">
|
||||||
</layout>
|
<widget class="QLabel" name="tagsLabel">
|
||||||
</item>
|
<property name="text">
|
||||||
<item row="5" column="0">
|
<string>T&ags:</string>
|
||||||
<widget class="QLabel" name="tagsLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>T&ags:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>tagsList</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>tagsList</cstring>
|
</item>
|
||||||
</property>
|
<item row="7" column="0">
|
||||||
</widget>
|
<widget class="QLabel" name="expireLabel">
|
||||||
</item>
|
<property name="text">
|
||||||
<item row="7" column="0">
|
<string>&Expires:</string>
|
||||||
<widget class="QLabel" name="expireLabel">
|
</property>
|
||||||
<property name="text">
|
<property name="alignment">
|
||||||
<string>&Expires:</string>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="buddy">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<cstring>expireCheck</cstring>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
</widget>
|
||||||
<cstring>expireCheck</cstring>
|
</item>
|
||||||
</property>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</widget>
|
||||||
</layout>
|
<customwidgets>
|
||||||
</widget>
|
<customwidget>
|
||||||
</widget>
|
<class>TagsEdit</class>
|
||||||
<customwidgets>
|
<extends>QWidget</extends>
|
||||||
<customwidget>
|
<header>gui/tag/TagsEdit.h</header>
|
||||||
<class>TagsEdit</class>
|
<container>1</container>
|
||||||
<extends>QWidget</extends>
|
</customwidget>
|
||||||
<header>gui/tag/TagsEdit.h</header>
|
<customwidget>
|
||||||
<container>1</container>
|
<class>URLEdit</class>
|
||||||
</customwidget>
|
<extends>QLineEdit</extends>
|
||||||
<customwidget>
|
<header>gui/URLEdit.h</header>
|
||||||
<class>URLEdit</class>
|
<container>1</container>
|
||||||
<extends>QLineEdit</extends>
|
</customwidget>
|
||||||
<header>gui/URLEdit.h</header>
|
<customwidget>
|
||||||
<container>1</container>
|
<class>PasswordWidget</class>
|
||||||
</customwidget>
|
<extends>QWidget</extends>
|
||||||
<customwidget>
|
<header>gui/PasswordWidget.h</header>
|
||||||
<class>PasswordWidget</class>
|
<container>1</container>
|
||||||
<extends>QWidget</extends>
|
</customwidget>
|
||||||
<header>gui/PasswordWidget.h</header>
|
</customwidgets>
|
||||||
<container>1</container>
|
<tabstops>
|
||||||
</customwidget>
|
<tabstop>titleEdit</tabstop>
|
||||||
</customwidgets>
|
<tabstop>usernameComboBox</tabstop>
|
||||||
<tabstops>
|
<tabstop>passwordEdit</tabstop>
|
||||||
<tabstop>titleEdit</tabstop>
|
<tabstop>urlEdit</tabstop>
|
||||||
<tabstop>usernameComboBox</tabstop>
|
<tabstop>fetchFaviconButton</tabstop>
|
||||||
<tabstop>passwordEdit</tabstop>
|
<tabstop>tagsList</tabstop>
|
||||||
<tabstop>urlEdit</tabstop>
|
<tabstop>expireCheck</tabstop>
|
||||||
<tabstop>fetchFaviconButton</tabstop>
|
<tabstop>expireDatePicker</tabstop>
|
||||||
<tabstop>tagsList</tabstop>
|
<tabstop>expirePresets</tabstop>
|
||||||
<tabstop>expireCheck</tabstop>
|
<tabstop>revealNotesButton</tabstop>
|
||||||
<tabstop>expireDatePicker</tabstop>
|
<tabstop>notesEdit</tabstop>
|
||||||
<tabstop>expirePresets</tabstop>
|
</tabstops>
|
||||||
<tabstop>revealNotesButton</tabstop>
|
<resources/>
|
||||||
<tabstop>notesEdit</tabstop>
|
<connections/>
|
||||||
</tabstops>
|
</ui>
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
#include <QRegExp>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
|
|
||||||
|
|
@ -368,8 +369,9 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
||||||
|
|
||||||
QStringList errors;
|
QStringList errors;
|
||||||
for (const QModelIndex& index : indexes) {
|
for (const QModelIndex& index : indexes) {
|
||||||
const QString filename = m_attachmentsModel->keyByIndex(index);
|
QString attachmentKey = m_attachmentsModel->keyByIndex(index);
|
||||||
const QString attachmentPath = saveDir.absoluteFilePath(filename);
|
const QString fileNameSanitized = attachmentKey.replace(QRegExp("[/\\\\]"), "");
|
||||||
|
const QString attachmentPath = saveDir.absoluteFilePath(fileNameSanitized);
|
||||||
|
|
||||||
if (QFileInfo::exists(attachmentPath)) {
|
if (QFileInfo::exists(attachmentPath)) {
|
||||||
|
|
||||||
|
|
@ -382,7 +384,7 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
||||||
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
|
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
|
||||||
|
|
||||||
auto result = MessageBox::question(
|
auto result = MessageBox::question(
|
||||||
this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
|
this, tr("Confirm overwrite"), questionText.arg(fileNameSanitized), buttons, MessageBox::Cancel);
|
||||||
|
|
||||||
if (result == MessageBox::Skip) {
|
if (result == MessageBox::Skip) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -392,11 +394,11 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
QFile file(attachmentPath);
|
QFile file(attachmentPath);
|
||||||
const QByteArray attachmentData = m_entryAttachments->value(filename);
|
const QByteArray attachmentData = m_entryAttachments->value(attachmentKey);
|
||||||
const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
|
const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
|
||||||
&& file.write(attachmentData) == attachmentData.size();
|
&& file.write(attachmentData) == attachmentData.size();
|
||||||
if (!saveOk) {
|
if (!saveOk) {
|
||||||
errors.append(QString("%1 - %2").arg(filename, file.errorString()));
|
errors.append(QString("%1 - %2").arg(fileNameSanitized, file.errorString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,7 @@ void TextAttachmentsEditWidget::updateUi()
|
||||||
{
|
{
|
||||||
m_ui->attachmentsTextEdit->setPlainText(m_attachment.data);
|
m_ui->attachmentsTextEdit->setPlainText(m_attachment.data);
|
||||||
m_ui->attachmentsTextEdit->setReadOnly(m_mode == attachments::OpenMode::ReadOnly);
|
m_ui->attachmentsTextEdit->setReadOnly(m_mode == attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
m_ui->attachmentsTextEdit->setTabStopDistance(
|
||||||
|
QFontMetrics(m_ui->attachmentsTextEdit->font()).horizontalAdvance(QString(4, ' ')));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,7 @@
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTextEdit" name="attachmentsTextEdit">
|
<widget class="QTextEdit" name="attachmentsTextEdit"/>
|
||||||
<property name="tabStopDistance">
|
|
||||||
<double>10.000000000000000</double>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,9 @@ void TextAttachmentsPreviewWidget::initTypeCombobox()
|
||||||
// Configure text browser to open external links
|
// Configure text browser to open external links
|
||||||
m_ui->previewTextBrowser->setOpenExternalLinks(true);
|
m_ui->previewTextBrowser->setOpenExternalLinks(true);
|
||||||
|
|
||||||
|
m_ui->previewTextBrowser->setTabStopDistance(
|
||||||
|
QFontMetrics(m_ui->previewTextBrowser->font()).horizontalAdvance(QString(4, ' ')));
|
||||||
|
|
||||||
m_ui->typeComboBox->setCurrentIndex(m_ui->typeComboBox->findData(PlainText));
|
m_ui->typeComboBox->setCurrentIndex(m_ui->typeComboBox->findData(PlainText));
|
||||||
|
|
||||||
onTypeChanged(m_ui->typeComboBox->currentIndex());
|
onTypeChanged(m_ui->typeComboBox->currentIndex());
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,7 @@
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTextBrowser" name="previewTextBrowser">
|
<widget class="QTextBrowser" name="previewTextBrowser"/>
|
||||||
<property name="tabStopDistance">
|
|
||||||
<double>10.000000000000000</double>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,6 @@ void DarkStyle::polish(QWidget* widget)
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x2D2D2D));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x2D2D2D));
|
||||||
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x2D2D2D));
|
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x2D2D2D));
|
||||||
}
|
}
|
||||||
#elif defined(Q_OS_WIN)
|
|
||||||
palette.setColor(QPalette::All, QPalette::Window, QRgb(0x2F2F30));
|
|
||||||
#else
|
#else
|
||||||
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2F2F30));
|
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2F2F30));
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x313133));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x313133));
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,6 @@ void LightStyle::polish(QWidget* widget)
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF5F5F5));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF5F5F5));
|
||||||
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xF5F5F5));
|
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xF5F5F5));
|
||||||
}
|
}
|
||||||
#elif defined(Q_OS_WIN)
|
|
||||||
palette.setColor(QPalette::All, QPalette::Window, QRgb(0xFFFFFF));
|
|
||||||
#else
|
#else
|
||||||
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xEFF0F1));
|
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xEFF0F1));
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xEFF0F1));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xEFF0F1));
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
|
||||||
LIBS testsupport ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testtools SOURCES TestTools.cpp
|
add_unit_test(NAME testtools SOURCES TestTools.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testconfig SOURCES TestConfig.cpp
|
add_unit_test(NAME testconfig SOURCES TestConfig.cpp
|
||||||
LIBS testsupport ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -854,3 +854,56 @@ void TestBrowser::testRestrictBrowserKey()
|
||||||
QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
|
QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
|
||||||
QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
|
QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestBrowser::testHideEntry()
|
||||||
|
{
|
||||||
|
const auto db = QSharedPointer<Database>::create();
|
||||||
|
auto* root = db->rootGroup();
|
||||||
|
|
||||||
|
const auto entry = new Entry();
|
||||||
|
entry->setGroup(root);
|
||||||
|
entry->beginUpdate();
|
||||||
|
entry->setUrl(QString("https://github.com/"));
|
||||||
|
entry->setUsername(QString("User 1"));
|
||||||
|
entry->setUuid(QUuid::createUuid());
|
||||||
|
entry->setTitle(QString("Name_ 1"));
|
||||||
|
entry->endUpdate();
|
||||||
|
|
||||||
|
// Entry should be found normally
|
||||||
|
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 1);
|
||||||
|
QCOMPARE(result[0]->url(), QString("https://github.com/"));
|
||||||
|
|
||||||
|
// Hide entry from entry settings, group setting is inherited
|
||||||
|
entry->customData()->set(BrowserService::OPTION_HIDE_ENTRY, TRUE_STR);
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 0);
|
||||||
|
|
||||||
|
// Disable hide from group settings, entry should be found
|
||||||
|
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Disable);
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 1);
|
||||||
|
|
||||||
|
// Enable hide from group setting, entry should not be found
|
||||||
|
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Enable);
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 0);
|
||||||
|
|
||||||
|
// Remove the hide settings from entry, return group setting to inherit
|
||||||
|
entry->customData()->set(BrowserService::OPTION_HIDE_ENTRY, FALSE_STR);
|
||||||
|
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Inherit);
|
||||||
|
|
||||||
|
// Entry should be found again
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 1);
|
||||||
|
|
||||||
|
// Enable hide from group setting, entry should not be found
|
||||||
|
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Enable);
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 0);
|
||||||
|
|
||||||
|
// Disable hide from group settings, entry should be found
|
||||||
|
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Disable);
|
||||||
|
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
|
||||||
|
QCOMPARE(result.length(), 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -51,6 +51,7 @@ private slots:
|
||||||
void testBestMatchingCredentials();
|
void testBestMatchingCredentials();
|
||||||
void testBestMatchingWithAdditionalURLs();
|
void testBestMatchingWithAdditionalURLs();
|
||||||
void testRestrictBrowserKey();
|
void testRestrictBrowserKey();
|
||||||
|
void testHideEntry();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<Entry*> createEntries(QStringList& urls, Group* root, bool additionalUrl = false) const;
|
QList<Entry*> createEntries(QStringList& urls, Group* root, bool additionalUrl = false) const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -327,6 +327,106 @@ void TestImports::testBitwardenPasskey()
|
||||||
QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
|
QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestImports::testBitwardenNestedFolders()
|
||||||
|
{
|
||||||
|
auto bitwardenPath =
|
||||||
|
QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_nested_export.json"));
|
||||||
|
|
||||||
|
BitwardenReader reader;
|
||||||
|
auto db = reader.convert(bitwardenPath);
|
||||||
|
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
|
||||||
|
QVERIFY(db);
|
||||||
|
|
||||||
|
/* The group tree should be:
|
||||||
|
/
|
||||||
|
- Example
|
||||||
|
- Test Authentication
|
||||||
|
/SecondTest
|
||||||
|
- GMail entry
|
||||||
|
/Test
|
||||||
|
- Gmail test 2
|
||||||
|
/Subfolder
|
||||||
|
- Webauthn.io test 2
|
||||||
|
/Subfolder
|
||||||
|
- Test Account
|
||||||
|
/SubFolder
|
||||||
|
- WebAuthn.io test
|
||||||
|
/AnotherSubFolder
|
||||||
|
- Another test account
|
||||||
|
- Webauthn.io test 3
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Verify groups
|
||||||
|
auto secondTestGroup = db->rootGroup()->findGroupByPath("/SecondTest");
|
||||||
|
QVERIFY(secondTestGroup);
|
||||||
|
auto testGroup = db->rootGroup()->findGroupByPath("/Test");
|
||||||
|
QVERIFY(testGroup);
|
||||||
|
auto testSubfolderLowercaseGroup = db->rootGroup()->findGroupByPath("/Test/Subfolder");
|
||||||
|
QVERIFY(testSubfolderLowercaseGroup);
|
||||||
|
auto testSubFolderGroup = db->rootGroup()->findGroupByPath("/Test/SubFolder");
|
||||||
|
QVERIFY(testSubFolderGroup);
|
||||||
|
auto longGroup = db->rootGroup()->findGroupByPath("/Test/SubFolder/AnotherSubFolder");
|
||||||
|
QVERIFY(longGroup);
|
||||||
|
|
||||||
|
// Verify entries and the groups they belong to
|
||||||
|
|
||||||
|
// GMail entry
|
||||||
|
auto entry = db->rootGroup()->findEntryByPath("/SecondTest/GMail entry");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("example@gmail.com"));
|
||||||
|
QCOMPARE(entry->group(), secondTestGroup);
|
||||||
|
|
||||||
|
// Test Authentication
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test Authentication");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("test@testauthentication.com"));
|
||||||
|
QCOMPARE(entry->group(), db->rootGroup());
|
||||||
|
|
||||||
|
// Gmail test 2
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/Gmail test 2");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("example2@gmail.com"));
|
||||||
|
QCOMPARE(entry->group(), testGroup);
|
||||||
|
|
||||||
|
// Example
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Example");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("user@example.com"));
|
||||||
|
QCOMPARE(entry->group(), db->rootGroup());
|
||||||
|
|
||||||
|
// WebAuthn.io test
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/WebAuthn.io test");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("testUser"));
|
||||||
|
QCOMPARE(entry->group(), testSubFolderGroup);
|
||||||
|
|
||||||
|
// Webauthn.io test 2
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/Subfolder/Webauthn.io test 2");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("testUser2"));
|
||||||
|
QCOMPARE(entry->group(), testSubfolderLowercaseGroup);
|
||||||
|
|
||||||
|
// Webauthn.io test 3
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/AnotherSubFolder/Webauthn.io test 3");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("testUser3"));
|
||||||
|
QCOMPARE(entry->group(), longGroup);
|
||||||
|
|
||||||
|
// Test Account
|
||||||
|
// There are two groups with an identical name. The group for this entry should not be the same group with the
|
||||||
|
// Webauthn.io test 2, but we cannot distinguish these.
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/Subfolder/Test Account");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("test-account"));
|
||||||
|
QCOMPARE(entry->group(), testSubfolderLowercaseGroup);
|
||||||
|
|
||||||
|
// Another test account
|
||||||
|
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/AnotherSubFolder/Another test account");
|
||||||
|
QVERIFY(entry);
|
||||||
|
QCOMPARE(entry->username(), QStringLiteral("anotherUser"));
|
||||||
|
QCOMPARE(entry->group(), longGroup);
|
||||||
|
}
|
||||||
|
|
||||||
void TestImports::testProtonPass()
|
void TestImports::testProtonPass()
|
||||||
{
|
{
|
||||||
auto protonPassPath =
|
auto protonPassPath =
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -31,6 +31,7 @@ private slots:
|
||||||
void testBitwarden();
|
void testBitwarden();
|
||||||
void testBitwardenEncrypted();
|
void testBitwardenEncrypted();
|
||||||
void testBitwardenPasskey();
|
void testBitwardenPasskey();
|
||||||
|
void testBitwardenNestedFolders();
|
||||||
void testProtonPass();
|
void testProtonPass();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -77,7 +77,7 @@ const QString PublicKeyCredential = R"(
|
||||||
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
|
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
|
||||||
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
|
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
|
||||||
"response": {
|
"response": {
|
||||||
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M",
|
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBdAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU",
|
||||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
|
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
|
||||||
},
|
},
|
||||||
"type": "public-key"
|
"type": "public-key"
|
||||||
|
|
@ -185,11 +185,13 @@ void TestPasskeys::testDecodeResponseData()
|
||||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||||
QCOMPARE(flags["AT"], true);
|
QCOMPARE(flags["AT"], true);
|
||||||
QCOMPARE(flags["UP"], true);
|
QCOMPARE(flags["UP"], true);
|
||||||
|
QCOMPARE(flags["BE"], true);
|
||||||
|
QCOMPARE(flags["BS"], true);
|
||||||
QCOMPARE(publicKey["1"], 2);
|
QCOMPARE(publicKey["1"], 2);
|
||||||
QCOMPARE(publicKey["3"], -7);
|
QCOMPARE(publicKey["3"], -7);
|
||||||
QCOMPARE(publicKey["-1"], 1);
|
QCOMPARE(publicKey["-1"], 1);
|
||||||
QCOMPARE(publicKey["-2"], QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0"));
|
QCOMPARE(publicKey["-2"], QString("crWJWKZ5HTZRiKnKIQqsqGF-ElEx7DwRsNYYq0xnbBQ"));
|
||||||
QCOMPARE(publicKey["-3"], QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M"));
|
QCOMPARE(publicKey["-3"], QString("btZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestPasskeys::testLoadingECPrivateKeyFromPem()
|
void TestPasskeys::testLoadingECPrivateKeyFromPem()
|
||||||
|
|
@ -276,23 +278,27 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
|
||||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||||
|
|
||||||
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond};
|
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond, QString()};
|
||||||
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
||||||
const auto credentialPrivateKey =
|
const auto credentialPrivateKey = browserPasskeys()->buildCredentialPrivateKey(alg, testingVariables);
|
||||||
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedFirst, predefinedSecond);
|
|
||||||
auto result = browserPasskeys()->buildAttestationObject(
|
auto result = browserPasskeys()->buildAttestationObject(
|
||||||
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
||||||
QCOMPARE(
|
QCOMPARE(
|
||||||
result,
|
result,
|
||||||
QString("\xA3"
|
QString("\xA3"
|
||||||
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
|
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
|
||||||
"9\x7F)%\x0B`\x84\x1E\xF0"
|
"9\x7F)%\x0B`\x84\x1E\xF0]\x00\x00\x00\x00\xFD\xB1"
|
||||||
"E\x00\x00\x00\x01\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b\x00 \x8B\xB0\xCA"
|
"A\xB2]\x84"
|
||||||
"6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:"
|
"D>\x8A"
|
||||||
"\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X "
|
"5F\x98\xC2\x05\xA5\x02\x00 \xCA\xBC\xC5'\x99pr\x94\xF0`\xC3\x9D])\xB1\x17\x96\xF9q\x84%\xA8\x13"
|
||||||
"e\xE2\xF2\x1F:cq\xD3G\xEA\xE0\xF7\x1F\xCF\xFA\\\xABO\xF6\x86\x88\x80\t\xAE\x81\x8BT\xB2\x9B\x15\x85~"
|
"3m\xB5?w\xEA\x05,\xEF\xA5\x01\x02\x03& \x01!X \x06\xEC\xAF"
|
||||||
"\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD"
|
"4[b\x91"
|
||||||
"5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7"));
|
"am\x19Y\x03\xA6P*\xCA"
|
||||||
|
"1\xC4\x95\xA8i\xE5\xF0\x87\xE5\xD4\xB8"
|
||||||
|
"2\xCD\b\x85\xDD\"X \xE2\xEE\x7F\xE9\x0F\x0E\xE9\x1D\x07\x83J\x03\t\xDB"
|
||||||
|
"B$\xB1\x0B\xD3%\xFF\x18"
|
||||||
|
"2\xE1S\x99\xB7\x1D"
|
||||||
|
"B\x04\xE7\x83"));
|
||||||
|
|
||||||
// Double check that the result can be decoded
|
// Double check that the result can be decoded
|
||||||
BrowserCbor browserCbor;
|
BrowserCbor browserCbor;
|
||||||
|
|
@ -313,6 +319,8 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
|
||||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||||
QCOMPARE(flags["AT"], true);
|
QCOMPARE(flags["AT"], true);
|
||||||
QCOMPARE(flags["UP"], true);
|
QCOMPARE(flags["UP"], true);
|
||||||
|
QCOMPARE(flags["BE"], true);
|
||||||
|
QCOMPARE(flags["BS"], true);
|
||||||
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2);
|
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2);
|
||||||
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256);
|
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256);
|
||||||
QCOMPARE(publicKey["-1"], 1);
|
QCOMPARE(publicKey["-1"], 1);
|
||||||
|
|
@ -344,10 +352,9 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
|
||||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||||
|
|
||||||
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent};
|
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent, QString()};
|
||||||
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
||||||
auto credentialPrivateKey =
|
auto credentialPrivateKey = browserPasskeys()->buildCredentialPrivateKey(alg, testingVariables);
|
||||||
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedModulus, predefinedExponent);
|
|
||||||
auto result = browserPasskeys()->buildAttestationObject(
|
auto result = browserPasskeys()->buildAttestationObject(
|
||||||
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
||||||
|
|
||||||
|
|
@ -370,6 +377,8 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
|
||||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||||
QCOMPARE(flags["AT"], true);
|
QCOMPARE(flags["AT"], true);
|
||||||
QCOMPARE(flags["UP"], true);
|
QCOMPARE(flags["UP"], true);
|
||||||
|
QCOMPARE(flags["BE"], true);
|
||||||
|
QCOMPARE(flags["BS"], true);
|
||||||
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA);
|
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA);
|
||||||
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256);
|
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256);
|
||||||
QCOMPARE(publicKey["-1"], predefinedModulus);
|
QCOMPARE(publicKey["-1"], predefinedModulus);
|
||||||
|
|
@ -380,8 +389,7 @@ void TestPasskeys::testRegister()
|
||||||
{
|
{
|
||||||
// Predefined values for a desired outcome
|
// Predefined values for a desired outcome
|
||||||
const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
||||||
const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
|
const auto predefinedData = QString("0x4B0E8AB07B1E62CCD4CB7B9D5BC9DE7B6EED7A3C8A3D466DB12897755E3D7E6D");
|
||||||
const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
|
|
||||||
const auto origin = QString("https://webauthn.io");
|
const auto origin = QString("https://webauthn.io");
|
||||||
const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
|
const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
|
||||||
const auto testDataResponse = testDataPublicKey["response"];
|
const auto testDataResponse = testDataPublicKey["response"];
|
||||||
|
|
@ -392,7 +400,7 @@ void TestPasskeys::testRegister()
|
||||||
publicKeyCredentialOptions, origin, &credentialCreationOptions);
|
publicKeyCredentialOptions, origin, &credentialCreationOptions);
|
||||||
QVERIFY(creationResult == 0);
|
QVERIFY(creationResult == 0);
|
||||||
|
|
||||||
TestingVariables testingVariables = {predefinedId, predefinedX, predefinedY};
|
TestingVariables testingVariables = {predefinedId, QString(), QString(), predefinedData};
|
||||||
auto result = browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions, testingVariables);
|
auto result = browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions, testingVariables);
|
||||||
auto publicKeyCredential = result.response;
|
auto publicKeyCredential = result.response;
|
||||||
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
|
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
|
||||||
|
|
@ -402,6 +410,9 @@ void TestPasskeys::testRegister()
|
||||||
auto response = publicKeyCredential["response"].toObject();
|
auto response = publicKeyCredential["response"].toObject();
|
||||||
auto attestationObject = response["attestationObject"].toString();
|
auto attestationObject = response["attestationObject"].toString();
|
||||||
auto clientDataJson = response["clientDataJSON"].toString();
|
auto clientDataJson = response["clientDataJSON"].toString();
|
||||||
|
QCOMPARE(response["publicKey"],
|
||||||
|
QString("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcrWJWKZ5HTZRiKnKIQqsqGF-"
|
||||||
|
"ElEx7DwRsNYYq0xnbBRu1nvRfXVIvHXtAUNZUBHf-qqTk6qtFL75aFzuHLD1hQ"));
|
||||||
QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
|
QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
|
||||||
|
|
||||||
// Parse clientDataJSON
|
// Parse clientDataJSON
|
||||||
|
|
@ -438,14 +449,14 @@ void TestPasskeys::testGet()
|
||||||
QCOMPARE(publicKeyCredential["id"].toString(), id);
|
QCOMPARE(publicKeyCredential["id"].toString(), id);
|
||||||
|
|
||||||
auto response = publicKeyCredential["response"].toObject();
|
auto response = publicKeyCredential["response"].toObject();
|
||||||
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
|
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAdAAAAAA"));
|
||||||
QCOMPARE(response["clientDataJSON"].toString(),
|
QCOMPARE(response["clientDataJSON"].toString(),
|
||||||
QString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtML"
|
QString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtML"
|
||||||
"Ux1R1Uxem9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYm"
|
"Ux1R1Uxem9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYm"
|
||||||
"F1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"));
|
"F1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"));
|
||||||
QCOMPARE(
|
QCOMPARE(
|
||||||
response["signature"].toString(),
|
response["signature"].toString(),
|
||||||
QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
|
QString("MEUCIQCvg3nXO2fiNK9ockxscgPtoM9_u6ERaW2-F1L99YasOAIgNhYOjPJyKJ-W8roV531kC59ss1USas7jy8TfRnbJLtg"));
|
||||||
|
|
||||||
auto clientDataJson = response["clientDataJSON"].toString();
|
auto clientDataJson = response["clientDataJSON"].toString();
|
||||||
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
|
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "core/Clock.h"
|
#include "core/Clock.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
|
#include "mock/MockClock.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
|
@ -33,8 +34,23 @@ namespace
|
||||||
{
|
{
|
||||||
return wholes + QLocale().decimalPoint() + fractions + " " + unit;
|
return wholes + QLocale().decimalPoint() + fractions + " " + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MockClock* s_clock = nullptr;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void TestTools::initTestCase()
|
||||||
|
{
|
||||||
|
Q_ASSERT(s_clock == nullptr);
|
||||||
|
s_clock = new MockClock(2026, 3, 8, 21, 45, 05);
|
||||||
|
MockClock::setup(s_clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTools::cleanupTestCase()
|
||||||
|
{
|
||||||
|
MockClock::teardown();
|
||||||
|
s_clock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void TestTools::testHumanReadableFileSize()
|
void TestTools::testHumanReadableFileSize()
|
||||||
{
|
{
|
||||||
constexpr auto kibibyte = 1024u;
|
constexpr auto kibibyte = 1024u;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ class TestTools : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private slots:
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
void cleanupTestCase();
|
||||||
void testHumanReadableFileSize();
|
void testHumanReadableFileSize();
|
||||||
void testIsHex();
|
void testIsHex();
|
||||||
void testIsBase64();
|
void testIsBase64();
|
||||||
|
|
|
||||||
249
tests/data/bitwarden_nested_export.json
Normal file
249
tests/data/bitwarden_nested_export.json
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
{
|
||||||
|
"encrypted": false,
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"id": "53f3c6e7-a167-47e2-91bb-b3f900a377a3",
|
||||||
|
"name": "SecondTest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14f22922-b8ed-4e9c-814b-b3f900a36a92",
|
||||||
|
"name": "Test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "da442766-39b4-4fb1-a2f3-b3f900a3a36d",
|
||||||
|
"name": "Test/Subfolder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0504d89b-00aa-41a5-9355-b3f900a3b43f",
|
||||||
|
"name": "Test/Subfolder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c96cf0e9-fd44-4a7e-9619-b3f900a58e59",
|
||||||
|
"name": "Test/SubFolder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5d262faf-329d-4197-9e8d-b3f900a3934c",
|
||||||
|
"name": "Test/SubFolder/AnotherSubFolder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T09:57:23.033Z",
|
||||||
|
"creationDate": "2026-02-17T16:55:23.210Z",
|
||||||
|
"id": "6b154a7d-4b62-44aa-ae0d-b3f40116e266",
|
||||||
|
"folderId": "53f3c6e7-a167-47e2-91bb-b3f900a377a3",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "GMail entry",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://accounts.google.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fido2Credentials": [],
|
||||||
|
"username": "example@gmail.com",
|
||||||
|
"password": "examplePassword",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-20T17:45:32.670Z",
|
||||||
|
"creationDate": "2026-02-20T17:45:32.670Z",
|
||||||
|
"id": "6ccafb74-ecab-482f-88b2-b3f70124a91b",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Test Authentication",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://testauthentication.com/login"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fido2Credentials": [],
|
||||||
|
"username": "test@testauthentication.com",
|
||||||
|
"password": "testPassword",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T09:57:15.540Z",
|
||||||
|
"creationDate": "2026-02-06T19:22:48.860Z",
|
||||||
|
"id": "a9f00893-346e-4be6-ac4e-b3e9013f6065",
|
||||||
|
"folderId": "14f22922-b8ed-4e9c-814b-b3f900a36a92",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Gmail test 2",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://accounts.google.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fido2Credentials": [],
|
||||||
|
"username": "example2@gmail.com",
|
||||||
|
"password": "examplePassword2",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-09T13:15:16.370Z",
|
||||||
|
"creationDate": "2026-02-09T13:15:16.113Z",
|
||||||
|
"id": "b375fe89-756a-41be-bcec-b3ec00da6d55",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Example",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://www.example.com/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "user@example.com",
|
||||||
|
"password": "examplePassword",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T10:03:05.596Z",
|
||||||
|
"creationDate": "2026-02-09T13:15:45.020Z",
|
||||||
|
"id": "b42284e2-a103-4dd0-982c-b3ec00da8f36",
|
||||||
|
"folderId": "c96cf0e9-fd44-4a7e-9619-b3f900a58e59",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "WebAuthn.io test",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://webauthn.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "testUser",
|
||||||
|
"password": "testPassword",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T09:56:58.923Z",
|
||||||
|
"creationDate": "2024-10-23T16:38:08.606Z",
|
||||||
|
"id": "a8e579f0-98c2-4ac9-a126-b212011225f8",
|
||||||
|
"folderId": "da442766-39b4-4fb1-a2f3-b3f900a3a36d",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Webauthn.io test 2",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://webauthn.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "testUser2",
|
||||||
|
"password": "testPassword2",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T09:56:46.026Z",
|
||||||
|
"creationDate": "2025-10-29T06:13:55.333Z",
|
||||||
|
"id": "a88363cf-9fea-43d8-a3dd-b3850066b36a",
|
||||||
|
"folderId": "5d262faf-329d-4197-9e8d-b3f900a3934c",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Webauthn.io test 3",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://webauthn.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "testUser3",
|
||||||
|
"password": "testPassword3",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2025-09-12T16:25:15.850Z",
|
||||||
|
"creationDate": "2025-09-12T16:25:15.850Z",
|
||||||
|
"id": "d2946603-1bfc-4eee-8805-b356010e9c65",
|
||||||
|
"folderId": "0504d89b-00aa-41a5-9355-b3f900a3b43f",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Test Account",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://testsite.com/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fido2Credentials": [],
|
||||||
|
"username": "test-account",
|
||||||
|
"password": "test",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passwordHistory": [],
|
||||||
|
"revisionDate": "2026-02-22T09:56:52.673Z",
|
||||||
|
"creationDate": "2024-10-23T19:17:45.433Z",
|
||||||
|
"id": "4e3b570e-3ead-4557-b5be-b212013dfcd0",
|
||||||
|
"folderId": "5d262faf-329d-4197-9e8d-b3f900a3934c",
|
||||||
|
"type": 1,
|
||||||
|
"reprompt": 0,
|
||||||
|
"name": "Another test account",
|
||||||
|
"notes": null,
|
||||||
|
"favorite": false,
|
||||||
|
"fields": [],
|
||||||
|
"login": {
|
||||||
|
"uris": [
|
||||||
|
{
|
||||||
|
"uri": "https://anothertestsite.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "anotherUser",
|
||||||
|
"password": "anotherPassword",
|
||||||
|
"totp": null
|
||||||
|
},
|
||||||
|
"collectionIds": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -346,8 +346,8 @@ void TestGui::testMergeDatabase()
|
||||||
fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
|
fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
|
||||||
triggerAction("actionDatabaseMerge");
|
triggerAction("actionDatabaseMerge");
|
||||||
|
|
||||||
QTRY_COMPARE(QApplication::focusWidget()->objectName(), QString("passwordEdit"));
|
QWidget* editPasswordMerge;
|
||||||
auto* editPasswordMerge = QApplication::focusWidget();
|
QTRY_VERIFY((editPasswordMerge = QApplication::focusWidget()) && editPasswordMerge->objectName() == "passwordEdit");
|
||||||
QVERIFY(editPasswordMerge->isVisible());
|
QVERIFY(editPasswordMerge->isVisible());
|
||||||
|
|
||||||
QTest::keyClicks(editPasswordMerge, "a");
|
QTest::keyClicks(editPasswordMerge, "a");
|
||||||
|
|
@ -1931,18 +1931,28 @@ void TestGui::testTrayRestoreHide()
|
||||||
trayIcon->activated(QSystemTrayIcon::Trigger);
|
trayIcon->activated(QSystemTrayIcon::Trigger);
|
||||||
QTRY_VERIFY(m_mainWindow->isVisible());
|
QTRY_VERIFY(m_mainWindow->isVisible());
|
||||||
|
|
||||||
|
// Wait out window hide grace period before triggering tray icon again
|
||||||
|
int gracePeriod = 250;
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
// Windows requires a shorter grace period
|
||||||
|
gracePeriod = 50;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Tools::wait(gracePeriod);
|
||||||
trayIcon->activated(QSystemTrayIcon::Trigger);
|
trayIcon->activated(QSystemTrayIcon::Trigger);
|
||||||
QTRY_VERIFY(!m_mainWindow->isVisible());
|
QTRY_VERIFY(!m_mainWindow->isVisible());
|
||||||
|
|
||||||
trayIcon->activated(QSystemTrayIcon::MiddleClick);
|
trayIcon->activated(QSystemTrayIcon::MiddleClick);
|
||||||
QTRY_VERIFY(m_mainWindow->isVisible());
|
QTRY_VERIFY(m_mainWindow->isVisible());
|
||||||
|
|
||||||
|
Tools::wait(gracePeriod);
|
||||||
trayIcon->activated(QSystemTrayIcon::MiddleClick);
|
trayIcon->activated(QSystemTrayIcon::MiddleClick);
|
||||||
QTRY_VERIFY(!m_mainWindow->isVisible());
|
QTRY_VERIFY(!m_mainWindow->isVisible());
|
||||||
|
|
||||||
trayIcon->activated(QSystemTrayIcon::DoubleClick);
|
trayIcon->activated(QSystemTrayIcon::DoubleClick);
|
||||||
QTRY_VERIFY(m_mainWindow->isVisible());
|
QTRY_VERIFY(m_mainWindow->isVisible());
|
||||||
|
|
||||||
|
Tools::wait(gracePeriod);
|
||||||
trayIcon->activated(QSystemTrayIcon::DoubleClick);
|
trayIcon->activated(QSystemTrayIcon::DoubleClick);
|
||||||
QTRY_VERIFY(!m_mainWindow->isVisible());
|
QTRY_VERIFY(!m_mainWindow->isVisible());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,7 @@ void TestImageAttachmentsWidget::testFitInView()
|
||||||
auto zoomFactor = m_imageAttachmentsView->transform();
|
auto zoomFactor = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
m_widget->setMinimumSize(m_widget->size() + QSize{100, 100});
|
m_widget->setMinimumSize(m_widget->size() + QSize{100, 100});
|
||||||
|
QTRY_VERIFY(zoomFactor != m_imageAttachmentsView->transform());
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QVERIFY(zoomFactor != m_imageAttachmentsView->transform());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestImageAttachmentsWidget::testZoomCombobox()
|
void TestImageAttachmentsWidget::testZoomCombobox()
|
||||||
|
|
@ -56,10 +53,7 @@ void TestImageAttachmentsWidget::testZoomCombobox()
|
||||||
QVERIFY(index != -1);
|
QVERIFY(index != -1);
|
||||||
|
|
||||||
m_zoomCombobox->setCurrentIndex(index);
|
m_zoomCombobox->setCurrentIndex(index);
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,10 +61,7 @@ void TestImageAttachmentsWidget::testEditZoomCombobox()
|
||||||
{
|
{
|
||||||
for (double i = 0.25; i < 5; i += 0.25) {
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
m_zoomCombobox->setCurrentText(QString::number(i * 100));
|
m_zoomCombobox->setCurrentText(QString::number(i * 100));
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,19 +70,13 @@ void TestImageAttachmentsWidget::testEditWithPercentZoomCombobox()
|
||||||
// Example 100 %
|
// Example 100 %
|
||||||
for (double i = 0.25; i < 5; i += 0.25) {
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
m_zoomCombobox->setCurrentText(QString("%1 %").arg(i * 100));
|
m_zoomCombobox->setCurrentText(QString("%1 %").arg(i * 100));
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example 100%
|
// Example 100%
|
||||||
for (double i = 0.25; i < 5; i += 0.25) {
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
m_zoomCombobox->setCurrentText(QString("%1%").arg(i * 100));
|
m_zoomCombobox->setCurrentText(QString("%1%").arg(i * 100));
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,10 +93,7 @@ void TestImageAttachmentsWidget::testInvalidValueZoomCombobox()
|
||||||
|
|
||||||
for (const auto& invalidValue : {"Help", "3,4", "", ".", "% 100"}) {
|
for (const auto& invalidValue : {"Help", "3,4", "", ".", "% 100"}) {
|
||||||
m_zoomCombobox->setCurrentText(invalidValue);
|
m_zoomCombobox->setCurrentText(invalidValue);
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectedTransform);
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), expectedTransform);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,10 +190,7 @@ void TestImageAttachmentsWidget::testZoomLowerBound()
|
||||||
true);
|
true);
|
||||||
|
|
||||||
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestImageAttachmentsWidget::testZoomUpperBound()
|
void TestImageAttachmentsWidget::testZoomUpperBound()
|
||||||
|
|
@ -237,8 +216,5 @@ void TestImageAttachmentsWidget::testZoomUpperBound()
|
||||||
true);
|
true);
|
||||||
|
|
||||||
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
from collections import defaultdict
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from urllib import request
|
|
||||||
|
|
||||||
txrc = Path.home() / '.transifexrc'
|
|
||||||
if not txrc.exists():
|
|
||||||
print('No Transifex config found. Run tx init first.')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
org = 'o:keepassxc'
|
|
||||||
proj = f'{org}:p:keepassxc'
|
|
||||||
resource = f'{proj}:r:share-translations-keepassxc-en-ts--master'
|
|
||||||
token = [l for l in open(txrc, 'r') if l.startswith('token')][0].split('=', 1)[1].strip()
|
|
||||||
member_blacklist = ['u:droidmonkey', 'u:phoerious']
|
|
||||||
|
|
||||||
|
|
||||||
def get_url(url):
|
|
||||||
req = request.Request(url)
|
|
||||||
req.add_header('Content-Type', 'application/vnd.api+json')
|
|
||||||
req.add_header('Authorization', f'Bearer {token}')
|
|
||||||
with request.urlopen(req) as resp:
|
|
||||||
return json.load(resp)
|
|
||||||
|
|
||||||
|
|
||||||
print('Fetching languages...', file=sys.stderr)
|
|
||||||
languages_json = get_url(f'https://rest.api.transifex.com/projects/{proj}/languages')
|
|
||||||
languages = {}
|
|
||||||
for lang in languages_json['data']:
|
|
||||||
languages[lang['id']] = lang['attributes']['name']
|
|
||||||
|
|
||||||
print('Fetching language stats...', file=sys.stderr)
|
|
||||||
language_stats_json = get_url('https://rest.api.transifex.com/resource_language_stats?'
|
|
||||||
f'filter[project]={proj}&filter[resource]={resource}')
|
|
||||||
completion = {}
|
|
||||||
for stat in language_stats_json['data']:
|
|
||||||
completion = stat['attributes']['translated_strings'] / stat['attributes']['total_strings']
|
|
||||||
if completion < .6:
|
|
||||||
languages.pop(stat['relationships']['language']['data']['id'])
|
|
||||||
|
|
||||||
print('Fetching language members...', end='', file=sys.stderr)
|
|
||||||
members_json = get_url(f'https://rest.api.transifex.com/team_memberships?filter[organization]={org}')
|
|
||||||
members = defaultdict(set)
|
|
||||||
for member in members_json['data']:
|
|
||||||
print('.', end='', file=sys.stderr)
|
|
||||||
if member['relationships']['user']['data']['id'] in member_blacklist:
|
|
||||||
continue
|
|
||||||
lid = member['relationships']['language']['data']['id']
|
|
||||||
if lid not in languages:
|
|
||||||
continue
|
|
||||||
user = get_url(member['relationships']['user']['links']['related'])['data']['attributes']['username']
|
|
||||||
members[lid].add(user)
|
|
||||||
print(file=sys.stderr)
|
|
||||||
|
|
||||||
print('<ul>')
|
|
||||||
for lang in sorted(languages, key=lambda x: languages[x]):
|
|
||||||
if not members[lang]:
|
|
||||||
continue
|
|
||||||
lines = [f' <li><strong>{languages[lang]}:</strong> ']
|
|
||||||
for i, m in enumerate(sorted(members[lang], key=lambda x: x.lower())):
|
|
||||||
if len(lines[-1]) + len(m) >= 120:
|
|
||||||
lines.append(' ')
|
|
||||||
lines[-1] += m
|
|
||||||
if i < len(members[lang]) - 1:
|
|
||||||
lines[-1] += ', '
|
|
||||||
lines[-1] += '</li>'
|
|
||||||
print('\n'.join(lines))
|
|
||||||
print('</ul>')
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "keepassxc",
|
"name": "keepassxc",
|
||||||
"version-string": "2.7.11",
|
"version-string": "2.7.11",
|
||||||
"builtin-baseline": "dfb72f61c5a066ab75cd0bdcb2e007228bfc3270",
|
"builtin-baseline": "66c0373dc7fca549e5803087b9487edfe3aca0a1",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
{
|
{
|
||||||
"name": "argon2",
|
"name": "argon2",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue