Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan White
87ccc85247 Improve CLI Show and Clip Commands
* Fixes #11767
* When requesting TOTP, only output that value and then exit
* Add explicit requests for UUID and Tags for both show and clip commands.
* When showing all values, also include UUID and Tags.
* Only hide the attribute names when specifically requesting them by name
2025-05-18 09:25:55 -04:00
197 changed files with 1507 additions and 5851 deletions

View File

@@ -54,7 +54,6 @@ IncludeCategories:
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertNewlineAtEOF: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''

View File

@@ -15,7 +15,6 @@ These are just guidelines, not rules. Use your best judgment, and feel free to p
* [Bug reports](#bug-reports)
* [Discuss with the team](#discuss-with-the-team)
* [Your first code contribution](#your-first-code-contribution)
* [Using AI](#using-ai)
* [Pull requests](#pull-requests)
* [Translations](#translations)
@@ -75,10 +74,6 @@ Unsure where to begin contributing to KeePassXC? You can start by looking throug
Both issue lists are sorted by total number of comments. While not perfect, looking at the number of comments on an issue can give a general idea of how much an impact a given change will have.
### Using AI
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If you use Generative AI to write the vast majority of your submission (e.g., agent-based or vibe coding) then you **must document your use of AI** in your pull request. Please include the service you used and/or model that generated the code. All code submissions go through a rigorous review process regardless of the development workflow used.
### Pull requests
Along with our desire to hear your feedback and suggestions, we're also interested in accepting direct assistance in the form of code.
@@ -87,7 +82,7 @@ All pull requests must comply with the above requirements and with the [stylegui
### Translations
Translations are managed on [Transifex](https://explore.transifex.com/keepassxc/keepassxc/) which offers a web interface.
Translations are managed on [Transifex](https://www.transifex.com/keepassxc/keepassxc/) which offers a web interface.
Please join an existing language team or request a new one if there is none.
If you open a Pull Request with new strings that require translations, you will need to run the following:

View File

@@ -1,13 +1,12 @@
[NOTE]: # ( Describe your changes in detail. Explain large or complex code modifications. )
[NOTE]: # ( Describe your changes in detail, why is this change required? )
[NOTE]: # ( Explain large or complex code modifications. )
[NOTE]: # ( If it fixes an open issue, please add "Fixes #XXX". )
[NOTE]: # ( If you used Generative AI to write the majority of your code, you must state this. )
## Screenshots
[NOTE]: # ( Do not include screenshots of your actual database! )
[TIP]: # ( Use View -> Allow Screen Capture )
## Testing strategy
[NOTE]: # ( Please describe in detail how you tested your changes. )
[TIP]: # ( We expect new code to be covered by unit tests and include helpful comments. )

View File

@@ -1,38 +0,0 @@
This is a C++ based repository that uses Qt5 as a primary support and GUI library. This repository is for a password manager application that stores passwords
and other highly sensitive information. The data format that passwords are stored is called KDBX which is a mixed binary and XML format that is fully encrypted
at rest. This format is unpacked into a series of data structures: Database, Groups, and Entries. Please follow these guidelines when contributing:
## Code Standards
### Required Before Each Commit
- Run `cmake --build . --target format` before committing any changes to ensure proper code formatting
- This will run clang-format to ensure all code conforms to the style guide
- From the checkout directory, also run `./release-tool i18n lupdate` to update translation files
### Development Flow
- Setup Build Folder: `mkdir build; cd build`
- Configure: `cmake -G Ninja -DWITH_XC_ALL=ON -DWITH_GUI_TESTS=ON ..`
- Build: `cmake --build . -- -j $(nproc)`
- Test: `ctest`
## Repository Structure
- `docs/topics`: Documentation written in asciidoctor syntax
- `src/`: Main source code files are under this subdirectory
- `src/autotype`: Code that emulates a virtual keyboard to type into interfaces
- `src/browser`: Interface with the KeePassXC Browser Extension using a JSON-based protocol
- `src/cli`: Command Line Interface code
- `src/core`: Contains files that define the data model and other shared code structures
- `src/format`: Code for import/export and reading/writing of KDBX databases
- `src/fdosecrets`: freedesktop.org Secret Service interface code
- `src/quickunlock`: Quick unlock interfaces for various platforms
- `src/sshagent`: SSH Agent interface code to load private keys from the database into ssh-agent
- `tests/`: Test source code files
- `tests/gui`: GUI test source code files
## Key Guidelines
1. Follow C++20 and Qt5 best practices and idiomatic patterns
2. Maintain existing code structure and organization
3. Prefer not to edit cryptographic handling code or other sensitive parts of the code base
4. Write unit tests for new functionality using QTest scaffolding
5. Suggest changes to the `docs/topics` folder when appropriate
6. Unless the change is simple, don't actually make edits to .ui files, just suggest the changes needed

View File

@@ -1,29 +0,0 @@
name: "Copilot Setup Steps"
# Setup the environment for Copilot agents to run in
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
# Needed to clone the repository
permissions:
contents: read
# Install dependencies
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update
sudo apt install --no-install-recommends build-essential cmake g++ ninja-build qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev

View File

@@ -223,7 +223,6 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/totp-copy.svg
share/icons/application/scalable/actions/totp-copy-password.svg
share/icons/application/scalable/actions/totp-edit.svg
share/icons/application/scalable/actions/totp-invalid.svg
share/icons/application/scalable/actions/trash.svg
share/icons/application/scalable/actions/url-copy.svg
share/icons/application/scalable/actions/user-guide.svg

View File

@@ -56,10 +56,6 @@ You may directly contribute your own code by submitting a pull request. Please r
Contributors are required to adhere to the project's [Code of Conduct](CODE-OF-CONDUCT.md).
## Generative AI
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If the majority of a code submission is made using Generative AI (e.g., agent-based or vibe coding) then **we will document that in the pull request.** All code submissions go through a rigorous review process regardless of the development workflow or submitter.
## License
KeePassXC code is licensed under GPL-2 or GPL-3. Additional licensing for third-party files is detailed in [COPYING](./COPYING).

View File

@@ -24,7 +24,7 @@ if(NOT PCSC_FOUND)
# Additional search paths for Windows if not running in Visual Studio environment
if (WIN32)
# Resolve the ambiguity of using two names for one architecture
# Resolve the ambiguity of using two names for one architechture
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x64")
set(ARCH_DIR "x64")
else()

View File

@@ -7,7 +7,6 @@ KeePassXC Team <team@keepassxc.org>
:imagesdir: images
:stylesheet: styles/dark.css
:toc: left
:experimental:
ifdef::backend-pdf[]
:title-page:
:title-logo-image: {imagesdir}/kpxc_logo.png

View File

@@ -7,7 +7,6 @@ KeePassXC Team <team@keepassxc.org>
:stylesheet: styles/dark.css
:toc: left
:sectanchors:
:experimental:
ifdef::backend-pdf[]
:title-page:
:title-logo-image: {imagesdir}/kpxc_logo.png
@@ -37,8 +36,6 @@ include::topics/Passkeys.adoc[tags=*]
include::topics/AutoType.adoc[tags=*]
include::topics/SecretService.adoc[tags=*]
include::topics/SSHAgent.adoc[tags=*]
include::topics/Reference.adoc[tags=*]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -52,11 +52,12 @@ It provides the ability to query and modify the entries of a KeePass database, d
Removes the named attachment from an entry.
*clip* [_options_] <__database__> <__entry__> [_timeout_]::
Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard.
Copies an attribute, current TOTP value, UUID, or tags list of a database entry to the clipboard.
If no attribute name is specified using the *-a* option, the password is copied.
If multiple entries with the same name exist in different groups, only the attribute for the first one is copied.
For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name.
Optionally, a timeout in seconds can be specified to automatically clear the clipboard, the default timeout is 10 seconds, set to 0 to disable.
Note: an error will be thrown if you specify multiple options at once (eg, *--uuid* and *-a*).
*close*::
In interactive mode, closes the currently opened database (see *open*).
@@ -143,8 +144,8 @@ It provides the ability to query and modify the entries of a KeePass database, d
Searches all entries that match a specific search term in a database.
*show* [_options_] <__database__> <__entry__>::
Shows the title, username, password, URL and notes of a database entry.
Can also show the current TOTP.
Shows the title, username, password, URL and notes of a database entry by default.
Can also show the current TOTP, entry UUID, and tags list.
Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the *clip* command section also applies here.
== OPTIONS
@@ -235,6 +236,12 @@ The same password generation options as documented for the generate command can
Copies the current TOTP instead of the specified attribute to the clipboard.
Will report an error if no TOTP is configured for the entry.
*--uuid*::
Copies the UUID of the entry to the clipboard.
*--tags*::
Copies the tags of the entry to the clipboard.
*-b*, *--best*::
Try to find and copy to clipboard a unique entry matching the input
If a unique matching entry is found it will be copied to the clipboard.
@@ -262,7 +269,6 @@ The same password generation options as documented for the generate command can
*-a*, *--attributes* <__attribute__>...::
Shows the named attributes.
This option can be specified more than once, with each attribute shown one-per-line in the given order.
If no attributes are specified and *-t* is not specified, a summary of the default attributes is given.
Protected attributes will be displayed in clear text if specified explicitly by this option.
*--all*::
@@ -275,7 +281,13 @@ The same password generation options as documented for the generate command can
Shows the attachment names along with the size of the attachments.
*-t*, *--totp*::
Also shows the current TOTP, reporting an error if no TOTP is configured for the entry.
Shows the current TOTP and then exits. An error is thrown if no TOTP is configured for the entry.
*--uuid*::
Shows the UUID of the entry.
*--tags*::
Shows the tag list of the entry.
=== Diceware options
*-W*, *--words* <__count__>::

View File

@@ -4,4 +4,3 @@ KeePassXC Team <team@keepassxc.org>
:stylesheet: ../styles/dark.css
:icons: font
:toc: left
:experimental:

View File

@@ -24,13 +24,13 @@ You can also set the time to remember the last used entry between presses of the
=== Configure Auto-Type Sequences
Each entry in your database can have multiple Auto-Type sequences associated with various window titles. Simulated key presses can be sent to any other currently open window of your choice (web browser windows, login dialogs boxes, and so on). When the Global Auto-Type hotkey is pressed, KeePassXC will search your database for entries matching the current selected window title.
NOTE: The default Auto-Type sequence is `{USERNAME}{TAB}{PASSWORD}{ENTER}`. This means that it first types the username of the selected entry, then presses the kbd:[Tab] key, then types the password of the entry and finally presses the kbd:[Enter] key.
NOTE: The default Auto-Type sequence is `{USERNAME}{TAB}{PASSWORD}{ENTER}`. This means that it first types the username of the selected entry, then presses the `Tab` key, then types the password of the entry and finally presses the `Enter` key.
TIP: To change the default Auto-Type sequence for all entries of your database, edit the root (top-most) group of your database and set a specific sequence. Child groups and entries will inherit this sequence by default.
To configure Auto-Type sequences for your entries, perform the following steps:
1. Navigate to the entries list and open the desired entry for editing. Click the _Auto-Type_ item from the left-hand menu bar *(1)*. Press the kbd:[+] button *(2)* to add a new sequence entry. Select the desired window using the drop-down menu, or simply type a window title in the box *(3)*.
1. Navigate to the entries list and open the desired entry for editing. Click the _Auto-Type_ item from the left-hand menu bar *(1)*. Press the `+` button *(2)* to add a new sequence entry. Select the desired window using the drop-down menu, or simply type a window title in the box *(3)*.
+
TIP: You can use an asterisk (`\*`) to match any value (e.g., when a window title contains a dynamic filename or website name). Set the window title to `*` to match all windows. Leave the window title blank to offer additional default Auto-Type sequences, such as custom attributes.
+
@@ -60,7 +60,7 @@ image::autotype_entry_sequences.png[]
|Press the corresponding keyboard key
|{UP}, {DOWN}, {LEFT}, {RIGHT} |Press the corresponding arrow key
|{LEFTBRACE}, {RIGHTBRACE} |Press kbd:[{] or kbd:[}], respectively
|{LEFTBRACE}, {RIGHTBRACE} |Press `{` or `}`, respectively
|{&lt;KEY&gt; X} |Repeat &lt;KEY&gt; X times (e.g., {SPACE 5} inserts five spaces)
|{DELAY=X} |Set delay between key presses to X milliseconds
|{DELAY X} |Pause typing for X milliseconds
@@ -89,7 +89,7 @@ When you press the global Auto-Type hotkey, KeePassXC searches all unlocked data
.Auto-Type sequence selection
image::autotype_selection_dialog.png[,70%]
Perform the selected Auto-Type sequence by double clicking the desired row or pressing kbd:[Enter]. Press the up and down arrows to navigate the list. Sequences can be filtered through the text edit field.
Perform the selected Auto-Type sequence by double clicking the desired row or pressing _Enter_. Press the up and down arrows to navigate the list. Sequences can be filtered through the text edit field.
.Auto-Type search database
image::autotype_selection_dialog_search.png[,70%]
@@ -104,7 +104,7 @@ The option to type just the username, password, or current TOTP value is availab
TIP: On Windows, you will see an option to use a virtual keyboard in this sub-menu. This is an experimental feature that allows you to type into virtual machines by simulating actual keyboard presses. Some international keyboards may be unsupported due to limitations in the Windows API.
=== Performing Entry-Level Auto-Type
You can quickly activate the default Auto-Type sequence for a particular entry using Entry-Level Auto-Type. For this operation, the KeePassXC window will be minimized and the Auto-Type sequence occurs in the previously selected window. You can perform Entry-Level Auto-Type from the toolbar icon *(A)*, entry context menu *(B)*, or by pressing kbd:[Ctrl+Shift+V].
You can quickly activate the default Auto-Type sequence for a particular entry using Entry-Level Auto-Type. For this operation, the KeePassXC window will be minimized and the Auto-Type sequence occurs in the previously selected window. You can perform Entry-Level Auto-Type from the toolbar icon *(A)*, entry context menu *(B)*, or by pressing `Ctrl+Shift+V`.
WARNING: Be careful when using Entry-Level Auto-Type as you can inadvertently type into the wrong window. For example, a chat window or email.

View File

@@ -107,34 +107,12 @@ You can then choose to update/add the credentials to your KeePassXC database dir
4. When you have successfully submitted the password on the website, a popup will appear asking you to either update an existing entry or add a new one.
// tag::advanced[]
=== Browser Integration Report
You can see a cross-section of all browser-related settings applied to entries within a database through the Browser Statistics report. To access, use the _Database_ -> _Database reports..._ menu option then click on _Browser Statistics_ on the left-hand menu. From here you can see all entries with URLs applied to them, explicitly allowed and denied URLs, and any entries with custom browser settings.
=== Browser statistics
You can see a cross-section of all browser-related settings applied to entries within a database through the Browser Statistics report. To access these, use the _Database_ -> _Database reports..._ menu option then click on _Browser Statistics_ on the left-hand menu. From here you can see all entries with URLs applied to them, explicitly allowed and denied URLs, and any entries with custom browser settings.
TIP: You can delete remembered site settings from the report by right clicking the entry you want to reset and selecting "Delete plugin data from entry".
.Browser Integration Report
.Browser statistics
image::browser_statistics.png[]
=== Additional Fill-In Fields
Sometimes login pages have additional fields you would like to fill (e.g., account number). Use the following instructions to add them:
1. Edit the entry you want to add fields to. Go to the advanced tab and add the attributes you need. Each attribute *must start with* `KPH:`, but otherwise the name does not matter. If multiple KPH attributes are defined, they are used in alphabetical order (i.e., the order shown in KeePassXC).
2. Within the browser, navigate to the page you want to use the additional fields on. Select the "Choose Custom Login Fields" button from the extension popup window. Choose Username, Password and String Field(s). Confirm the selections.
3. Refresh the web page. The new KPH attribute(s) should be filled to the extra fields.
.String Fields Selection in Browser
image:browser_integration_additional_attribute.png[]
=== Clearing Remembered Sites
Entries that you have chosen to remember allow/deny rules are stored in their respect custom data fields. You can clear all of these remembered settings at once through the database settings. Follow these steps:
1. Go to *Database* → *Database Settings* or click the database settings icon in the toolbar.
2. Go to the *Browser Integration* tab, then click on the *Forget all site-specific settings on entries* button.
3. Confirm this action in the popup dialog. This cannot be undone once the database is saved.
+
.Clear Remembered Sites
image::browser_integration_clear_sites.png[,100%]
=== Advanced Usage
You can configure unique browser integration behavior for each entry. This allows you to add multiple URLs to an entry, hide an entry from the browser integration, and more. To access these settings, open an entry for editing then click on _Browser Integration_ option in the left-hand menu *(1)*.

View File

@@ -75,7 +75,7 @@ NOTE: On Windows, you will be prompted to authenticate to Windows Hello after un
.Windows Hello example
image::quick_unlock_windows_hello.png[]
When your database is locked, you will see the following unlock dialog. Simply press kbd:[Enter] or click on _Unlock Database_ to initiate the biometric authentication process. If you are using a hardware key (e.g. Yubikey), it must be connected to your computer to complete the unlock.
When your database is locked, you will see the following unlock dialog. Simply press _Enter_ or click on _Unlock Database_ to initiate the biometric authentication process. If you are using a hardware key (e.g. Yubikey), it must be connected to your computer to complete the unlock.
.Quick Unlock
image::quick_unlock.png[]
@@ -92,7 +92,7 @@ All the details such as usernames, passwords, URLs, attachments, notes, and so o
To add an entry, perform the following step:
1. Navigate to Entries > New Entry (or press kbd:[Ctrl+N]). The following screen appears:
1. Navigate to Entries > New Entry (Or, press Ctrl+N). The following screen appears:
+
.Adding a new entry
image::edit_entry.png[]
@@ -115,7 +115,7 @@ To edit the details in an entry, perform the following steps:
1. Select the entry you want to edit.
2. Press kbd:[Enter], click the edit toolbar icon, or right-click and select Edit Entry from the menu.
2. Press `Enter`, click the edit toolbar icon, or right-click and select Edit Entry from the menu.
3. Make the desired changes.
@@ -156,13 +156,13 @@ TIP: Each KeePass application has different default icons. If you use a mobile a
==== Deleting an Entry
To delete an entry, perform the following steps:
1. Select the entry you want to delete and press the kbd:[Del] button on your keyboard.
1. Select the entry you want to delete and press the `Delete` button on your keyboard.
2. You will be prompted to move the entry to the Recycle Bin (if enabled).
+
NOTE: You can disable the recycle bin within the Database Settings. If the recycle bin is disabled then deleted entries will be permanently removed from the database.
3. To permanently delete the entry, navigate to the Recycle Bin, select the entry you want to delete and press the kbd:[Del] button on your keyboard.
3. To permanently delete the entry, navigate to the Recycle Bin, select the entry you want to delete and press the `Delete` button on your keyboard.
// tag::advanced[]
==== Clone an Entry
@@ -170,7 +170,7 @@ Creating a clone of an entry provides you a ready-to-use template for creating n
To create a clone of an existing entry, perform the following steps:
1. Right-click on the entry for which you want to create a clone and select _Clone Entry_. Alternatively, select the desired entry and press kbd:[Ctrl+K].
1. Right-click on the entry for which you want to create a clone and select _Clone Entry_. Alternatively, select the desired entry and press `Ctrl+K`.
+
.Clone entry from context menu
image::clone_entry.png[]
@@ -375,7 +375,7 @@ image::database_settings.png[]
* *Database name:* This is the default identifier for your database and is shown in the tab bar and title bar (when active). You can change this name as desired.
* *Database description:* Provide some meaningful description for your database.
* *Default username:* Provide a default username for all new entries that you create in this database.
* *Public Database Metadata:* Here you can set a public (unencrypted) name, icon, and color for your database. This is used on the database unlock screen to help distinguish multiple databases from each other.
* *Public Databse Metadata:* Here you can set a public (unencrypted) name, icon, and color for your database. This is used on the database unlock screen to help distinguish multiple databases from each other.
* *Max history items:* This is the maximum number of history items that are stored for each entry. When you set this to 0, no history will be saved. Set this value to a low value to prevent the database from getting too large (we recommend no more than 10).
* *Max. history size:* When the history of an entry gets above this size, it is truncated. For example, this happens when entries have large attachments. Set this value small to prevent the database from getting too large (we recommend 6 MiB).
* *Use recycle bin:* Select this check-box if you want deleted entries to move to the recycle bin instead of being permanently removed. The recycle bin will be created if it does not already exist after your first deletion. To delete entries permanently, you must empty the recycle bin manually.

View File

@@ -3,63 +3,52 @@ include::.sharedheader[]
:imagesdir: ../images
// tag::content[]
NOTE: On macOS please substitute kbd:[Ctrl] with kbd:[Cmd] (AKA kbd:[⌘]).
NOTE: On macOS please substitute `Ctrl` with `Cmd` (aka `⌘`).
[grid=rows, frame=none, width=75%]
|===
|Action | Keyboard Shortcut
|Action | Keyboard Shortcut
|Settings | kbd:[Ctrl + ,]
|Open Database | kbd:[Ctrl + O]
|Save Database | kbd:[Ctrl + S]
|Save Database As | kbd:[Ctrl + Shift + S]
|New Database | kbd:[Ctrl + Shift + N]
|Close Database | kbd:[Ctrl + W] +
_or_ +
kbd:[Ctrl + F4]
|Lock Current Database | kbd:[Ctrl + L]
|Lock All Databases | kbd:[Ctrl + Shift + L]
|Database Settings | kbd:[Ctrl + Shift + ,]
|Database Reports | kbd:[Ctrl + Shift + R]
|Quit | kbd:[Ctrl + Q]
|New Entry | kbd:[Ctrl + N]
|Edit Entry | kbd:[Enter] +
_or_ +
kbd:[Ctrl + E]
|Delete Entry | kbd:[Del]
|Clone Entry | kbd:[Ctrl + D]
|Copy Username | kbd:[Ctrl + B]
|Copy Password | kbd:[Ctrl + C]
|Copy URL | kbd:[Ctrl + U]
|Open URL | kbd:[Ctrl + Shift + U]
|Copy TOTP | kbd:[Ctrl + T]
|Copy Password and TOTP | kbd:[Ctrl + Y]
|Show TOTP | kbd:[Ctrl + Shift + T]
|Trigger AutoType | kbd:[Ctrl + Shift + V]
|Add key to SSH Agent | kbd:[Ctrl + H]
|Remove key from SSH Agent | kbd:[Ctrl + Shift + H]
|Jump to Group (from search) | kbd:[Ctrl + Shift + J]
|Move entry up (if unsorted) | kbd:[Ctrl + Alt + Up]
|Move entry down (if unsorted) | kbd:[Ctrl + Alt + Down]
|Sort Groups A-Z | kbd:[Ctrl + Down]
|Sort Groups Z-A | kbd:[Ctrl + Up]
|Minimize Window | kbd:[Ctrl + M]
|Hide Window | kbd:[Ctrl + Shift + M]
|Select Next Database Tab | kbd:[Ctrl + Tab] +
_or_ +
kbd:[Ctrl + PgDn]
|Select Previous Database Tab | kbd:[Ctrl + Shift + Tab] +
_or_ +
kbd:[Ctrl + PgUp]
|Select the nth database | kbd:[Ctrl + &lt;n&gt;], where kbd:[&lt;n&gt;] is the number of the database tab
|Toggle Passwords Hidden | kbd:[Ctrl + Shift + C]
|Toggle Usernames Hidden | kbd:[Ctrl + Shift + B]
|Focus Groups (edit if focused) | kbd:[F1]
|Focus Entries (edit if focused) | kbd:[F2]
|Focus Search | kbd:[F3] +
_or_ +
kbd:[Ctrl + F]
|Clear Search | kbd:[Esc]
|Show Keyboard Shortcuts | kbd:[Ctrl + /]
|Settings | Ctrl + ,
|Open Database | Ctrl + O
|Save Database | Ctrl + S
|Save Database As | Ctrl + Shift + S
|New Database | Ctrl + Shift + N
|Close Database | Ctrl + W ; Ctrl + F4
|Lock Current Database | Ctrl + L
|Lock All Databases | Ctrl + Shift + L
|Database Settings | Ctrl + Shift + ,
|Database Reports | Ctrl + Shift + R
|Quit | Ctrl + Q
|New Entry | Ctrl + N
|Edit Entry | Enter ; Ctrl + E
|Delete Entry | Delete
|Clone Entry | Ctrl + D
|Copy Username | Ctrl + B
|Copy Password | Ctrl + C
|Copy URL | Ctrl + U
|Open URL | Ctrl + Shift + U
|Copy TOTP | Ctrl + T
|Copy Password and TOTP | Ctrl + Y
|Show TOTP | Ctrl + Shift + T
|Trigger AutoType | Ctrl + Shift + V
|Add key to SSH Agent | Ctrl + H
|Remove key from SSH Agent | Ctrl + Shift + H
|Move entry up (if unsorted) | Ctrl + Alt + Up
|Move entry down (if unsorted) | Ctrl + Alt + Down
|Sort Groups A-Z | Ctrl + Down
|Sort Groups Z-A | Ctrl + Up
|Minimize Window | Ctrl + M
|Hide Window | Ctrl + Shift + M
|Select Next Database Tab | Ctrl + Tab ; Ctrl + PageDn
|Select Previous Database Tab | Ctrl + Shift + Tab ; Ctrl + PageUp
|Select the nth database | Ctrl + n, where n is the number of the database tab
|Toggle Passwords Hidden | Ctrl + Shift + C
|Toggle Usernames Hidden | Ctrl + Shift + B
|Focus Groups (edit if focused) | F1
|Focus Entries (edit if focused) | F2
|Focus Search | F3 ; Ctrl + F
|Clear Search | Escape
|Show Keyboard Shortcuts | Ctrl + /
|===
// end::content[]

View File

@@ -19,8 +19,8 @@ image::password_generator.png[]
3. Select the length of the desired password by dragging the Length slider.
4. Select the character-sets that you want to include in your password.
5. Use the regenerate button (kbd:[Ctrl + R]) to make a new password using the chosen options.
6. Use the clipboard button (kbd:[Ctrl + C]) to copy the generated password to the clipboard.
5. Use the regenerate button (Ctrl + R) to make a new password using the chosen options.
6. Use the clipboard button (Ctrl + C) to copy the generated password to the clipboard.
7. Click the Advanced button to specify additional conditions for your desired password.
+
.Advanced Password Generator Options
@@ -39,6 +39,6 @@ Word Count slider.
3. In the Word Separator field, enter a character, word, number, or space that you want to use as a separator between the words in your passphrase.
4. _(Optional)_ You can choose a word case between lower, upper, and title case options.
5. _(Optional)_ You can also load your own custom word lists. Click the plus sign button to the right of the wordlist selection dialog to choose a custom word list. You can download alternative lists from the https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases[EFF's Website] or from https://github.com/redacted/XKCD-password-generator#additional-languages[GitHub].
6. Click the Regenerate button (kbd:[Ctrl + R]) to generate a new random passphrase.
7. Click the Clipboard button (kbd:[Ctrl + C]) to copy the passphrase to the clipboard.
6. Click the Regenerate button (Ctrl + R) to generate a new random passphrase.
7. Click the Clipboard button (Ctrl + C) to copy the passphrase to the clipboard.
// end::content[]

View File

@@ -77,8 +77,8 @@ Examples: +
|Press the corresponding keyboard key
|{UP}, {DOWN}, {LEFT}, {RIGHT} |Press the corresponding arrow key
|{F1}, {F2}, ..., {F16} |Press kbd:[F1], kbd:[F2], etc.
|{LEFTBRACE}, {RIGHTBRACE} |Press kbd:[{] or kbd:[}], respectively
|{F1}, {F2}, ..., {F16} |Press F1, F2, etc.
|{LEFTBRACE}, {RIGHTBRACE} |Press `{` or `}`, respectively
|{&lt;KEY&gt; X} |Repeat &lt;KEY&gt; X times (e.g., {SPACE 5} inserts five spaces)
|{DELAY=X} |Set delay between key presses to X milliseconds
|{DELAY X} |Pause typing for X milliseconds
@@ -90,10 +90,10 @@ Examples: +
|===
|Modifier |Description
|+ |kbd:[Shift]
|^ |kbd:[Ctrl]
|% |kbd:[Alt]
|# |kbd:[Win]/kbd:[Cmd]
|+ |SHIFT
|^ |CTRL
|% |ALT
|# |WIN/CMD
|===
*Text Conversions:*

View File

@@ -1,48 +0,0 @@
= KeePassXC Secret Service Integration
include::.sharedheader[]
:imagesdir: ../images
// tag::content[]
== Secret Service Integration
This feature allows KeePassXC to act as a Secret Service provider over DBus. It enables applications to store and retrieve secrets securely via the https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/[Secret Storage specification]. While running, KeePassXC acts as a Secret Service server registered on DBus so clients like seahorse, python-secretstorage, secret-tool, or other implementations can connect and access the exposed database in KeePassXC.
=== Enabling the Integration
Only one secret service provider can be enabled at a time. You may have to disable other providers, such as GNOME Keyring or KWallet, to use KeePassXC as a secret service provider. You will see a notice when attempting to enable KeePassXC as the secret service provider if another is already running.
To replace most third party secret service providers with KeePassXC, run the following shell snippet:
```bash
mkdir -p "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services"
cat > "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services/org.freedesktop.secrets.service" <<EOF
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/bin/keepassxc
EOF
```
NOTE: You may need to restart your session or log out and back in for the changes to take effect.
1. Open KeePassXC → **Tools → Settings → Secret Service Integration** → check **Enable KeePassXC Freedesktop.org Secret Service Integration**. Then press OK to save this setting and enable the integration. Go back into this settings screen to see currently open databases that you can unlock and edit their exposure to secret service.
+
.Secret Service Settings
image::secretservice_enable_settings.png[]
2. Either click the pencil icon in the previous settings screen, or go to **Database → Database Settings → Secret Service Integration**. Enable **Expose entries under this group**, and select the desired group. All entries within this group and all subgroups will be exposed to the service.
+
.Secret Service Database Settings
image::secretservice_database_settings.png[]
3. Use apps that integrate with secret service to start saving and using credentials within KeePassXC. If you enabled confirmation prior to access, you will see the following dialog:
+
.Secret Service Access Confirmation Dialog
image::secretservice_access_dialog.png[]
TIP: When applications use `secret-tool` and you have access confirmation enabled, then you will be prompted each time credentials are requested. This is due to `secret-tool` obtaining a new process id each time it is run.
=== Implementation Details
* The user can specify the database and group that is exposed to the service.
* Desktop notifications when a secret is retrieved and access confirmation dialogs.
* `FdoSecrets::Service` is the top level DBus service. There is one `FdoSecrets::Collection` per opened database tab and each entry under the exposed database group has a corresponding `FdoSecrets::Item` DBus object.
* The following entry attributes are exposed to the secret service: Title, Username, Password, URL, Notes, TOTP, and non-protected Custom Attributes.
// end::content[]

View File

@@ -18,16 +18,7 @@ image::main_interface.png[]
*(D) Preview* Shows a preview of the selected group or entry. You can interact with most information stored in an entry from here without opening the entry for editing. You can temporarily hide this preview using the down-arrow button on the right hand side or completely disable it from the View menu.
[TIP]
====
Starting with version 2.7.0, double-click copying of entry usernames and passwords is disabled by default.
To enable it:
. Open *KeePassXC*, and navigate to *Tools* → *Settings*.
. In the left sidebar, select *General*.
. Under *Entry Management*, check the box for _"Copy data on double clicking field in entry view"_.
====
TIP: You can enable double-click copying of entry username and password in the Application Security Settings. This is turned off by default starting with version 2.7.0.
=== Toolbar
The toolbar provides a quick way to perform common tasks with your database. Some entries in the toolbar are dynamically disabled based on the information contained in the selected entry. Every common action in KeePassXC can be controlled with a keyboard shortcut as well.

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.47 15.08L11 13V7H12.5V12.25L15.58 14.08C15.17 14.36 14.79 14.7 14.47 15.08M13.08 19.92C12.72 19.97 12.37 20 12 20C7.58 20 4 16.42 4 12S7.58 4 12 4 20 7.58 20 12C20 12.37 19.97 12.72 19.92 13.08C20.61 13.18 21.25 13.4 21.84 13.72C21.94 13.16 22 12.59 22 12C22 6.5 17.5 2 12 2S2 6.5 2 12C2 17.5 6.47 22 12 22C12.59 22 13.16 21.94 13.72 21.84C13.4 21.25 13.18 20.61 13.08 19.92M21.12 15.46L19 17.59L16.88 15.47L15.47 16.88L17.59 19L15.47 21.12L16.88 22.54L19 20.41L21.12 22.54L22.54 21.12L20.41 19L22.54 16.88L21.12 15.46Z" /></svg>

Before

Width:  |  Height:  |  Size: 602 B

View File

@@ -92,7 +92,6 @@
<file>application/scalable/actions/totp-copy.svg</file>
<file>application/scalable/actions/totp-copy-password.svg</file>
<file>application/scalable/actions/totp-edit.svg</file>
<file>application/scalable/actions/totp-invalid.svg</file>
<file>application/scalable/actions/trash.svg</file>
<file>application/scalable/actions/url-copy.svg</file>
<file>application/scalable/actions/user-guide.svg</file>

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,6 @@
<!-- Custom Controls for KPXC Installer -->
<Control Id="DesktopShortcutCheckBox" Type="CheckBox" X="20" Y="150" Width="290" Height="17" Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="Create a shortcut on the desktop" />
<Control Id="AutostartCheckBox" Type="CheckBox" X="20" Y="170" Width="290" Height="17" Property="AUTOSTARTPROGRAM" CheckBoxValue="1" Text="Autostart KeePassXC on login" />
<Control Id="AddToPathCheckBox" Type="CheckBox" X="20" Y="190" Width="290" Height="17" Property="ADDTOPATH" CheckBoxValue="1" Text="Add KeePassXC to the PATH environment variable" />
</Dialog>
</UI>
</Fragment>

View File

@@ -56,13 +56,6 @@
<Condition>AUTOSTARTPROGRAM</Condition>
</Component>
<!-- Add KeePassXC to PATH -->
<Component Id="AddKeePassXCToPath" Guid="*" Directory="INSTALL_ROOT">
<Condition>ADDTOPATH</Condition>
<Environment Id="KeePassXCSystemPathEntryENV" Action="set" Part="last" Name="PATH" Value="[INSTALL_ROOT]" System="yes"/>
<RegistryValue Root="HKCU" Key="Software\KeePassXC" Name="AddedToPATH" Type="integer" Value="1" KeyPath="yes" />
</Component>
<DirectoryRef Id="TARGETDIR">
<!-- Startmenu shortcuts -->
<Directory Id="ProgramMenuFolder">
@@ -80,7 +73,6 @@
Name="KeePassXC - User Guide"
Target="[#CM_FP_share.docs.KeePassXC_UserGuide.html]"
WorkingDirectory="INSTALL_ROOT" />
<RemoveFile Id="RemoveShortcuts" Name="*.*" On="uninstall" />
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\KeePassXC" Name="StartMenuShortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
@@ -112,21 +104,15 @@
<RegistrySearch Id="AutoStartSearch" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\Run" Name="$(var.CPACK_PACKAGE_NAME)" Type="raw" />
</Property>
<Property Id="INSTALLDESKTOPSHORTCUT" Secure="yes" />
<Property Id="ADDTOPATH" Value="1" Secure="yes" />
<Property Id="ADDTOPATH_REGISTRY">
<RegistrySearch Id="AddToPathSearch" Root="HKCU" Key="Software\KeePassXC" Name="AddedToPATH" Type="raw" />
</Property>
<!-- Set properties based on existing conditions, prevents changing state on upgrade -->
<SetProperty Id="AUTOSTARTPROGRAM" After="AppSearch" Value="" Sequence="first">AUTOSTARTPROGRAM="0" OR (WIX_UPGRADE_DETECTED AND NOT AUTOSTARTPROGRAM_REGISTRY)</SetProperty>
<SetProperty Id="ADDTOPATH" After="AppSearch" Value="" Sequence="first">ADDTOPATH="0" OR (WIX_UPGRADE_DETECTED AND NOT ADDTOPATH_REGISTRY)</SetProperty>
<SetProperty Id="AUTOSTARTPROGRAM" After="AppSearch" Value="">AUTOSTARTPROGRAM="0" OR (WIX_UPGRADE_DETECTED AND NOT AUTOSTARTPROGRAM_REGISTRY)</SetProperty>
<SetProperty Id="LicenseAccepted" After="AppSearch" Value="1">WIX_UPGRADE_DETECTED</SetProperty>
<FeatureRef Id="ProductFeature">
<ComponentRef Id="ApplicationShortcuts" />
<ComponentRef Id="Autostart" />
<ComponentRef Id="DesktopShortcut" />
<ComponentRef Id="AddKeePassXCToPath"/>
</FeatureRef>
<!-- Action to launch application after installer exits -->

View File

@@ -134,7 +134,6 @@ set(gui_SOURCES
gui/IconModels.cpp
gui/KMessageWidget.cpp
gui/MainWindow.cpp
gui/MergeDialog.cpp
gui/MessageBox.cpp
gui/MessageWidget.cpp
gui/PasswordWidget.cpp
@@ -160,14 +159,8 @@ set(gui_SOURCES
gui/entry/EntryAttachmentsModel.cpp
gui/entry/EntryAttachmentsWidget.cpp
gui/entry/EntryAttributesModel.cpp
gui/entry/EditEntryAttachmentsDialog.cpp
gui/entry/NewEntryAttachmentsDialog.cpp
gui/entry/PreviewEntryAttachmentsDialog.cpp
gui/entry/attachments/TextAttachmentsWidget.cpp
gui/entry/attachments/ImageAttachmentsWidget.cpp
gui/entry/attachments/ImageAttachmentsView.cpp
gui/entry/attachments/TextAttachmentsPreviewWidget.cpp
gui/entry/attachments/TextAttachmentsEditWidget.cpp
gui/entry/attachments/AttachmentWidget.cpp
gui/entry/EntryHistoryModel.cpp
gui/entry/EntryModel.cpp
gui/entry/EntryView.cpp

View File

@@ -637,16 +637,10 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
// Platform-specific field clearing
actions << QSharedPointer<AutoTypeClearField>::create();
} else if (placeholder == "totp") {
if (entry->hasValidTotp()) {
// Entry totp (requires special handling)
QString totp = entry->totp();
for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
} else if (entry->hasTotp()) {
// Entry has TOTP configured but invalid settings
error = tr("Entry has invalid TOTP settings");
return {};
// Entry totp (requires special handling)
QString totp = entry->totp();
for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
} else if (placeholder.startsWith("pickchars")) {
// Reset to the original capture to preserve case

View File

@@ -37,7 +37,6 @@ enum MENU_FIELD
USERNAME = 1,
PASSWORD,
TOTP,
URL,
};
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
@@ -265,8 +264,7 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
bool hasUsername = !match.first->username().isEmpty();
bool hasPassword = !match.first->password().isEmpty();
bool hasTotp = match.first->hasValidTotp();
bool hasUrl = !match.first->url().isEmpty();
bool hasTotp = match.first->hasTotp();
for (auto action : m_actionMenu->actions()) {
auto prop = action->property(MENU_FIELD_PROP_NAME);
@@ -281,9 +279,6 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
case MENU_FIELD::TOTP:
action->setEnabled(hasTotp);
break;
case MENU_FIELD::URL:
action->setEnabled(hasUrl);
break;
}
}
}
@@ -295,19 +290,15 @@ void AutoTypeSelectDialog::buildActionMenu()
auto typeUsernameAction = new QAction(icons()->icon("auto-type"), tr("Type {USERNAME}"), this);
auto typePasswordAction = new QAction(icons()->icon("auto-type"), tr("Type {PASSWORD}"), this);
auto typeTotpAction = new QAction(icons()->icon("auto-type"), tr("Type {TOTP}"), this);
auto typeUrlAction = new QAction(icons()->icon("auto-type"), tr("Type {URL}"), this);
auto copyUsernameAction = new QAction(icons()->icon("username-copy"), tr("Copy Username"), this);
auto copyPasswordAction = new QAction(icons()->icon("password-copy"), tr("Copy Password"), this);
auto copyTotpAction = new QAction(icons()->icon("totp"), tr("Copy TOTP"), this);
auto copyUrlAction = new QAction(icons()->icon("url-copy"), tr("Copy URL"), this);
m_actionMenu->addAction(typeUsernameAction);
m_actionMenu->addAction(typePasswordAction);
m_actionMenu->addAction(typeTotpAction);
m_actionMenu->addAction(typeUrlAction);
m_actionMenu->addAction(copyUsernameAction);
m_actionMenu->addAction(copyPasswordAction);
m_actionMenu->addAction(copyTotpAction);
m_actionMenu->addAction(copyUrlAction);
typeUsernameAction->setShortcut(Qt::CTRL + Qt::Key_1);
typeUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
@@ -333,18 +324,10 @@ void AutoTypeSelectDialog::buildActionMenu()
submitAutoTypeMatch(match);
});
typeUrlAction->setShortcut(Qt::CTRL + Qt::Key_4);
typeUrlAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::URL);
connect(typeUrlAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{URL}";
submitAutoTypeMatch(match);
});
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
auto typeVirtualAction = new QAction(icons()->icon("auto-type"), tr("Use Virtual Keyboard"), nullptr);
m_actionMenu->insertAction(copyUsernameAction, typeVirtualAction);
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_5);
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_4);
connect(typeVirtualAction, &QAction::triggered, this, [&] {
m_virtualMode = true;
activateCurrentMatch();
@@ -381,29 +364,17 @@ void AutoTypeSelectDialog::buildActionMenu()
}
});
copyUrlAction->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_4);
copyUrlAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::URL);
connect(copyUrlAction, &QAction::triggered, this, [&] {
auto entry = m_ui->view->currentMatch().first;
if (entry) {
clipboard()->setText(entry->resolvePlaceholder(entry->url()));
reject();
}
});
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
// Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
typeUsernameAction->setShortcutVisibleInContextMenu(true);
typePasswordAction->setShortcutVisibleInContextMenu(true);
typeTotpAction->setShortcutVisibleInContextMenu(true);
typeUrlAction->setShortcutVisibleInContextMenu(true);
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
typeVirtualAction->setShortcutVisibleInContextMenu(true);
#endif
copyUsernameAction->setShortcutVisibleInContextMenu(true);
copyPasswordAction->setShortcutVisibleInContextMenu(true);
copyTotpAction->setShortcutVisibleInContextMenu(true);
copyUrlAction->setShortcutVisibleInContextMenu(true);
}
void AutoTypeSelectDialog::showEvent(QShowEvent* event)

View File

@@ -51,18 +51,12 @@
<enum>Qt::NoFocus</enum>
</property>
<property name="toolTip">
<string>&lt;p&gt;The following shortcuts are available:&lt;br/&gt;
Ctrl+F - Focus search&lt;br/&gt;
<string>&lt;p&gt;You can use advanced search queries to find any entry in your open databases. The following shortcuts are useful:&lt;br/&gt;
Ctrl+F - Toggle database search&lt;br/&gt;
Ctrl+1 - Type username&lt;br/&gt;
Ctrl+2 - Type password&lt;br/&gt;
Ctrl+3 - Type TOTP&lt;br/&gt;
Ctrl+4 - Type URL&lt;br/&gt;
Ctrl+5 - Use Virtual Keyboard (Windows Only)&lt;br/&gt;
Ctrl+Shift+1 - Copy username&lt;br/&gt;
Ctrl+Shift+2 - Copy password&lt;br/&gt;
Ctrl+Shift+3 - Copy TOTP&lt;br/&gt;
Ctrl+Shift+4 - Copy URL&lt;br/&gt;
&lt;/p&gt;</string>
Ctrl+4 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</string>
</property>
<property name="styleSheet">
<string notr="true">QToolButton {
@@ -178,9 +172,6 @@ Ctrl+Shift+4 - Copy URL&lt;br/&gt;
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>You can use advanced search queries to find any entry in your open databases.</string>
</property>
<property name="placeholderText">
<string>Search…</string>
</property>

View File

@@ -150,13 +150,14 @@ void BrowserAccessControlDialog::selectionChanged()
bool BrowserAccessControlDialog::areAllDisabled() const
{
auto areAllDisabled = true;
for (const auto& item : getAllItems()) {
if (item->flags() != Qt::NoItemFlags) {
return false;
areAllDisabled = false;
}
}
return true;
return areAllDisabled;
}
QList<QTableWidgetItem*> BrowserAccessControlDialog::getAllItems() const

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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
@@ -71,7 +71,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
}
const auto authenticatorAttachment = credentialCreationOptions["authenticatorAttachment"];
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toString();
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toObject();
const auto extensions = credentialCreationOptions["extensions"].toString();
const auto credentialId = testingVariables.credentialId.isEmpty()
? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
@@ -98,7 +98,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
// Response
QJsonObject responseObject;
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataJson.toUtf8());
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
responseObject["clientExtensionResults"] = credentialCreationOptions["clientExtensionResults"];
// Additions for extension side functions
@@ -130,8 +130,8 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& asse
const auto authenticatorData =
buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString());
const auto clientDataJson = assertionOptions["clientDataJson"].toString();
const auto clientDataArray = clientDataJson.toUtf8();
const auto clientDataJson = assertionOptions["clientDataJson"].toObject();
const auto clientDataArray = QJsonDocument(clientDataJson).toJson(QJsonDocument::Compact);
const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
if (signature.isEmpty()) {
@@ -140,7 +140,7 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& asse
QJsonObject responseObject;
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
responseObject["clientExtensionResults"] = assertionOptions["clientExtensionResults"];
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
responseObject["userHandle"] = userHandle;

View File

@@ -155,4 +155,4 @@ void BrowserPasskeysConfirmationDialog::updateEntriesToTable(const QList<Entry*>
m_ui->credentialsTable->resizeColumnsToContents();
m_ui->credentialsTable->horizontalHeader()->setStretchLastSection(true);
}
}

View File

@@ -330,7 +330,6 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName, bool isPass
}
#endif
name = newGroup->name();
newGroup->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Disable);
uuid = Tools::uuidToHex(newGroup->uuid());
previousGroup = newGroup;
continue;
@@ -413,7 +412,7 @@ BrowserService::findEntries(const EntryParameters& entryParameters, const String
continue;
case Unknown:
if (alwaysAllowAccess || (entryParameters.httpAuth && ignoreHttpAuth)) {
if (alwaysAllowAccess) {
allowedEntries.append(entry);
} else {
entriesToConfirm.append(entry);
@@ -898,6 +897,16 @@ void BrowserService::addEntry(const EntryParameters& entryParameters,
const QString host = QUrl(entryParameters.siteUrl).host();
const QString submitHost = QUrl(entryParameters.formUrl).host();
BrowserEntryConfig config;
config.allow(host);
if (!submitHost.isEmpty()) {
config.allow(submitHost);
}
if (!entryParameters.realm.isEmpty()) {
config.setRealm(entryParameters.realm);
}
config.save(entry);
if (downloadFavicon && m_currentDatabaseWidget) {
m_currentDatabaseWidget->downloadFaviconInBackground(entry);
@@ -1182,7 +1191,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
if (entry->hasValidTotp()) {
if (entry->hasTotp()) {
res["totp"] = entry->totp();
}
@@ -1562,11 +1571,11 @@ bool BrowserService::handleURLWithWildcards(const QUrl& entryQUrl, const QString
}
// Escape illegal characters
auto re = Tools::escapeRegex(firstPart);
auto re = firstPart.replace(QRegularExpression(R"(([!\^\$\+\-\(\)@<>]))"), "\\\\1");
if (hostnameUsed) {
// Replace all host parts with wildcards
re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)\\.");
re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)");
}
// Append a + to the end of regex to match all paths after the last asterisk

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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
@@ -53,8 +53,8 @@ bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialC
{
if (!credentialCreationOptions["attestation"].isString()
|| credentialCreationOptions["attestation"].toString().isEmpty()
|| !credentialCreationOptions["clientDataJSON"].isString()
|| credentialCreationOptions["clientDataJSON"].toString().isEmpty()
|| !credentialCreationOptions["clientDataJSON"].isObject()
|| credentialCreationOptions["clientDataJSON"].toObject().isEmpty()
|| !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty()
|| !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty()
|| !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined()
@@ -75,7 +75,7 @@ bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialC
// Basic check for the object that it contains necessary variables in a correct form
bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const
{
if (!assertionOptions["clientDataJson"].isString() || assertionOptions["clientDataJson"].toString().isEmpty()
if (!assertionOptions["clientDataJson"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty()
|| !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty()
|| !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined()
|| !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) {
@@ -352,11 +352,15 @@ ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) c
return {};
}
// Serialization order: https://w3c.github.io/webauthn/#clientdatajson-serialization
QString PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
{
return QString("{\"type\":\"%1\",\"challenge\":\"%2\",\"origin\":\"%3\",\"crossOrigin\":false}")
.arg((get ? QString("webauthn.get") : QString("webauthn.create")), publicKey["challenge"].toString(), origin);
QJsonObject clientData;
clientData["challenge"] = publicKey["challenge"];
clientData["crossOrigin"] = false;
clientData["origin"] = origin;
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
return clientData;
}
QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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
@@ -58,7 +58,7 @@ public:
bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const;
bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const;
ExtensionResult buildExtensionData(QJsonObject& extensionObject) const;
QString buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const;
QString getCredentialIdFromEntry(const Entry* entry) const;
QString getUsernameFromEntry(const Entry* entry) const;

View File

@@ -37,6 +37,12 @@ const QCommandLineOption Clip::TotpOption =
QCommandLineOption(QStringList() << "t" << "totp",
QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\")."));
const QCommandLineOption Clip::UuidOption =
QCommandLineOption(QStringList() << "uuid", QObject::tr("Copy the entry's UUID to the clipboard."));
const QCommandLineOption Clip::TagsOption =
QCommandLineOption(QStringList() << "tags", QObject::tr("Copy the entry's tag list to the clipboard."));
const QCommandLineOption Clip::BestMatchOption =
QCommandLineOption(QStringList() << "b" << "best-match",
QObject::tr("Must match only one entry, otherwise a list of possible matches is shown."));
@@ -47,6 +53,8 @@ Clip::Clip()
description = QObject::tr("Copy an entry's attribute to the clipboard.");
options.append(Clip::AttributeOption);
options.append(Clip::TotpOption);
options.append(Clip::UuidOption);
options.append(Clip::TagsOption);
options.append(Clip::BestMatchOption);
positionalArguments.append(
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
@@ -99,8 +107,13 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return EXIT_FAILURE;
}
if (parser->isSet(AttributeOption) && parser->isSet(TotpOption)) {
err << QObject::tr("ERROR: Please specify one of --attribute or --totp, not both.") << Qt::endl;
auto optionCount = parser->isSet(AttributeOption) ? 1 : 0;
optionCount += parser->isSet(TotpOption) ? 1 : 0;
optionCount += parser->isSet(UuidOption) ? 1 : 0;
optionCount += parser->isSet(TagsOption) ? 1 : 0;
if (optionCount > 1) {
err << QObject::tr("ERROR: Cannot specify multiple options at once (--attribute, --totp, --uuid, --tags).")
<< Qt::endl;
return EXIT_FAILURE;
}
@@ -113,11 +126,16 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return EXIT_FAILURE;
}
selectedAttribute = "totp";
found = true;
value = entry->totp();
} else if (Utils::EntryFieldNames.contains(selectedAttribute)) {
value = Utils::getTopLevelField(entry, selectedAttribute);
selectedAttribute = "TOTP";
found = true;
} else if (parser->isSet(UuidOption)) {
value = entry->uuid().toString();
selectedAttribute = "UUID";
found = true;
} else if (parser->isSet(TagsOption)) {
value = entry->tags();
selectedAttribute = "Tags";
found = true;
} else {
QStringList attrs = Utils::findAttributes(*entry->attributes(), selectedAttribute);

View File

@@ -29,6 +29,8 @@ public:
static const QCommandLineOption AttributeOption;
static const QCommandLineOption TotpOption;
static const QCommandLineOption UuidOption;
static const QCommandLineOption TagsOption;
static const QCommandLineOption BestMatchOption;
};

View File

@@ -67,8 +67,8 @@ int DatabaseInfo::executeWithDatabase(QSharedPointer<Database> database, QShared
out << QObject::tr("Number of short passwords") << ": " << QString::number(stats.shortPasswords) << Qt::endl;
out << QObject::tr("Number of weak passwords") << ": " << QString::number(stats.weakPasswords) << Qt::endl;
out << QObject::tr("Entries excluded from reports") << ": " << QString::number(stats.excludedEntries) << Qt::endl;
out << QObject::tr("Average password length") << ": "
<< QObject::tr("%1 character(s)", "", stats.averagePwdLength()).arg(stats.averagePwdLength()) << Qt::endl;
out << QObject::tr("Average password length") << ": " << QObject::tr("%1 characters").arg(stats.averagePwdLength())
<< Qt::endl;
return EXIT_SUCCESS;
}

View File

@@ -68,9 +68,11 @@ int Diceware::execute(const QStringList& arguments)
dicewareGenerator.setWordList(wordListFile);
}
// Show a warning if the wordlist is smaller than the recommended size
if (!dicewareGenerator.isWordListValid()) {
err << QObject::tr("Warning: the chosen wordlist is smaller than the minimum recommended size!") << Qt::endl;
if (!dicewareGenerator.isValid()) {
// We already validated the word count input so if the generator is invalid, it
// must be because the word list is too small.
err << QObject::tr("Cannot generate valid passphrases because the wordlist is too short") << Qt::endl;
return EXIT_FAILURE;
}
QString password = dicewareGenerator.generatePassphrase();

View File

@@ -87,10 +87,10 @@ int Merge::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer
}
Merger merger(db2.data(), database.data());
auto changeList = merger.merge();
QStringList changeList = merger.merge();
for (const auto& mergeChange : changeList) {
out << "\t" << mergeChange.toString() << Qt::endl;
for (auto& mergeChange : changeList) {
out << "\t" << mergeChange << Qt::endl;
}
if (!changeList.isEmpty() && !parser->isSet(Merge::DryRunOption)) {

View File

@@ -24,14 +24,21 @@
#include <QCommandLineParser>
const QCommandLineOption Show::TotpOption =
QCommandLineOption(QStringList() << "t" << "totp", QObject::tr("Show the entry's current TOTP."));
QCommandLineOption(QStringList() << "t" << "totp", QObject::tr("Only show the entry's current TOTP."));
const QCommandLineOption Show::UuidOption =
QCommandLineOption(QStringList() << "uuid", QObject::tr("Show the entry's UUID."));
const QCommandLineOption Show::TagsOption =
QCommandLineOption(QStringList() << "tags", QObject::tr("Show the entry's tags."));
const QCommandLineOption Show::ProtectedAttributesOption =
QCommandLineOption(QStringList() << "s" << "show-protected",
QObject::tr("Show the protected attributes in clear text."));
const QCommandLineOption Show::AllAttributesOption =
QCommandLineOption(QStringList() << "all", QObject::tr("Show all the attributes of the entry."));
QCommandLineOption(QStringList() << "all",
QObject::tr("Show all the attributes of the entry, including UUID and Tags."));
const QCommandLineOption Show::AttachmentsOption =
QCommandLineOption(QStringList() << "show-attachments", QObject::tr("Show the attachments of the entry."));
@@ -49,6 +56,8 @@ Show::Show()
name = QString("show");
description = QObject::tr("Show an entry's information.");
options.append(Show::TotpOption);
options.append(Show::UuidOption);
options.append(Show::TagsOption);
options.append(Show::AttributesOption);
options.append(Show::ProtectedAttributesOption);
options.append(Show::AllAttributesOption);
@@ -63,9 +72,10 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
const QStringList args = parser->positionalArguments();
const QString& entryPath = args.at(1);
bool showTotp = parser->isSet(Show::TotpOption);
bool showProtectedAttributes = parser->isSet(Show::ProtectedAttributesOption);
bool showAllAttributes = parser->isSet(Show::AllAttributesOption);
bool showUuid = parser->isSet(Show::UuidOption);
bool showTags = parser->isSet(Show::TagsOption);
QStringList attributes = parser->values(Show::AttributesOption);
Entry* entry = database->rootGroup()->findEntryByPath(entryPath);
@@ -74,18 +84,23 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return EXIT_FAILURE;
}
if (showTotp && !entry->hasTotp()) {
err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << Qt::endl;
return EXIT_FAILURE;
// Early exit if the user only wants to show the TOTP
if (parser->isSet(Show::TotpOption)) {
if (!entry->hasTotp()) {
err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << Qt::endl;
return EXIT_FAILURE;
}
out << entry->totp() << Qt::endl;
return EXIT_SUCCESS;
}
bool attributesWereSpecified = true;
bool attributesWereSpecified = !showUuid && !showTags;
if (showAllAttributes) {
attributesWereSpecified = false;
showUuid = true;
showTags = true;
attributes = EntryAttributes::DefaultAttributes;
for (QString fieldName : Utils::EntryFieldNames) {
attributes.append(fieldName);
}
// Adding the custom attributes after the default attributes so that
// the default attributes are always shown first.
for (QString attributeName : entry->attributes()->keys()) {
@@ -94,26 +109,16 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
}
attributes.append(attributeName);
}
} else if (attributes.isEmpty() && !showTotp) {
} else if (attributes.isEmpty() && !showUuid && !showTags) {
// If no attributes are specified, output the default attribute set.
attributesWereSpecified = false;
attributes = EntryAttributes::DefaultAttributes;
for (QString fieldName : Utils::EntryFieldNames) {
attributes.append(fieldName);
}
showTags = true;
}
// Iterate over the attributes and output them line-by-line.
bool encounteredError = false;
for (const QString& attributeName : asConst(attributes)) {
if (Utils::EntryFieldNames.contains(attributeName)) {
if (!attributesWereSpecified) {
out << attributeName << ": ";
}
out << Utils::getTopLevelField(entry, attributeName) << Qt::endl;
continue;
}
QStringList attrs = Utils::findAttributes(*entry->attributes(), attributeName);
if (attrs.isEmpty()) {
encounteredError = true;
@@ -137,6 +142,14 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
}
}
// Output UUID and Tags if a certain field wasn't specified
if (showTags) {
out << "Tags: " << entry->tags() << Qt::endl;
}
if (showUuid) {
out << "UUID: " << entry->uuid().toString() << Qt::endl;
}
if (parser->isSet(Show::AttachmentsOption)) {
// Separate attachment output from attributes output via a newline.
out << Qt::endl;
@@ -156,9 +169,5 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
}
}
if (showTotp) {
out << entry->totp() << Qt::endl;
}
return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS;
}

View File

@@ -28,6 +28,8 @@ public:
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
static const QCommandLineOption TotpOption;
static const QCommandLineOption UuidOption;
static const QCommandLineOption TagsOption;
static const QCommandLineOption AllAttributesOption;
static const QCommandLineOption AttributesOption;
static const QCommandLineOption ProtectedAttributesOption;

View File

@@ -395,17 +395,6 @@ namespace Utils
return result;
}
QString getTopLevelField(const Entry* entry, const QString& fieldName)
{
if (fieldName == UuidFieldName) {
return entry->uuid().toString();
}
if (fieldName == TagsFieldName) {
return entry->tags();
}
return "";
}
QStringList findAttributes(const EntryAttributes& attributes, const QString& name)
{
QStringList result;

View File

@@ -34,10 +34,6 @@ namespace Utils
extern QTextStream STDIN;
extern QTextStream DEVNULL;
static const QString UuidFieldName = "Uuid";
static const QString TagsFieldName = "Tags";
static const QStringList EntryFieldNames(QStringList() << UuidFieldName << TagsFieldName);
void setDefaultTextStreams();
void resetTextStreams();
@@ -61,10 +57,6 @@ namespace Utils
* (case-insensitive).
*/
QStringList findAttributes(const EntryAttributes& attributes, const QString& name);
/**
* Get the value of a top-level Entry field using its name.
*/
QString getTopLevelField(const Entry* entry, const QString& fieldName);
}; // namespace Utils
#endif // KEEPASSXC_UTILS_H

View File

@@ -147,7 +147,6 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_HidePasswordPreviewPanel, {QS("Security/HidePasswordPreviewPanel"), Roaming, true}},
{Config::Security_HideTotpPreviewPanel, {QS("Security/HideTotpPreviewPanel"), Roaming, false}},
{Config::Security_AutoTypeAsk, {QS("Security/AutotypeAsk"), Roaming, true}},
{Config::Security_AutoTypeSkipMainWindowConfirmation, {QS("Security/AutoTypeSkipMainWindowConfirmation"), Roaming, false}},
{Config::Security_IconDownloadFallback, {QS("Security/IconDownloadFallback"), Roaming, false}},
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},

View File

@@ -98,7 +98,6 @@ public:
GUI_CompactMode,
GUI_CheckForUpdates,
GUI_CheckForUpdatesIncludeBetas,
SearchWaitForEnter,
GUI_ShowExpiredEntriesOnDatabaseUnlock,
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
GUI_FontSizeOffset,
@@ -129,7 +128,6 @@ public:
Security_HidePasswordPreviewPanel,
Security_HideTotpPreviewPanel,
Security_AutoTypeAsk,
Security_AutoTypeSkipMainWindowConfirmation,
Security_IconDownloadFallback,
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,

View File

@@ -826,12 +826,7 @@ void Database::updateTagList()
}
m_tagList = tagSet.values();
QCollator collator;
collator.setNumericMode(true);
collator.setCaseSensitivity(Qt::CaseInsensitive);
std::sort(m_tagList.begin(), m_tagList.end(), collator);
m_tagList.sort();
emit tagListUpdated();
}

View File

@@ -570,12 +570,6 @@ bool Entry::hasTotp() const
return !m_data.totpSettings.isNull();
}
bool Entry::hasValidTotp() const
{
auto error = Totp::checkValidSettings(m_data.totpSettings);
return error.isEmpty();
}
bool Entry::hasPasskey() const
{
return m_attributes->hasPasskey();
@@ -587,13 +581,10 @@ void Entry::removePasskey()
removeTag(tr("Passkey"));
}
QString Entry::totp(bool* isValid) const
QString Entry::totp() const
{
if (hasTotp()) {
return Totp::generateTotp(m_data.totpSettings, isValid);
}
if (isValid) {
*isValid = false;
return Totp::generateTotp(m_data.totpSettings);
}
return {};
}
@@ -959,68 +950,6 @@ bool Entry::equals(const Entry* other, CompareItemOptions options) const
return true;
}
QStringList Entry::calculateDifference(const Entry* other)
{
QStringList modifiedFields;
if (*attributes() != *other->attributes()) {
bool foundAttribute = false;
if (title() != other->title()) {
modifiedFields << tr("Title");
foundAttribute = true;
}
if (username() != other->username()) {
modifiedFields << tr("Username");
foundAttribute = true;
}
if (password() != other->password()) {
modifiedFields << tr("Password");
foundAttribute = true;
}
if (url() != other->url()) {
modifiedFields << tr("URL");
foundAttribute = true;
}
if (notes() != other->notes()) {
modifiedFields << tr("Notes");
foundAttribute = true;
}
if (!foundAttribute) {
modifiedFields << tr("Custom Attributes");
}
}
if (iconNumber() != other->iconNumber() || iconUuid() != other->iconUuid()) {
modifiedFields << tr("Icon");
}
if (foregroundColor() != other->foregroundColor() || backgroundColor() != other->backgroundColor()) {
modifiedFields << tr("Color");
}
if (timeInfo().expires() != other->timeInfo().expires()
|| timeInfo().expiryTime() != other->timeInfo().expiryTime()) {
modifiedFields << tr("Expiration");
}
if (totp() != other->totp()) {
modifiedFields << tr("TOTP");
}
if (*customData() != *other->customData()) {
modifiedFields << tr("Custom Data");
}
if (*attachments() != *other->attachments()) {
modifiedFields << tr("Attachments");
}
if (*autoTypeAssociations() != *other->autoTypeAssociations() || autoTypeEnabled() != other->autoTypeEnabled()
|| defaultAutoTypeSequence() != other->defaultAutoTypeSequence()) {
modifiedFields << tr("Auto-Type");
}
if (tags() != other->tags()) {
modifiedFields << tr("Tags");
}
return modifiedFields;
}
Entry* Entry::clone(CloneFlags flags) const
{
auto entry = new Entry();

View File

@@ -109,7 +109,7 @@ public:
QString password() const;
QString notes() const;
QString attribute(const QString& key) const;
QString totp(bool* isValid = nullptr) const;
QString totp() const;
QString totpSettingsString() const;
QSharedPointer<Totp::Settings> totpSettings() const;
Group* previousParentGroup();
@@ -126,7 +126,6 @@ public:
void removePasskey();
bool hasTotp() const;
bool hasValidTotp() const;
bool isExpired() const;
bool willExpireInDays(int days) const;
void expireNow();
@@ -179,13 +178,6 @@ public:
bool equals(const Entry* other, CompareItemOptions options = CompareItemDefault) const;
/**
* Determine differences between attributes of this and another entry.
*
* @return The list of attribute names that are different between the two entries
*/
QStringList calculateDifference(const Entry* other);
enum CloneFlag
{
CloneNoFlags = 0,

View File

@@ -221,13 +221,6 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
}
found = false;
break;
case Field::Has:
if (term.word.compare("totp", Qt::CaseInsensitive) == 0) {
found = entry->hasTotp();
break;
}
found = false;
break;
case Field::Uuid:
found = term.regex.match(entry->uuidToHex()).hasMatch();
break;
@@ -267,7 +260,6 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
{QStringLiteral("group"), Field::Group},
{QStringLiteral("tag"), Field::Tag},
{QStringLiteral("is"), Field::Is},
{QStringLiteral("has"), Field::Has},
{QStringLiteral("uuid"), Field::Uuid}};
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string

View File

@@ -41,7 +41,6 @@ public:
Group,
Tag,
Is,
Has,
Uuid
};

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
*
* 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
@@ -457,7 +457,6 @@ const Group* Group::parentGroup() const
void Group::setParent(Group* parent, int index, bool trackPrevious)
{
Q_ASSERT(parent);
Q_ASSERT(this != parent);
Q_ASSERT(index >= -1 && index <= parent->children().size());
// setting a new parent for root groups is not allowed
Q_ASSERT(!m_db || (m_db->rootGroup() != this));
@@ -1143,24 +1142,6 @@ bool Group::resolveAutoTypeEnabled() const
}
}
bool Group::resolveBrowserOptionEnabled(const QString& option) const
{
switch (resolveCustomDataTriState(option, true)) {
case Inherit:
if (!m_parent) {
return false;
}
return m_parent->resolveBrowserOptionEnabled(option);
case Enable:
return true;
case Disable:
return false;
default:
Q_ASSERT(false);
return false;
}
}
Entry* Group::addEntryWithPath(const QString& entryPath)
{
if (entryPath.isEmpty() || findEntryByPath(entryPath)) {

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
*
* 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
@@ -94,7 +94,6 @@ public:
Group::MergeMode mergeMode() const;
bool resolveSearchingEnabled() const;
bool resolveAutoTypeEnabled() const;
bool resolveBrowserOptionEnabled(const QString& option) const;
Entry* lastTopVisibleEntry() const;
bool isExpired() const;
bool isRecycled() const;

View File

@@ -20,28 +20,28 @@
#include <QCoreApplication>
#include <QTimer>
namespace
{
// Minimum timeout is 10 seconds
constexpr int MIN_TIMEOUT = 10000;
} // namespace
InactivityTimer::InactivityTimer(QObject* parent)
: QObject(parent)
, m_timer(new QTimer(this))
, m_active(false)
{
m_timer->setSingleShot(false);
m_timer->setSingleShot(true);
connect(m_timer, SIGNAL(timeout()), SLOT(timeout()));
}
void InactivityTimer::activate(int inactivityTimeout)
void InactivityTimer::setInactivityTimeout(int inactivityTimeout)
{
Q_ASSERT(inactivityTimeout > 0);
m_timer->setInterval(inactivityTimeout);
}
void InactivityTimer::activate()
{
if (!m_active) {
qApp->installEventFilter(this);
}
m_active = true;
m_resetBlocked = false;
m_timer->setInterval(qMax(MIN_TIMEOUT, inactivityTimeout));
m_timer->start();
}
@@ -54,15 +54,12 @@ void InactivityTimer::deactivate()
bool InactivityTimer::eventFilter(QObject* watched, QEvent* event)
{
const auto type = event->type();
const QEvent::Type type = event->type();
// clang-format off
if (!m_resetBlocked &&
((type >= QEvent::MouseButtonPress && type <= QEvent::KeyRelease) ||
(type >= QEvent::HoverEnter && type <= QEvent::HoverMove) ||
type == QEvent::Wheel)) {
if ((type >= QEvent::MouseButtonPress && type <= QEvent::KeyRelease)
|| (type >= QEvent::HoverEnter && type <= QEvent::HoverMove)
|| (type == QEvent::Wheel)) {
m_timer->start();
m_resetBlocked = true;
QTimer::singleShot(500, this, [this]() { m_resetBlocked = false; });
}
// clang-format on
@@ -76,7 +73,7 @@ void InactivityTimer::timeout()
return;
}
if (m_active) {
if (m_active && !m_timer->isActive()) {
emit inactivityDetected();
}

View File

@@ -29,7 +29,8 @@ class InactivityTimer : public QObject
public:
explicit InactivityTimer(QObject* parent = nullptr);
void activate(int inactivityTimeout);
void setInactivityTimeout(int inactivityTimeout);
void activate();
void deactivate();
signals:
@@ -43,8 +44,7 @@ private slots:
private:
QTimer* m_timer;
bool m_active = false;
bool m_resetBlocked = false;
bool m_active;
QMutex m_emitMutx;
};

View File

@@ -21,107 +21,6 @@
#include "core/Metadata.h"
#include "core/Tools.h"
Merger::Change::Change(Type type, QString details)
: m_type{type}
, m_details{std::move(details)}
{
}
Merger::Change::Change(Type type, const Group& group, QString details)
: m_type{type}
, m_group{group.fullPath()}
, m_uuid{group.uuid()}
, m_details{std::move(details)}
{
}
Merger::Change::Change(Type type, const Entry& entry, QString details)
: m_type{type}
, m_title{entry.title()}
, m_uuid{entry.uuid()}
, m_details{std::move(details)}
{
if (const auto* group = entry.group()) {
m_group = group->fullPath();
}
}
Merger::Change::Change(QString details)
: m_details{std::move(details)}
{
}
bool Merger::Change::operator==(const Merger::Change& other) const
{
return m_type == other.m_type && m_group == other.m_group && m_title == other.m_title && m_uuid == other.m_uuid
&& m_details == other.m_details;
}
bool Merger::Change::operator!=(const Merger::Change& other) const
{
return !(*this == other);
}
Merger::Change::Type Merger::Change::type() const
{
return m_type;
}
const QString& Merger::Change::title() const
{
return m_title;
}
const QString& Merger::Change::group() const
{
return m_group;
}
const QUuid& Merger::Change::uuid() const
{
return m_uuid;
}
const QString& Merger::Change::details() const
{
return m_details;
}
QString Merger::Change::typeString() const
{
switch (m_type) {
case Type::Added:
return tr("Added");
case Type::Modified:
return tr("Modified");
case Type::Moved:
return tr("Moved");
case Type::Deleted:
return tr("Deleted");
case Type::Metadata:
return "Metadata";
case Type::Unspecified:
return "";
default:
return "?";
}
}
QString Merger::Change::toString() const
{
QString result;
if (m_type != Type::Unspecified) {
result += QString("%1: ").arg(typeString());
}
if (!m_group.isEmpty()) {
result += QString("'%1'").arg(m_group);
}
if (!m_title.isEmpty()) {
result += QString("/'%1'").arg(m_title);
}
if (!m_uuid.isNull()) {
result += QString(" [%1]").arg(m_uuid.toString());
}
if (!m_details.isEmpty()) {
result += QString(" (%1)").arg(m_details);
}
return result;
}
Merger::Merger(const Database* sourceDb, Database* targetDb)
: m_mode(Group::Default)
{
@@ -165,9 +64,8 @@ void Merger::setSkipDatabaseCustomData(bool state)
m_skipCustomData = state;
}
Merger::ChangeList Merger::merge(bool dryRun)
QStringList Merger::merge()
{
m_dryRun = dryRun;
// Order of merge steps is important - it is possible that we
// create some items before deleting them afterwards
ChangeList changes;
@@ -176,10 +74,9 @@ Merger::ChangeList Merger::merge(bool dryRun)
changes << mergeMetadata(m_context);
// At this point we have a list of changes we may want to show the user
if (!changes.isEmpty() && !dryRun) {
if (!changes.isEmpty()) {
m_context.m_targetDb->markAsModified();
}
m_dryRun = false;
return changes;
}
@@ -191,58 +88,42 @@ Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
for (Entry* sourceEntry : sourceEntries) {
Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid());
if (!targetEntry) {
changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
// This entry does not exist at all. Create it.
changes << Change(Change::Type::Added, *sourceEntry);
if (!m_dryRun) {
targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
moveEntry(targetEntry, context.m_targetGroup);
}
targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
moveEntry(targetEntry, context.m_targetGroup);
} else {
// Entry is already present in the database. Update it.
const bool locationChanged =
targetEntry->timeInfo().locationChanged() < sourceEntry->timeInfo().locationChanged();
if (locationChanged && targetEntry->group() != context.m_targetGroup) {
changes << Change(
Change::Type::Moved, *sourceEntry, tr("Previous location: %1").arg(targetEntry->group()->name()));
if (!m_dryRun) {
moveEntry(targetEntry, context.m_targetGroup);
}
changes << tr("Relocating %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
moveEntry(targetEntry, context.m_targetGroup);
}
changes << resolveEntryConflict(context, sourceEntry, targetEntry);
}
}
// merge child groups recursively
// merge groups recursively
const QList<Group*> sourceChildGroups = context.m_sourceGroup->children();
for (Group* sourceChildGroup : sourceChildGroups) {
bool groupCreated = false;
Group* targetChildGroup = context.m_targetRootGroup->findGroupByUuid(sourceChildGroup->uuid());
if (!targetChildGroup) {
changes << Change(
Change::Type::Added,
*sourceChildGroup,
tr("Number of entries in group: %1").arg(QString::number(sourceChildGroup->entries().size())));
// Create the target group, it will be cleaned up later if in dry run mode
changes << tr("Creating missing %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
targetChildGroup = sourceChildGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
groupCreated = true;
if (!m_dryRun) {
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
}
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
} else {
bool locationChanged =
targetChildGroup->timeInfo().locationChanged() < sourceChildGroup->timeInfo().locationChanged();
if (locationChanged && targetChildGroup->parent() != context.m_targetGroup) {
changes << Change(
Change::Type::Moved, *sourceChildGroup, tr("Previous location: %1").arg(targetChildGroup->name()));
if (!m_dryRun) {
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
}
changes << tr("Relocating %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
}
changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup);
}
@@ -253,10 +134,6 @@ Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
sourceChildGroup,
targetChildGroup};
changes << mergeGroup(subcontext);
// Cleanup the temporary target group structure
if (m_dryRun && groupCreated) {
delete subcontext.m_targetGroup;
}
}
return changes;
}
@@ -272,68 +149,24 @@ Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChi
// only if the other group is newer, update the existing one.
if (timeExisting < timeOther) {
QStringList modifications;
auto updateIfNecessary = [&modifications, this](const auto& targetValue,
const auto& sourceValue,
auto&& updateFunction,
const QString& modification) {
if (targetValue != sourceValue) {
modifications << modification;
if (!m_dryRun) {
updateFunction(sourceValue);
}
return true;
}
return false;
};
updateIfNecessary(
targetChildGroup->name(),
sourceChildGroup->name(),
[&](auto&& newValue) { targetChildGroup->setName(newValue); },
tr("Group name"));
updateIfNecessary(
targetChildGroup->notes(),
sourceChildGroup->notes(),
[&](auto&& newValue) { targetChildGroup->setNotes(newValue); },
tr("Notes"));
changes << tr("Overwriting %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
targetChildGroup->setName(sourceChildGroup->name());
targetChildGroup->setNotes(sourceChildGroup->notes());
if (sourceChildGroup->iconNumber() == 0) {
updateIfNecessary(
targetChildGroup->iconUuid(),
sourceChildGroup->iconUuid(),
[&](auto&& newValue) { targetChildGroup->setIcon(newValue); },
tr("Icon (UUID)"));
targetChildGroup->setIcon(sourceChildGroup->iconUuid());
} else {
updateIfNecessary(
targetChildGroup->iconNumber(),
sourceChildGroup->iconNumber(),
[&](auto&& newValue) { targetChildGroup->setIcon(newValue); },
tr("Icon (Number)"));
targetChildGroup->setIcon(sourceChildGroup->iconNumber());
}
updateIfNecessary(
targetChildGroup->timeInfo().expiryTime(),
sourceChildGroup->timeInfo().expiryTime(),
[&](auto&& newValue) { targetChildGroup->setExpiryTime(newValue); },
tr("Expiry time"));
updateIfNecessary(
timeExisting,
timeOther,
[&](auto&& newValue) {
TimeInfo timeInfo = targetChildGroup->timeInfo();
timeInfo.setLastModificationTime(newValue);
targetChildGroup->setTimeInfo(timeInfo);
},
tr("Modification time"));
changes << Change(Change::Type::Modified, *sourceChildGroup, modifications.join(", "));
targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime());
TimeInfo timeInfo = targetChildGroup->timeInfo();
timeInfo.setLastModificationTime(timeOther);
targetChildGroup->setTimeInfo(timeInfo);
}
return changes;
}
void Merger::moveEntry(Entry* entry, Group* targetGroup)
{
if (m_dryRun) {
return;
}
Q_ASSERT(entry);
Group* sourceGroup = entry->group();
if (sourceGroup == targetGroup) {
@@ -363,10 +196,6 @@ void Merger::moveEntry(Entry* entry, Group* targetGroup)
void Merger::moveGroup(Group* group, Group* targetGroup)
{
if (m_dryRun) {
return;
}
Q_ASSERT(group);
Group* sourceGroup = group->parentGroup();
if (sourceGroup == targetGroup) {
@@ -396,10 +225,6 @@ void Merger::moveGroup(Group* group, Group* targetGroup)
void Merger::eraseEntry(Entry* entry)
{
if (m_dryRun) {
return;
}
Database* database = entry->database();
// most simple method to remove an item from DeletedObjects :(
const QList<DeletedObject> deletions = database->deletedObjects();
@@ -417,10 +242,6 @@ void Merger::eraseEntry(Entry* entry)
void Merger::eraseGroup(Group* group)
{
if (m_dryRun) {
return;
}
Database* database = group->database();
// most simple method to remove an item from DeletedObjects :(
const QList<DeletedObject> deletions = database->deletedObjects();
@@ -447,8 +268,6 @@ Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContex
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(),
sourceEntry->timeInfo().lastModificationTime(),
CompareItemIgnoreMilliseconds);
auto differences = targetEntry->calculateDifference(sourceEntry);
differences += "History";
const int maxItems = targetEntry->database()->metadata()->historyMaxItems();
if (comparison < 0) {
Group* currentGroup = targetEntry->group();
@@ -457,9 +276,7 @@ Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContex
qPrintable(targetEntry->title()),
qPrintable(sourceEntry->title()),
qPrintable(currentGroup->name()));
changes << Change(Change::Type::Modified,
*targetEntry,
tr("%1 (Add local modifications to new entry)").arg(differences.join(", ")));
changes << tr("Synchronizing from newer source %1 [%2]").arg(targetEntry->title(), targetEntry->uuidToHex());
mergeHistory(targetEntry, clonedEntry, mergeMethod, maxItems);
eraseEntry(targetEntry);
moveEntry(clonedEntry, currentGroup);
@@ -470,9 +287,8 @@ Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContex
qPrintable(targetEntry->group()->name()));
const bool changed = mergeHistory(sourceEntry, targetEntry, mergeMethod, maxItems);
if (changed) {
changes << Change(Change::Type::Modified,
*targetEntry,
tr("%1 (Add new modifications to existing entry)").arg(differences.join(", ")));
changes
<< tr("Synchronizing from older source %1 [%2]").arg(targetEntry->title(), targetEntry->uuidToHex());
}
}
return changes;
@@ -484,10 +300,8 @@ Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEnt
// We need to cut off the milliseconds since the persistent format only supports times down to seconds
// so when we import data from a remote source, it may represent the (or even some msec newer) data
// which may be discarded due to higher runtime precision
Group::MergeMode mergeMode = m_mode;
if (mergeMode == Group::Default && context.m_targetGroup) {
mergeMode = context.m_targetGroup->mergeMode();
}
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
return resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
}
@@ -510,11 +324,11 @@ bool Merger::mergeHistory(const Entry* sourceEntry,
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
if (merged.contains(modificationTime)
&& !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution "
"may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
::qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution "
"may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
}
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
}
@@ -523,10 +337,11 @@ bool Merger::mergeHistory(const Entry* sourceEntry,
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
if (merged.contains(modificationTime)
&& !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
qWarning("History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
::qWarning(
"History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
}
if (preferRemote && merged.contains(modificationTime)) {
// forcefully apply the remote history item
@@ -542,9 +357,9 @@ bool Merger::mergeHistory(const Entry* sourceEntry,
if (targetModificationTime == sourceModificationTime
&& !targetEntry->equals(sourceEntry,
CompareItemIgnoreMilliseconds | CompareItemIgnoreHistory | CompareItemIgnoreLocation)) {
qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()));
::qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()));
}
if (targetModificationTime < sourceModificationTime) {
@@ -583,24 +398,22 @@ bool Merger::mergeHistory(const Entry* sourceEntry,
qDeleteAll(updatedHistoryItems);
return false;
}
if (!m_dryRun) {
// We need to prevent any modification to the database since every change should be tracked either
// in a clone history item or in the Entry itself
const TimeInfo timeInfo = targetEntry->timeInfo();
const bool blockedSignals = targetEntry->blockSignals(true);
bool updateTimeInfo = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->removeHistoryItems(targetHistoryItems);
for (Entry* historyItem : merged) {
Q_ASSERT(!historyItem->parent());
targetEntry->addHistoryItem(historyItem);
}
targetEntry->truncateHistory();
targetEntry->blockSignals(blockedSignals);
targetEntry->setUpdateTimeinfo(updateTimeInfo);
Q_ASSERT(timeInfo == targetEntry->timeInfo());
Q_UNUSED(timeInfo);
// We need to prevent any modification to the database since every change should be tracked either
// in a clone history item or in the Entry itself
const TimeInfo timeInfo = targetEntry->timeInfo();
const bool blockedSignals = targetEntry->blockSignals(true);
bool updateTimeInfo = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->removeHistoryItems(targetHistoryItems);
for (Entry* historyItem : merged) {
Q_ASSERT(!historyItem->parent());
targetEntry->addHistoryItem(historyItem);
}
targetEntry->truncateHistory();
targetEntry->blockSignals(blockedSignals);
targetEntry->setUpdateTimeinfo(updateTimeInfo);
Q_ASSERT(timeInfo == targetEntry->timeInfo());
Q_UNUSED(timeInfo);
return true;
}
@@ -624,6 +437,7 @@ Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
for (const auto& object : (targetDeletions + sourceDeletions)) {
if (!mergedDeletions.contains(object.uuid)) {
mergedDeletions[object.uuid] = object;
auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid);
if (entry) {
entries << entry;
@@ -651,18 +465,17 @@ Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
}
deletions << object;
if (entry->group()) {
changes << Change(Change::Type::Deleted, *entry, tr("Explicit deletion"));
changes << tr("Deleting child %1 [%2]").arg(entry->title(), entry->uuidToHex());
} else {
changes << Change(Change::Type::Deleted, *entry, tr("Implicit deletion (e.g. removal of parent group)"));
}
if (!m_dryRun) {
eraseEntry(entry);
changes << tr("Deleting orphan %1 [%2]").arg(entry->title(), entry->uuidToHex());
}
// Entry is inserted into deletedObjects after deletions are processed
eraseEntry(entry);
}
while (!groups.isEmpty()) {
auto* group = groups.takeFirst();
if (!(group->children().toSet() & groups.toSet()).isEmpty()) {
if (Tools::asSet(group->children()).intersects(Tools::asSet(groups))) {
// we need to finish all children before we are able to determine if the group can be removed
groups << group;
continue;
@@ -678,22 +491,17 @@ Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
}
deletions << object;
if (group->parentGroup()) {
changes << Change(Change::Type::Deleted, *group, tr("Explicit deletion"));
changes << tr("Deleting child %1 [%2]").arg(group->name(), group->uuidToHex());
} else {
changes << Change(Change::Type::Deleted, *group, tr("Implicit deletion (e.g. removal of parent group)"));
}
if (!m_dryRun) {
eraseGroup(group);
changes << tr("Deleting orphan %1 [%2]").arg(group->name(), group->uuidToHex());
}
eraseGroup(group);
}
// Put every deletion to the earliest date of deletion
if (deletions != context.m_targetDb->deletedObjects()) {
changes << Change(Change::Type::Metadata, tr("Changed deleted objects"));
if (!m_dryRun) {
context.m_targetDb->setDeletedObjects(deletions);
}
changes << tr("Changed deleted objects");
}
context.m_targetDb->setDeletedObjects(deletions);
return changes;
}
@@ -708,11 +516,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
for (const auto& iconUuid : sourceMetadata->customIconsOrder()) {
if (!targetMetadata->hasCustomIcon(iconUuid)) {
changes << Change(Change::Type::Metadata,
tr("Adding new icon %1").arg(QString::fromLatin1(iconUuid.toRfc4122().toHex())));
if (!m_dryRun) {
targetMetadata->addCustomIcon(iconUuid, sourceMetadata->customIcon(iconUuid));
}
targetMetadata->addCustomIcon(iconUuid, sourceMetadata->customIcon(iconUuid));
changes << tr("Adding missing icon %1").arg(QString::fromLatin1(iconUuid.toRfc4122().toHex()));
}
}
@@ -735,10 +540,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
// Do not remove protected custom data
if (!sourceMetadata->customData()->contains(key) && !sourceMetadata->customData()->isProtected(key)) {
auto value = targetMetadata->customData()->value(key);
changes << Change(Change::Type::Metadata, tr("Removed custom data %1 [%2]").arg(key, value));
if (!m_dryRun) {
targetMetadata->customData()->remove(key);
}
targetMetadata->customData()->remove(key);
changes << tr("Removed custom data %1 [%2]").arg(key, value);
}
}
@@ -753,10 +556,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
auto targetValue = targetMetadata->customData()->value(key);
// Merge only if the values are not the same.
if (sourceValue != targetValue) {
changes << Change(Change::Type::Metadata, tr("Adding custom data %1 [%2]").arg(key, sourceValue));
if (!m_dryRun) {
targetMetadata->customData()->set(key, sourceValue);
}
targetMetadata->customData()->set(key, sourceValue);
changes << tr("Adding custom data %1 [%2]").arg(key, sourceValue);
}
}
}

View File

@@ -32,51 +32,12 @@ public:
void setForcedMergeMode(Group::MergeMode mode);
void resetForcedMergeMode();
void setSkipDatabaseCustomData(bool state);
class Change
{
public:
enum class Type
{
Unspecified,
Added,
Modified,
Moved,
Deleted,
Metadata,
};
Change(Type type, QString details);
Change(Type type, const Group& group, QString details = "");
Change(Type type, const Entry& entry, QString details = "");
explicit Change(QString details = "");
[[nodiscard]] Type type() const;
[[nodiscard]] QString typeString() const;
[[nodiscard]] const QString& title() const;
[[nodiscard]] const QString& group() const;
[[nodiscard]] const QUuid& uuid() const;
[[nodiscard]] const QString& details() const;
[[nodiscard]] QString toString() const;
void merge();
bool operator==(const Change& other) const;
bool operator!=(const Change& other) const;
private:
Type m_type{Type::Unspecified};
QString m_title;
QString m_group;
QUuid m_uuid;
QString m_details;
};
using ChangeList = QList<Change>;
ChangeList merge(bool dryRun = false);
QStringList merge();
private:
typedef QString Change;
typedef QStringList ChangeList;
struct MergeContext
{
QPointer<const Database> m_sourceDb;
@@ -86,7 +47,6 @@ private:
QPointer<const Group> m_sourceGroup;
QPointer<Group> m_targetGroup;
};
ChangeList mergeGroup(const MergeContext& context);
ChangeList mergeDeletions(const MergeContext& context);
ChangeList mergeMetadata(const MergeContext& context);
@@ -108,7 +68,6 @@ private:
MergeContext m_context;
Group::MergeMode m_mode;
bool m_skipCustomData = false;
bool m_dryRun = false;
};
#endif // KEEPASSXC_MERGER_H

View File

@@ -99,7 +99,7 @@ void PassphraseGenerator::setWordList(const QString& path)
m_wordlist = wordset.toList();
if (!isWordListValid()) {
if (m_wordlist.size() < m_minimum_wordlist_length) {
qWarning("Wordlist is less than minimum acceptable size: %s", qPrintable(path));
}
}
@@ -117,7 +117,8 @@ void PassphraseGenerator::setWordSeparator(const QString& separator)
QString PassphraseGenerator::generatePassphrase() const
{
if (m_wordlist.isEmpty()) {
// In case there was an error loading the wordlist
if (!isValid() || m_wordlist.empty()) {
return {};
}
@@ -148,7 +149,7 @@ QString PassphraseGenerator::generatePassphrase() const
return words.join(m_separator);
}
bool PassphraseGenerator::isWordListValid() const
bool PassphraseGenerator::isValid() const
{
return m_wordlist.size() >= m_minWordListSize;
return m_wordCount > 0 && m_wordlist.size() >= m_minimum_wordlist_length;
}

View File

@@ -40,7 +40,7 @@ public:
void setWordCase(PassphraseWordCase wordCase);
void setDefaultWordList();
void setWordSeparator(const QString& separator);
bool isWordListValid() const;
bool isValid() const;
QString generatePassphrase() const;
@@ -50,7 +50,7 @@ public:
private:
int m_wordCount;
int m_minWordListSize = 1296;
int m_minimum_wordlist_length = 4000;
PassphraseWordCase m_wordCase;
QString m_separator;
QList<QString> m_wordlist;

View File

@@ -35,7 +35,6 @@
#include <QIODevice>
#include <QLocale>
#include <QMetaProperty>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QStringList>
#include <QUrl>
@@ -151,23 +150,23 @@ namespace Tools
if (seconds >= secondsInYear) {
auto years = std::floor(seconds / secondsInYear);
return QObject::tr("over %1 year(s)", "", years).arg(years);
return QObject::tr("over %1 year(s)", nullptr, years).arg(years);
} else if (seconds >= secondsInMonth) {
auto months = std::round(seconds / secondsInMonth);
return QObject::tr("about %1 month(s)", "", months).arg(months);
return QObject::tr("about %1 month(s)", nullptr, months).arg(months);
} else if (seconds >= secondsInWeek) {
auto weeks = std::round(seconds / secondsInWeek);
return QObject::tr("%1 week(s)", "", weeks).arg(weeks);
return QObject::tr("%1 week(s)", nullptr, weeks).arg(weeks);
} else if (seconds >= secondsInDay) {
auto days = std::floor(seconds / secondsInDay);
return QObject::tr("%1 day(s)", "", days).arg(days);
return QObject::tr("%1 day(s)", nullptr, days).arg(days);
} else if (seconds >= secondsInHour) {
auto hours = std::floor(seconds / secondsInHour);
return QObject::tr("%1 hour(s)", "", hours).arg(hours);
return QObject::tr("%1 hour(s)", nullptr, hours).arg(hours);
}
auto minutes = std::floor(seconds / 60);
return QObject::tr("%1 minute(s)", "", minutes).arg(minutes);
return QObject::tr("%1 minute(s)", nullptr, minutes).arg(minutes);
}
bool readFromDevice(QIODevice* device, QByteArray& data, int size)
@@ -479,57 +478,29 @@ namespace Tools
MimeType toMimeType(const QString& mimeName)
{
const static QStringList TextFormats = {"text/",
"application/json",
"application/xml",
"application/soap+xml",
"application/x-yaml",
"application/protobuf",
"application/x-zerosize"};
const static QStringList HtmlFormats = {"text/html"};
const static QStringList MarkdownFormats = {"text/markdown"};
const static QStringList ImageFormats = {"image/"};
static QStringList textFormats = {
"text/",
"application/json",
"application/xml",
"application/soap+xml",
"application/x-yaml",
"application/protobuf",
};
static QStringList imageFormats = {"image/"};
static auto isCompatible = [](const QString& format, const QStringList& list) {
return std::any_of(
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
};
if (isCompatible(mimeName, ImageFormats)) {
if (isCompatible(mimeName, imageFormats)) {
return MimeType::Image;
}
if (isCompatible(mimeName, TextFormats)) {
if (isCompatible(mimeName, HtmlFormats)) {
return MimeType::Html;
} else if (isCompatible(mimeName, MarkdownFormats)) {
return MimeType::Markdown;
}
if (isCompatible(mimeName, textFormats)) {
return MimeType::PlainText;
}
return MimeType::Unknown;
}
MimeType getMimeType(const QByteArray& data)
{
QMimeDatabase mimeDb;
const auto mime = mimeDb.mimeTypeForData(data);
return toMimeType(mime.name());
}
MimeType getMimeType(const QFileInfo& fileInfo)
{
QMimeDatabase mimeDb;
const auto mime = mimeDb.mimeTypeForFile(fileInfo);
return toMimeType(mime.name());
}
bool isTextMimeType(MimeType mimeType)
{
return mimeType == Tools::MimeType::PlainText || mimeType == Tools::MimeType::Html
|| mimeType == Tools::MimeType::Markdown;
}
} // namespace Tools

View File

@@ -22,7 +22,6 @@
#include "core/Global.h"
#include <QDateTime>
#include <QFileInfo>
#include <QList>
#include <QProcessEnvironment>
#include <QSet>
@@ -120,16 +119,10 @@ namespace Tools
{
Image,
PlainText,
Html,
Markdown,
Unknown
};
MimeType toMimeType(const QString& mimeName);
MimeType getMimeType(const QByteArray& data);
MimeType getMimeType(const QFileInfo& fileInfo);
bool isTextMimeType(MimeType mimeType);
} // namespace Tools
#endif // KEEPASSX_TOOLS_H

View File

@@ -210,33 +210,12 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
}
}
QString Totp::checkValidSettings(const QSharedPointer<Totp::Settings>& settings)
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
{
Q_ASSERT(!settings.isNull());
if (settings.isNull()) {
return QObject::tr("Invalid Settings", "TOTP");
}
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
if (secret.isNull()) {
return QObject::tr("Invalid Key", "TOTP");
}
if (settings->step == 0) {
return QObject::tr("Invalid Step", "TOTP");
}
if (settings->digits == 0) {
return QObject::tr("Invalid Digits", "TOTP");
}
return {};
}
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, bool* isValid, const quint64 time)
{
auto error = checkValidSettings(settings);
if (!error.isEmpty()) {
if (isValid) {
*isValid = false;
}
return error;
}
const Encoder& encoder = settings->encoder;
uint step = settings->step;
@@ -250,6 +229,9 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, bool*
}
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
if (secret.isNull()) {
return QObject::tr("Invalid Key", "TOTP");
}
QCryptographicHash::Algorithm cryptoHash;
switch (settings->algorithm) {
@@ -292,9 +274,6 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, bool*
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
password /= encoder.alphabet.size();
}
if (isValid) {
*isValid = true;
}
return retval;
}

View File

@@ -91,10 +91,8 @@ namespace Totp
const QString& title = {},
const QString& username = {},
bool forceOtp = false);
// Returns an empty string if settings are valid, otherwise an error message is supplied
QString checkValidSettings(const QSharedPointer<Totp::Settings>& settings);
QString
generateTotp(const QSharedPointer<Totp::Settings>& settings, bool* isValid = nullptr, const quint64 time = 0ull);
QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull);
bool hasCustomSettings(const QSharedPointer<Totp::Settings>& settings);

View File

@@ -33,11 +33,11 @@
*/
Argon2Kdf::Argon2Kdf(Type type)
: Kdf::Kdf(type == Type::Argon2d ? KeePass2::KDF_ARGON2D : KeePass2::KDF_ARGON2ID)
, m_version(ARGON2_DEFAULT_VERSION)
, m_memory(ARGON2_DEFAULT_MEMORY)
, m_parallelism(qMin<quint32>(QThread::idealThreadCount(), ARGON2_DEFAULT_PARALLELISM))
, m_version(0x13)
, m_memory(1 << 16)
, m_parallelism(static_cast<quint32>(QThread::idealThreadCount()))
{
m_rounds = ARGON2_DEFAULT_ROUNDS;
m_rounds = 10;
}
quint32 Argon2Kdf::version() const
@@ -52,7 +52,7 @@ bool Argon2Kdf::setVersion(quint32 version)
m_version = version;
return true;
}
m_version = ARGON2_DEFAULT_VERSION;
m_version = 0x13;
return false;
}
@@ -73,7 +73,7 @@ bool Argon2Kdf::setMemory(quint64 kibibytes)
m_memory = kibibytes;
return true;
}
m_memory = ARGON2_DEFAULT_MEMORY;
m_memory = 16;
return false;
}
@@ -89,7 +89,7 @@ bool Argon2Kdf::setParallelism(quint32 threads)
m_parallelism = threads;
return true;
}
m_parallelism = ARGON2_DEFAULT_PARALLELISM;
m_parallelism = 1;
return false;
}

View File

@@ -20,11 +20,6 @@
#include "Kdf.h"
constexpr auto ARGON2_DEFAULT_VERSION = 0x13;
constexpr auto ARGON2_DEFAULT_ROUNDS = 10;
constexpr auto ARGON2_DEFAULT_MEMORY = 1 << 16;
constexpr auto ARGON2_DEFAULT_PARALLELISM = 4;
class Argon2Kdf : public Kdf
{
public:
@@ -52,15 +47,6 @@ public:
int benchmark(int msec) const override;
static quint64 toMebibytes(quint64 kibibytes)
{
return kibibytes >> 10;
}
static quint64 toKibibytes(quint64 mebibits)
{
return mebibits << 10;
}
quint32 m_version;
quint64 m_memory;
quint32 m_parallelism;

View File

@@ -125,7 +125,7 @@ namespace FdoSecrets
// add some informative and readonly attributes
attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex();
attrs[ItemAttributes::PathKey] = path();
if (m_backend->hasValidTotp()) {
if (m_backend->hasTotp()) {
attrs[ItemAttributes::TotpKey] = m_backend->totp();
}
return {};

View File

@@ -73,13 +73,7 @@ namespace
}
if (loginMap.contains("itemEmail")) {
// Place the email value as the username if empty, otherwise set it as an attribute
const auto email = loginMap.value("itemEmail").toString();
if (entry->username().isEmpty()) {
entry->setUsername(email);
} else if (!email.isEmpty()) {
entry->attributes()->set("login_email", email);
}
entry->attributes()->set("login_email", loginMap.value("itemEmail").toString());
}
// Set the entry url(s)

View File

@@ -113,7 +113,7 @@ Application::Application(int& argc, char** argv)
.toUtf8()
.constData();
// forcibly reset the lock file
// forceably reset the lock file
m_lockFile->removeStaleLockFile();
m_lockFile->tryLock();
// start the listen server

View File

@@ -27,11 +27,9 @@
#include "autotype/AutoType.h"
#include "core/Translator.h"
#include "gui/GuiTools.h"
#include "gui/Icons.h"
#include "gui/MainWindow.h"
#include "gui/osutils/OSUtils.h"
#include "gui/styles/StateColorPalette.h"
#include "quickunlock/QuickUnlockInterface.h"
#include "FileDialog.h"
@@ -64,6 +62,28 @@ private:
QWidget* widget;
};
/**
* Helper class to ignore mouse wheel events on non-focused widgets
* NOTE: The widget must NOT have a focus policy of "WHEEL"
*/
class MouseWheelEventFilter : public QObject
{
public:
explicit MouseWheelEventFilter(QObject* parent)
: QObject(parent){};
protected:
bool eventFilter(QObject* obj, QEvent* event) override
{
const auto* widget = qobject_cast<QWidget*>(obj);
if (event->type() == QEvent::Wheel && widget && !widget->hasFocus()) {
event->ignore();
return true;
}
return QObject::eventFilter(obj, event);
}
};
ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
: EditWidget(parent)
, m_secWidget(new QWidget())
@@ -109,8 +129,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
connect(m_generalUi->backupFilePathPicker, SIGNAL(pressed()), SLOT(selectBackupDirectory()));
connect(m_generalUi->showExpiredEntriesOnDatabaseUnlockCheckBox, SIGNAL(toggled(bool)),
SLOT(showExpiredEntriesOnDatabaseUnlockToggled(bool)));
connect(m_generalUi->autoTypeAskCheckBox, SIGNAL(toggled(bool)),
SLOT(autoTypeAskToggled(bool)));
connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)),
m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool)));
@@ -137,10 +155,7 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_generalUi->autoTypeShortcutWidget->setStyleSheet("");
} else {
QToolTip::showText(mapToGlobal(rect().bottomLeft()), error);
StateColorPalette statePalette;
auto color = statePalette.color(StateColorPalette::ColorRole::Error);
m_generalUi->autoTypeShortcutWidget->setStyleSheet(
QString("QLineEdit { background: %1; }").arg(color.name()));
m_generalUi->autoTypeShortcutWidget->setStyleSheet("background-color: #FF9696;");
}
});
connect(m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutReset, this, [this] {
@@ -287,9 +302,6 @@ void ApplicationSettingsWidget::loadSettings()
showExpiredEntriesOnDatabaseUnlockToggled(m_generalUi->showExpiredEntriesOnDatabaseUnlockCheckBox->isChecked());
m_generalUi->autoTypeAskCheckBox->setChecked(config()->get(Config::Security_AutoTypeAsk).toBool());
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->setChecked(
config()->get(Config::Security_AutoTypeSkipMainWindowConfirmation).toBool());
autoTypeAskToggled(m_generalUi->autoTypeAskCheckBox->isChecked());
m_generalUi->autoTypeRelockDatabaseCheckBox->setChecked(config()->get(Config::Security_RelockAutoType).toBool());
if (autoType()->isAvailable()) {
@@ -433,8 +445,6 @@ void ApplicationSettingsWidget::saveSettings()
m_generalUi->showExpiredEntriesOnDatabaseUnlockOffsetSpinBox->value());
config()->set(Config::Security_AutoTypeAsk, m_generalUi->autoTypeAskCheckBox->isChecked());
config()->set(Config::Security_AutoTypeSkipMainWindowConfirmation,
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->isChecked());
config()->set(Config::Security_RelockAutoType, m_generalUi->autoTypeRelockDatabaseCheckBox->isChecked());
if (autoType()->isAvailable()) {
@@ -608,11 +618,6 @@ void ApplicationSettingsWidget::showExpiredEntriesOnDatabaseUnlockToggled(bool c
m_generalUi->showExpiredEntriesOnDatabaseUnlockOffsetSpinBox->setEnabled(checked);
}
void ApplicationSettingsWidget::autoTypeAskToggled(bool checked)
{
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->setEnabled(checked);
}
void ApplicationSettingsWidget::selectBackupDirectory()
{
auto backupDirectory =
@@ -621,4 +626,4 @@ void ApplicationSettingsWidget::selectBackupDirectory()
m_generalUi->backupFilePath->setText(
QDir(backupDirectory).filePath(config()->getDefault(Config::BackupFilePathPattern).toString()));
}
}
}

View File

@@ -63,7 +63,6 @@ private slots:
void rememberDatabasesToggled(bool checked);
void checkUpdatesToggled(bool checked);
void showExpiredEntriesOnDatabaseUnlockToggled(bool checked);
void autoTypeAskToggled(bool checked);
void selectBackupDirectory();
private:

View File

@@ -1157,42 +1157,6 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="autoTypeSkipMainWindowLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="autoTypeSkipMainWindowSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="autoTypeSkipMainWindowConfirmationCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Skip confirmation for main window Auto-Type actions</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="autoTypeHideExpiredEntryCheckBox">
<property name="text">
@@ -1357,9 +1321,6 @@
<property name="text">
<string>Remember last typed entry for:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">

View File

@@ -44,7 +44,6 @@
#include "gui/FileDialog.h"
#include "gui/GuiTools.h"
#include "gui/MainWindow.h"
#include "gui/MergeDialog.h"
#include "gui/MessageBox.h"
#include "gui/TotpDialog.h"
#include "gui/TotpExportSettingsDialog.h"
@@ -878,18 +877,12 @@ void DatabaseWidget::performAutoType(const QString& sequence)
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
// Check if we need to ask for confirmation
bool shouldAsk = config()->get(Config::Security_AutoTypeAsk).toBool();
bool skipMainWindowConfirmation = config()->get(Config::Security_AutoTypeSkipMainWindowConfirmation).toBool();
// Show confirmation if Security_AutoTypeAsk is true AND Security_AutoTypeSkipMainWindowConfirmation is false
if (shouldAsk && !skipMainWindowConfirmation) {
// TODO: Include name of previously active window in confirmation question
if (MessageBox::question(
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
!= MessageBox::Yes) {
return;
}
// TODO: Include name of previously active window in confirmation question
if (config()->get(Config::Security_AutoTypeAsk).toBool()
&& MessageBox::question(
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
!= MessageBox::Yes) {
return;
}
if (sequence.isEmpty()) {
@@ -925,16 +918,6 @@ void DatabaseWidget::performAutoTypeTOTP()
performAutoType(QStringLiteral("{TOTP}"));
}
void DatabaseWidget::performAutoTypeURL()
{
performAutoType(QStringLiteral("{URL}"));
}
void DatabaseWidget::performAutoTypeURLEnter()
{
performAutoType(QStringLiteral("{URL}{ENTER}"));
}
void DatabaseWidget::openUrl()
{
auto currentEntry = currentSelectedEntry();
@@ -1121,8 +1104,8 @@ void DatabaseWidget::deleteGroup()
if (inRecycleBin || isRecycleBin || isRecycleBinSubgroup || !m_db->metadata()->recycleBinEnabled()) {
auto result = MessageBox::question(
this,
tr("Confirm Delete Group"),
tr("Do you really want to permanently delete the group \"%1\"?").arg(currentGroup->name().toHtmlEscaped()),
tr("Delete group"),
tr("Do you really want to delete the group \"%1\" for good?").arg(currentGroup->name().toHtmlEscaped()),
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
@@ -1131,7 +1114,7 @@ void DatabaseWidget::deleteGroup()
}
} else {
auto result = MessageBox::question(this,
tr("Confirm Recycle Group"),
tr("Move group to recycle bin?"),
tr("Do you really want to move the group "
"\"%1\" to the recycle bin?")
.arg(currentGroup->name().toHtmlEscaped()),
@@ -1388,30 +1371,18 @@ void DatabaseWidget::mergeDatabase(bool accepted)
return;
}
#ifdef WITH_XC_KEESHARE
// Disable KeeShare while merging to avoid conflicts with incoming changes
KeeShare::instance()->setSharingEnabled(m_db, false);
#endif
Merger merger(srcDb.data(), m_db.data());
QStringList changeList = merger.merge();
auto* mergeDialog = new MergeDialog(srcDb, m_db, this);
connect(mergeDialog, &MergeDialog::databaseMerged, [this](bool changed) {
if (changed) {
showMessage(tr("Successfully merged the selected database."), MessageWidget::Positive);
emit databaseMerged(m_db);
} else {
showMessage(tr("No changes were made by the merge operation."), MessageWidget::Information);
}
});
connect(mergeDialog, &MergeDialog::finished, [this](int result) {
if (result == QDialog::Rejected) {
showMessage(tr("Merge canceled, no changes were made."), MessageWidget::Information);
}
#ifdef WITH_XC_KEESHARE
KeeShare::instance()->setSharingEnabled(m_db, true);
#endif
});
mergeDialog->open();
if (!changeList.isEmpty()) {
showMessage(tr("Successfully merged the database files."), MessageWidget::Information);
} else {
showMessage(tr("Database was not modified by merge operation."), MessageWidget::Information);
}
}
switchToMainView();
emit databaseMerged(m_db);
}
void DatabaseWidget::syncUnlockedDatabase(bool accepted)
@@ -1451,7 +1422,7 @@ bool DatabaseWidget::syncWithDatabase(const QSharedPointer<Database>& otherDb, Q
emit updateSyncProgress(50, tr("Syncing..."));
Merger firstMerge(m_db.data(), otherDb.data());
Merger secondMerge(otherDb.data(), m_db.data());
auto changeList = firstMerge.merge() + secondMerge.merge();
QStringList changeList = firstMerge.merge() + secondMerge.merge();
if (!changeList.isEmpty()) {
// Save synced databases
@@ -1556,7 +1527,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
}
break;
case EntryModel::Totp:
if (entry->hasValidTotp()) {
if (entry->hasTotp()) {
setClipboardTextAndMinimize(entry->totp());
} else {
setupTotp();
@@ -2415,7 +2386,7 @@ bool DatabaseWidget::currentEntryHasTotp()
if (!currentEntry) {
return false;
}
return currentEntry->hasValidTotp();
return currentEntry->hasTotp();
}
#ifdef WITH_XC_SSHAGENT
@@ -2653,7 +2624,7 @@ bool DatabaseWidget::saveBackup()
}
const QString newFilePath = fileDialog()->getSaveFileName(this,
tr("Save Database Backup"),
tr("Save database backup"),
FileDialog::getLastDir("backup", oldFilePath),
tr("KeePass 2 Database").append(" (*.kdbx)"));
@@ -2713,22 +2684,6 @@ bool DatabaseWidget::isRecycleBinSelected() const
return (group && group->isRecycled()) || (entry && entry->isRecycled());
}
bool DatabaseWidget::hasRecycledSelectedEntries() const
{
if (!m_entryView) {
return false;
}
// Check if any of the selected entries are actually recycled
for (auto* entry : m_entryView->selectedEntries()) {
if (entry && entry->isRecycled()) {
return true;
}
}
return false;
}
void DatabaseWidget::emptyRecycleBin()
{
if (!isRecycleBinSelected()) {

View File

@@ -99,7 +99,6 @@ public:
bool canDeleteCurrentGroup() const;
bool isGroupSelected() const;
bool isRecycleBinSelected() const;
bool hasRecycledSelectedEntries() const;
int numberOfSelectedEntries() const;
int currentEntryIndex() const;
@@ -215,8 +214,6 @@ public slots:
void performAutoTypePassword();
void performAutoTypePasswordEnter();
void performAutoTypeTOTP();
void performAutoTypeURL();
void performAutoTypeURLEnter();
void setClipboardTextAndMinimize(const QString& text);
void openUrl();
void downloadSelectedFavicons();

View File

@@ -84,7 +84,7 @@ int EditWidget::pageIndex(const QWidget* widget) const
for (int i = 0; i < m_ui->stackedWidget->count(); i++) {
auto* scrollArea = qobject_cast<QScrollArea*>(m_ui->stackedWidget->widget(i));
if (scrollArea && (scrollArea == widget || scrollArea->widget() == widget)) {
if (scrollArea && scrollArea->widget() == widget) {
return i;
}
}

View File

@@ -70,7 +70,8 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
m_ui->entryTotpLabel->installEventFilter(this);
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotp, SLOT(setVisible(bool)));
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpLabel, SLOT(setVisible(bool)));
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpProgress, SLOT(setVisible(bool)));
connect(m_ui->entryCloseButton, SIGNAL(clicked()), SLOT(hide()));
connect(m_ui->toggleUsernameButton, SIGNAL(clicked(bool)), SLOT(setUsernameVisible(bool)));
connect(m_ui->togglePasswordButton, SIGNAL(clicked(bool)), SLOT(setPasswordVisible(bool)));
@@ -94,8 +95,6 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
connect(config(), &Config::changed, this, [this](Config::ConfigKey key) {
if (key == Config::GUI_HidePreviewPanel) {
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
} else if (key == Config::Security_HideTotpPreviewPanel) {
m_ui->entryTotpButton->setChecked(!config()->get(Config::Security_HideTotpPreviewPanel).toBool());
}
refresh();
});
@@ -260,9 +259,9 @@ void EntryPreviewWidget::updateEntryTotp()
m_totpTimer.start(1000);
m_ui->entryTotpProgress->setMaximum(m_currentEntry->totpSettings()->step);
updateTotpLabel();
m_ui->entryTotp->setVisible(m_ui->entryTotpButton->isChecked());
} else {
m_ui->entryTotp->hide();
m_ui->entryTotpLabel->hide();
m_ui->entryTotpProgress->hide();
m_ui->entryTotpButton->setChecked(false);
m_ui->entryTotpLabel->clear();
m_totpTimer.stop();
@@ -547,23 +546,16 @@ void EntryPreviewWidget::updateGroupSharingTab()
void EntryPreviewWidget::updateTotpLabel()
{
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
bool isValid = false;
auto totpCode = m_currentEntry->totp(&isValid);
if (isValid) {
totpCode.insert(totpCode.size() / 2, " ");
auto step = m_currentEntry->totpSettings()->step;
auto timeleft = step - (Clock::currentSecondsSinceEpoch() % step);
m_ui->entryTotpProgress->setValue(timeleft);
m_ui->entryTotpProgress->update();
} else {
m_totpTimer.stop();
}
m_ui->entryTotpProgress->setVisible(isValid);
auto totpCode = m_currentEntry->totp();
totpCode.insert(totpCode.size() / 2, " ");
m_ui->entryTotpLabel->setText(totpCode);
auto step = m_currentEntry->totpSettings()->step;
auto timeleft = step - (Clock::currentSecondsSinceEpoch() % step);
m_ui->entryTotpProgress->setValue(timeleft);
m_ui->entryTotpProgress->update();
} else {
m_ui->entryTotp->setVisible(false);
m_ui->entryTotpLabel->clear();
m_totpTimer.stop();
}
}

View File

@@ -110,66 +110,47 @@
</layout>
</item>
<item>
<widget class="QWidget" name="entryTotp" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="entryTotpLabel">
<property name="font">
<font>
<pointsize>10</pointsize>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Double click to copy to clipboard</string>
</property>
<property name="text">
<string notr="true">1234567</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="entryTotpProgress">
<property name="maximumSize">
<size>
<width>57</width>
<height>4</height>
</size>
</property>
<property name="value">
<number>50</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<item>
<widget class="QLabel" name="entryTotpLabel">
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Double click to copy to clipboard</string>
</property>
<property name="text">
<string notr="true">1234567</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="entryTotpProgress">
<property name="maximumSize">
<size>
<width>57</width>
<height>4</height>
</size>
</property>
<property name="value">
<number>50</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QToolButton" name="entryTotpButton">

View File

@@ -32,15 +32,14 @@ namespace GuiTools
if (permanent) {
QString prompt;
if (entries.size() == 1) {
auto entry = entries.first();
prompt = QObject::tr("Do you really want to permanently delete the entry \"%1\"?")
.arg(entry->resolvePlaceholder(entry->title()).toHtmlEscaped());
prompt = QObject::tr("Do you really want to delete the entry \"%1\" for good?")
.arg(entries.first()->title().toHtmlEscaped());
} else {
prompt = QObject::tr("Do you really want to permanently delete %n entry(s)?", "", entries.size());
prompt = QObject::tr("Do you really want to delete %n entry(s) for good?", "", entries.size());
}
auto answer = MessageBox::question(parent,
QObject::tr("Confirm Delete Entry(s)", "", entries.size()),
QObject::tr("Delete entry(s)?", "", entries.size()),
prompt,
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
@@ -51,15 +50,14 @@ namespace GuiTools
} else {
QString prompt;
if (entries.size() == 1) {
auto entry = entries.first();
prompt = QObject::tr("Do you really want to move entry \"%1\" to the recycle bin?")
.arg(entry->resolvePlaceholder(entry->title()).toHtmlEscaped());
.arg(entries.first()->title().toHtmlEscaped());
} else {
prompt = QObject::tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size());
}
auto answer = MessageBox::question(parent,
QObject::tr("Confirm Recycle Entry(s)", "", entries.size()),
QObject::tr("Move entry(s) to recycle bin?", "", entries.size()),
prompt,
MessageBox::Move | MessageBox::Cancel,
MessageBox::Cancel);
@@ -74,12 +72,11 @@ namespace GuiTools
return false;
}
auto answer =
MessageBox::question(parent,
QObject::tr("Confirm Delete Plugin Data"),
QObject::tr("Delete plugin data from the selected entry(s)?", "", entries.size()),
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
auto answer = MessageBox::question(parent,
QObject::tr("Delete plugin data?"),
QObject::tr("Delete plugin data from Entry(s)?", "", entries.size()),
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
return answer == MessageBox::Delete;
}
@@ -103,7 +100,7 @@ namespace GuiTools
// Prompt the user on what to do with the reference (Overwrite, Delete, Skip)
auto result = MessageBox::question(
parent,
QObject::tr("Confirm Replace Entry References"),
QObject::tr("Replace references to entry?"),
QObject::tr(
"Entry \"%1\" has %2 reference(s). "
"Do you want to overwrite references with values, skip this entry, or delete anyway?",

View File

@@ -18,7 +18,6 @@
#ifndef KEEPASSXC_GUITOOLS_H
#define KEEPASSXC_GUITOOLS_H
#include <QEvent>
#include <QList>
#include <QWidget>
@@ -30,27 +29,4 @@ namespace GuiTools
bool confirmDeletePluginData(QWidget* parent, const QList<Entry*>& entries);
size_t deleteEntriesResolveReferences(QWidget* parent, const QList<Entry*>& entries, bool permanent);
} // namespace GuiTools
/**
* Helper class to ignore mouse wheel events on non-focused widgets
* NOTE: The widget must NOT have a focus policy of "WHEEL"
*/
class MouseWheelEventFilter : public QObject
{
public:
explicit MouseWheelEventFilter(QObject* parent)
: QObject(parent){};
protected:
bool eventFilter(QObject* obj, QEvent* event) override
{
const auto* widget = qobject_cast<QWidget*>(obj);
if (event->type() == QEvent::Wheel && widget && !widget->hasFocus()) {
event->ignore();
return true;
}
return QObject::eventFilter(obj, event);
}
};
#endif // KEEPASSXC_GUITOOLS_H

View File

@@ -60,4 +60,4 @@ private:
friend class TestIconDownloader;
};
#endif // KEEPASSXC_ICONDOWNLOADER_H
#endif // KEEPASSXC_ICONDOWNLOADER_H

View File

@@ -38,6 +38,7 @@ IconDownloaderDialog::IconDownloaderDialog(QWidget* parent)
, m_ui(new Ui::IconDownloaderDialog())
, m_dataModel(new QStandardItemModel(this))
{
setWindowFlags(Qt::Window);
setAttribute(Qt::WA_DeleteOnClose);
m_ui->setupUi(this);

View File

@@ -172,8 +172,6 @@ MainWindow::MainWindow()
autotypeMenu->addAction(m_ui->actionEntryAutoTypePassword);
autotypeMenu->addAction(m_ui->actionEntryAutoTypePasswordEnter);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeTOTP);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeURL);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeURLEnter);
m_ui->actionEntryAutoType->setMenu(autotypeMenu);
auto autoTypeButton = qobject_cast<QToolButton*>(m_ui->toolBar->widgetForAction(m_ui->actionEntryAutoType));
if (autoTypeButton) {
@@ -275,7 +273,7 @@ MainWindow::MainWindow()
m_ui->actionAllowScreenCapture->setVisible(osUtils->canPreventScreenCapture());
m_inactivityTimer = new InactivityTimer(this);
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockAllDatabases()));
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
applySettingsChanges();
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
@@ -389,8 +387,6 @@ MainWindow::MainWindow()
m_ui->actionEntryAutoTypePassword->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypePasswordEnter->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeTOTP->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeURL->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeURLEnter->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryMoveUp->setIcon(icons()->icon("move-up"));
m_ui->actionEntryMoveDown->setIcon(icons()->icon("move-down"));
m_ui->actionEntryCopyUsername->setIcon(icons()->icon("username-copy"));
@@ -530,9 +526,6 @@ MainWindow::MainWindow()
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypePasswordEnter, SIGNAL(triggered()), SLOT(performAutoTypePasswordEnter()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoTypeTOTP, SIGNAL(triggered()), SLOT(performAutoTypeTOTP()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoTypeURL, SIGNAL(triggered()), SLOT(performAutoTypeURL()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypeURLEnter, SIGNAL(triggered()), SLOT(performAutoTypeURLEnter()));
m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl()));
m_actionMultiplexer.connect(m_ui->actionEntryDownloadIcon, SIGNAL(triggered()), SLOT(downloadSelectedFavicons()));
#ifdef WITH_XC_SSHAGENT
@@ -828,8 +821,6 @@ void MainWindow::updateSetTagsMenu()
return nullptr;
};
m_ui->menuTags->setTearOffEnabled(true);
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
if (dbWidget) {
// Enumerate tags applied to the selected entries
@@ -840,30 +831,31 @@ void MainWindow::updateSetTagsMenu()
}
}
// Remove missing tags
// Add known database tags as actions and set checked if
// a selected entry has that tag
const auto tagList = dbWidget->database()->tagList();
for (const auto action : m_ui->menuTags->actions()) {
if (!tagList.contains(action->text()) || !action->isEnabled()) {
delete action;
for (const auto& tag : tagList) {
auto action = actionForTag(m_ui->menuTags, tag);
if (action) {
action->setChecked(selectedTags.contains(tag));
} else {
action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
action->setCheckable(true);
action->setChecked(selectedTags.contains(tag));
m_setTagsMenuActions->addAction(action);
}
}
// Add known database tags as actions and set checked if
// a selected entry has that tag
for (const auto& tag : tagList) {
auto action = actionForTag(m_ui->menuTags, tag);
if (!action) {
action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
action->setCheckable(true);
m_setTagsMenuActions->addAction(action);
// Remove missing tags
for (const auto action : m_ui->menuTags->actions()) {
if (!tagList.contains(action->text())) {
action->deleteLater();
}
action->setChecked(selectedTags.contains(tag));
}
}
// If no tags exist in the database then show a tip to the user
if (m_ui->menuTags->isEmpty()) {
m_ui->menuTags->setTearOffEnabled(false);
auto action = m_ui->menuTags->addAction(tr("No Tags"));
action->setEnabled(false);
}
@@ -944,20 +936,8 @@ void MainWindow::updateMenuActionState()
m_ui->actionEntryEdit->setEnabled(singleEntrySelected);
m_ui->actionEntryExpire->setEnabled(multiEntrySelected);
m_ui->actionEntryDelete->setEnabled(multiEntrySelected);
if (dbWidget) {
if (dbWidget->database()->metadata()->recycleBinEnabled() && !inRecycleBin) {
m_ui->actionEntryDelete->setToolTip(
tr("Move selected entry(s) to the recycle bin", "", dbWidget->numberOfSelectedEntries()));
} else {
m_ui->actionEntryDelete->setToolTip(
tr("Permanently delete the selected entry(s)", "", dbWidget->numberOfSelectedEntries()));
}
} else {
m_ui->actionEntryDelete->setToolTip(tr("Delete Entry"));
}
bool hasRecycledEntries = (inDatabase && dbWidget && dbWidget->hasRecycledSelectedEntries());
m_ui->actionEntryRestore->setVisible(multiEntrySelected && hasRecycledEntries);
m_ui->actionEntryRestore->setEnabled(multiEntrySelected && hasRecycledEntries);
m_ui->actionEntryRestore->setVisible(multiEntrySelected && inRecycleBin);
m_ui->actionEntryRestore->setEnabled(multiEntrySelected && inRecycleBin);
if (dbWidget) {
m_ui->actionEntryRestore->setText(tr("Restore Entry(s)", "", dbWidget->numberOfSelectedEntries()));
m_ui->actionEntryRestore->setToolTip(tr("Restore Entry(s)", "", dbWidget->numberOfSelectedEntries()));
@@ -995,8 +975,6 @@ void MainWindow::updateMenuActionState()
m_ui->actionEntryAutoTypePassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword());
m_ui->actionEntryAutoTypePasswordEnter->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword());
m_ui->actionEntryAutoTypeTOTP->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryAutoTypeURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryAutoTypeURLEnter->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryAutoTypeTOTP->setVisible(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryOpenUrl->setEnabled(singleEntryOrEditing && dbWidget->currentEntryHasUrl());
m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
@@ -1038,7 +1016,7 @@ void MainWindow::updateMenuActionState()
m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && groupHasEntries && !inRecycleBin);
// Database Menu
m_ui->actionDatabaseSave->setEnabled(databaseUnlocked && m_ui->tabWidget->canSave());
m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave());
m_ui->actionDatabaseSaveAs->setEnabled(databaseUnlocked);
m_ui->actionDatabaseSaveBackup->setEnabled(databaseUnlocked);
m_ui->actionDatabaseClose->setEnabled(dbWidget);
@@ -1336,11 +1314,6 @@ void MainWindow::databaseTabChanged(int tabIndex)
m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget());
updateEntryCountLabel();
// Clear the tags menu to prevent re-use between databases
for (const auto action : m_ui->menuTags->actions()) {
delete action;
}
}
bool MainWindow::event(QEvent* event)
@@ -1677,23 +1650,21 @@ void MainWindow::showGroupContextMenu(const QPoint& globalPos)
void MainWindow::applySettingsChanges()
{
int timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000;
if (timeout <= 0) {
timeout = 60;
}
m_inactivityTimer->setInactivityTimeout(timeout);
if (config()->get(Config::Security_LockDatabaseIdle).toBool()) {
auto timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000;
m_inactivityTimer->activate(timeout);
m_inactivityTimer->activate();
} else {
m_inactivityTimer->deactivate();
}
auto hideToolbar = config()->get(Config::GUI_HideToolbar).toBool();
auto hideMenubar = config()->get(Config::GUI_HideMenubar).toBool();
m_ui->actionShowToolbar->setChecked(!hideToolbar);
m_ui->actionShowMenubar->setChecked(!hideMenubar);
// When menubar is hidden with setHidden() the menu keyboard shortcuts are disabled on Wayland,
// so force height of 0 instead and use maximumHeight() > 0 instead of isVisible() elsewhere
m_ui->menubar->setMaximumHeight(hideMenubar ? 0 : QWIDGETSIZE_MAX);
m_ui->actionShowToolbar->setChecked(!config()->get(Config::GUI_HideToolbar).toBool());
m_ui->actionShowMenubar->setChecked(!config()->get(Config::GUI_HideMenubar).toBool());
m_ui->menubar->setHidden(config()->get(Config::GUI_HideMenubar).toBool());
m_ui->toolBar->setHidden(config()->get(Config::GUI_HideToolbar).toBool());
auto movable = config()->get(Config::GUI_MovableToolbar).toBool();
m_ui->toolBar->setMovable(movable);
@@ -1856,6 +1827,13 @@ void MainWindow::closeModalWindow()
}
}
void MainWindow::lockDatabasesAfterInactivity()
{
if (!m_ui->tabWidget->lockDatabases()) {
m_inactivityTimer->activate();
}
}
bool MainWindow::isTrayIconEnabled() const
{
return m_trayIcon && m_trayIcon->isVisible();
@@ -1910,7 +1888,7 @@ void MainWindow::bringToFront()
void MainWindow::handleScreenLock()
{
if (config()->get(Config::Security_LockDatabaseScreenLock).toBool()) {
lockAllDatabases();
lockDatabasesAfterInactivity();
}
}
@@ -1960,7 +1938,7 @@ void MainWindow::closeAllDatabases()
void MainWindow::lockAllDatabases()
{
m_ui->tabWidget->lockDatabases();
lockDatabasesAfterInactivity();
}
void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint)
@@ -2016,7 +1994,6 @@ void MainWindow::initViewMenu()
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
} else {
kpxcApp->applyTheme();
kpxcApp->applyFontSize();
}
});
@@ -2207,11 +2184,10 @@ MainWindowEventFilter::MainWindowEventFilter(QObject* parent)
m_menubarTimer.setSingleShot(false);
connect(&m_menubarTimer, &QTimer::timeout, this, [this] {
auto mainwindow = getMainWindow();
if (mainwindow && mainwindow->m_ui->menubar->maximumHeight() > 0
&& config()->get(Config::GUI_HideMenubar).toBool()) {
if (mainwindow && mainwindow->m_ui->menubar->isVisible() && config()->get(Config::GUI_HideMenubar).toBool()) {
// If the menu bar is visible with no active menu, hide it
if (!mainwindow->m_ui->menubar->activeAction()) {
mainwindow->m_ui->menubar->setMaximumHeight(0);
mainwindow->m_ui->menubar->setVisible(false);
m_altCoolDown.start();
m_menubarTimer.stop();
}
@@ -2270,13 +2246,9 @@ bool MainWindowEventFilter::eventFilter(QObject* watched, QEvent* event)
if (keyEvent->key() == Qt::Key_Alt && !keyEvent->modifiers() && config()->get(Config::GUI_HideMenubar).toBool()
&& !m_altCoolDown.isActive()) {
auto menubar = mainWindow->m_ui->menubar;
menubar->setMaximumHeight(menubar->maximumHeight() > 0 ? 0 : QWIDGETSIZE_MAX);
if (menubar->maximumHeight() > 0) {
QTimer::singleShot(0, [menubar, mainWindow] {
// Run this with a singleshot timer so it's after menubar->setMaximumHeight() has taken effect,
// otherwise it won't be selected and menubarTimer will hide the menubar instantly
menubar->setActiveAction(mainWindow->m_ui->menuFile->menuAction());
});
menubar->setVisible(!menubar->isVisible());
if (menubar->isVisible()) {
menubar->setActiveAction(mainWindow->m_ui->menuFile->menuAction());
m_menubarTimer.start();
} else {
m_menubarTimer.stop();

View File

@@ -140,6 +140,7 @@ private slots:
void applySettingsChanges();
void trayIconTriggered(QSystemTrayIcon::ActivationReason reason);
void processTrayIconTrigger();
void lockDatabasesAfterInactivity();
void handleScreenLock();
void showErrorMessage(const QString& message);
void selectNextDatabaseTab();

View File

@@ -532,7 +532,7 @@
<string>&amp;New Entry…</string>
</property>
<property name="toolTip">
<string>New Entry</string>
<string>Create Entry</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+N</string>
@@ -542,9 +542,6 @@
<property name="text">
<string>&amp;Edit Entry…</string>
</property>
<property name="iconText">
<string>Edit Entry…</string>
</property>
<property name="toolTip">
<string>Edit Entry</string>
</property>
@@ -557,21 +554,12 @@
<bool>false</bool>
</property>
<property name="text">
<string>E&amp;xpire Entry</string>
</property>
<property name="iconText">
<string>Expire Entry</string>
</property>
<property name="toolTip">
<string>Expire Entry</string>
<string>E&amp;xpire Entry</string>
</property>
</action>
<action name="actionEntryDelete">
<property name="text">
<string>&amp;Delete Entry</string>
</property>
<property name="iconText">
<string>Delete Entry</string>
<string>&amp;Delete Entry</string>
</property>
<property name="toolTip">
<string>Delete Entry</string>
@@ -871,34 +859,6 @@
<string>Perform Auto-Type: {TOTP}</string>
</property>
</action>
<action name="actionEntryAutoTypeURL">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">{URL}</string>
</property>
<property name="iconText">
<string notr="true">{URL}</string>
</property>
<property name="toolTip">
<string notr="true">{URL}</string>
</property>
</action>
<action name="actionEntryAutoTypeURLEnter">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">{URL}{ENTER}</string>
</property>
<property name="iconText">
<string notr="true">{URL}{ENTER}</string>
</property>
<property name="toolTip">
<string notr="true">{URL}{ENTER}</string>
</property>
</action>
<action name="actionEntryDownloadIcon">
<property name="text">
<string>Download &amp;Favicon</string>

View File

@@ -1,199 +0,0 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MergeDialog.h"
#include "ui_MergeDialog.h"
#include "core/Database.h"
#include <QPushButton>
#include <QShortcut>
MergeDialog::MergeDialog(QSharedPointer<Database> source, QSharedPointer<Database> target, QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::MergeDialog())
, m_headerContextMenu(new QMenu())
, m_sourceDatabase(std::move(source))
, m_targetDatabase(std::move(target))
{
setAttribute(Qt::WA_DeleteOnClose);
// block input to other windows since other interactions can lead to unexpected merge results
setWindowModality(Qt::WindowModality::ApplicationModal);
m_ui->setupUi(this);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Merge"));
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus();
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &MergeDialog::cancelMerge);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &MergeDialog::performMerge);
setupChangeTable();
updateChangeTable();
}
MergeDialog::MergeDialog(const Merger::ChangeList& changes, QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::MergeDialog())
, m_headerContextMenu(new QMenu())
, m_changes(changes)
{
setAttribute(Qt::WA_DeleteOnClose);
m_ui->setupUi(this);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus();
m_ui->buttonBox->button(QDialogButtonBox::Abort)->hide();
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &MergeDialog::close);
setupChangeTable();
}
MergeDialog::~MergeDialog() = default;
QVector<MergeDialog::MergeDialogColumns> MergeDialog::columns()
{
return {MergeDialogColumns::Group,
MergeDialogColumns::Title,
MergeDialogColumns::Uuid,
MergeDialogColumns::Type,
MergeDialogColumns::Details};
}
int MergeDialog::columnIndex(MergeDialogColumns column)
{
return columns().indexOf(column);
}
QString MergeDialog::columnName(MergeDialogColumns column)
{
switch (column) {
case MergeDialogColumns::Group:
return tr("Group");
case MergeDialogColumns::Title:
return tr("Title");
case MergeDialogColumns::Uuid:
return tr("UUID");
case MergeDialogColumns::Type:
return tr("Change");
case MergeDialogColumns::Details:
return tr("Details");
}
return {};
}
QString MergeDialog::cellValue(const Merger::Change& change, MergeDialogColumns column)
{
switch (column) {
case MergeDialogColumns::Group:
return change.group();
case MergeDialogColumns::Title:
return change.title();
case MergeDialogColumns::Uuid:
if (!change.uuid().isNull()) {
return change.uuid().toString();
}
break;
case MergeDialogColumns::Type:
return change.typeString();
case MergeDialogColumns::Details:
return change.details();
}
return {};
}
bool MergeDialog::isColumnHiddenByDefault(MergeDialogColumns column)
{
return column == MergeDialogColumns::Uuid;
}
void MergeDialog::setupChangeTable()
{
Q_ASSERT(m_ui);
Q_ASSERT(m_ui->changeTable);
m_ui->changeTable->verticalHeader()->setVisible(false);
m_ui->changeTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Interactive);
m_ui->changeTable->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu);
m_ui->changeTable->setShowGrid(false);
m_ui->changeTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_ui->changeTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_ui->changeTable->setSelectionMode(QAbstractItemView::SingleSelection);
// Create the header context menu
for (auto column : columns()) {
auto* action = new QAction(columnName(column), this);
action->setCheckable(true);
action->setChecked(!isColumnHiddenByDefault(column));
connect(action, &QAction::toggled, [this, column](bool checked) {
m_ui->changeTable->setColumnHidden(columnIndex(column), !checked);
m_ui->changeTable->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
});
m_ui->changeTable->horizontalHeader()->addAction(action);
}
}
void MergeDialog::updateChangeTable()
{
Q_ASSERT(m_ui);
Q_ASSERT(m_ui->changeTable);
Q_ASSERT(m_sourceDatabase.get());
Q_ASSERT(m_targetDatabase.get());
m_changes = Merger(m_sourceDatabase.data(), m_targetDatabase.get()).merge(true);
m_ui->changeTable->clear();
auto allColumns = columns();
m_ui->changeTable->setColumnCount(allColumns.size());
m_ui->changeTable->setRowCount(m_changes.size());
for (auto column : allColumns) {
auto name = columnName(column);
auto index = columnIndex(column);
m_ui->changeTable->setHorizontalHeaderItem(index, new QTableWidgetItem(name));
m_ui->changeTable->setColumnHidden(index, isColumnHiddenByDefault(column));
}
for (int row = 0; row < m_changes.size(); ++row) {
const auto& change = m_changes[row];
for (auto column : allColumns) {
m_ui->changeTable->setItem(row, columnIndex(column), new QTableWidgetItem(cellValue(change, column)));
}
}
m_ui->changeTable->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
}
void MergeDialog::performMerge()
{
auto changes = Merger(m_sourceDatabase.data(), m_targetDatabase.data()).merge();
if (changes != m_changes) {
qWarning("Merge results differed from the expected changes. Expected: %d, Actual: %d",
m_changes.size(),
changes.size());
}
emit databaseMerged(!changes.isEmpty());
done(QDialog::Accepted);
}
void MergeDialog::cancelMerge()
{
done(QDialog::Rejected);
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_MERGEDIALOG_H
#define KEEPASSX_MERGEDIALOG_H
#include "core/Merger.h"
#include <QDialog>
#include <QMenu>
namespace Ui
{
class MergeDialog;
}
class Database;
class MergeDialog : public QDialog
{
Q_OBJECT
public:
/**
* Merge source into copy of target and display changes.
* On user confirmation, merge source into target.
*/
explicit MergeDialog(QSharedPointer<Database> source, QSharedPointer<Database> target, QWidget* parent = nullptr);
/**
* Display given changes.
*/
explicit MergeDialog(const Merger::ChangeList& changes, QWidget* parent = nullptr);
~MergeDialog() override;
signals:
// Signal will be emitted when a normal merge operation has been performed.
void databaseMerged(bool databaseChanged);
private slots:
void performMerge();
void cancelMerge();
private:
enum class MergeDialogColumns
{
Group,
Title,
Uuid,
Type,
Details
};
static QVector<MergeDialogColumns> columns();
static int columnIndex(MergeDialogColumns column);
static QString columnName(MergeDialogColumns column);
static QString cellValue(const Merger::Change& change, MergeDialogColumns column);
static bool isColumnHiddenByDefault(MergeDialogColumns column);
void setupChangeTable();
void updateChangeTable();
QScopedPointer<Ui::MergeDialog> m_ui;
QScopedPointer<QMenu> m_headerContextMenu;
Merger::ChangeList m_changes;
QSharedPointer<Database> m_sourceDatabase;
QSharedPointer<Database> m_targetDatabase;
};
#endif // KEEPASSX_MERGEDIALOG_H

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MergeDialog</class>
<widget class="QWidget" name="MergeDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
<string>Database Merge Confirmation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableWidget" name="changeTable"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Abort|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -91,9 +91,7 @@ MessageBox::Button MessageBox::messageBox(QWidget* parent,
msgBox.setTextFormat(Qt::RichText);
msgBox.setIcon(icon);
msgBox.setWindowTitle(title);
// Replace newlines with HTML line breaks
auto fixedText = text;
msgBox.setText(fixedText.replace("\n", "<br>"));
msgBox.setText(text);
if (m_overrideParent) {
// Force the creation of the QWindow, without this windowHandle() will return nullptr

View File

@@ -65,7 +65,7 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
connect(m_ui->buttonApply, SIGNAL(clicked()), SLOT(applyPassword()));
connect(m_ui->buttonCopy, SIGNAL(clicked()), SLOT(copyPassword()));
connect(m_ui->buttonGenerate, SIGNAL(clicked()), SLOT(regeneratePassword()));
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(removeCustomWordList()));
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(deleteWordList()));
connect(m_ui->buttonAddWordList, SIGNAL(clicked()), SLOT(addWordList()));
connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed()));
@@ -115,11 +115,6 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
m_ui->comboBoxWordList->addItem(fileName, path.absolutePath() + QDir::separator() + fileName);
}
// Set color of wordlist warning
StateColorPalette statePalette;
auto color = statePalette.color(StateColorPalette::ColorRole::False);
m_ui->labelWordListWarning->setStyleSheet(QString("QLabel { color: %1; }").arg(color.name()));
loadSettings();
}
@@ -262,7 +257,9 @@ void PasswordGeneratorWidget::regeneratePassword()
m_ui->editNewPassword->setText(m_passwordGenerator->generatePassword());
}
} else {
m_ui->editNewPassword->setText(m_dicewareGenerator->generatePassphrase());
if (m_dicewareGenerator->isValid()) {
m_ui->editNewPassword->setText(m_dicewareGenerator->generatePassphrase());
}
}
}
@@ -382,28 +379,33 @@ bool PasswordGeneratorWidget::isPasswordGenerated() const
return m_passwordGenerated;
}
void PasswordGeneratorWidget::removeCustomWordList()
void PasswordGeneratorWidget::deleteWordList()
{
if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) {
return;
}
auto wordlist = m_ui->comboBoxWordList->currentText();
auto result = MessageBox::question(this,
tr("Confirm Remove Wordlist"),
tr("Do you really want to remove the wordlist \"%1\"?").arg(wordlist),
MessageBox::Remove | MessageBox::Cancel,
MessageBox::Cancel);
if (result == MessageBox::Remove) {
QFile file(m_ui->comboBoxWordList->currentData().toString());
if (file.exists() && !file.remove()) {
MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString());
}
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
updateGenerator();
QFile file(m_ui->comboBoxWordList->currentData().toString());
if (!file.exists()) {
return;
}
auto result = MessageBox::question(this,
tr("Confirm Delete Wordlist"),
tr("Do you really want to delete the wordlist \"%1\"?").arg(file.fileName()),
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
if (result != MessageBox::Delete) {
return;
}
if (!file.remove()) {
MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString());
return;
}
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
updateGenerator();
}
void PasswordGeneratorWidget::addWordList()
@@ -587,7 +589,11 @@ void PasswordGeneratorWidget::updateGenerator()
}
m_passwordGenerator->setFlags(flags);
m_ui->buttonGenerate->setEnabled(m_passwordGenerator->isValid());
if (m_passwordGenerator->isValid()) {
m_ui->buttonGenerate->setEnabled(true);
} else {
m_ui->buttonGenerate->setEnabled(false);
}
} else {
m_dicewareGenerator->setWordCase(
static_cast<PassphraseGenerator::PassphraseWordCase>(m_ui->wordCaseComboBox->currentData().toInt()));
@@ -604,8 +610,11 @@ void PasswordGeneratorWidget::updateGenerator()
m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text());
m_ui->labelWordListWarning->setVisible(!m_dicewareGenerator->isWordListValid());
m_ui->buttonGenerate->setEnabled(true);
if (m_dicewareGenerator->isValid()) {
m_ui->buttonGenerate->setEnabled(true);
} else {
m_ui->buttonGenerate->setEnabled(false);
}
}
regeneratePassword();

View File

@@ -67,7 +67,7 @@ public slots:
void applyPassword();
void copyPassword();
void setPasswordVisible(bool visible);
void removeCustomWordList();
void deleteWordList();
void addWordList();
protected:

View File

@@ -769,113 +769,7 @@ QProgressBar::chunk {
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Wordlist:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLineEdit" name="editWordSeparator">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordCount">
<property name="text">
<string>Word Count:</string>
</property>
<property name="buddy">
<cstring>spinBoxLength</cstring>
</property>
</widget>
</item>
<item row="4" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="wordCaseLabel">
<property name="text">
<string>Word Case:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDeleteWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Delete selected wordlist</string>
</property>
<property name="accessibleDescription">
<string>Delete selected wordlist</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Add custom wordlist</string>
</property>
<property name="accessibleDescription">
<string>Add custom wordlist</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
@@ -920,7 +814,61 @@ QProgressBar::chunk {
</item>
</layout>
</item>
<item row="4" column="2">
<item row="3" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="wordCaseLabel">
<property name="text">
<string>Word Case:</string>
</property>
</widget>
</item>
<item row="2" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordSeparator">
<property name="text">
<string>Word Separator:</string>
</property>
</widget>
</item>
<item row="0" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDeleteWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Delete selected wordlist</string>
</property>
<property name="accessibleDescription">
<string>Delete selected wordlist</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Add custom wordlist</string>
</property>
<property name="accessibleDescription">
<string>Add custom wordlist</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QComboBox" name="wordCaseComboBox"/>
@@ -940,23 +888,62 @@ QProgressBar::chunk {
</item>
</layout>
</item>
<item row="3" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordSeparator">
<item row="1" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordCount">
<property name="text">
<string>Word Separator:</string>
<string>Word Count:</string>
</property>
<property name="buddy">
<cstring>spinBoxLength</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="labelWordListWarning">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLineEdit" name="editWordSeparator">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Warning: the chosen wordlist is smaller than the minimum recommended size!</string>
<string>Wordlist:</string>
</property>
</widget>
</item>

View File

@@ -231,7 +231,7 @@ bool PasswordWidget::eventFilter(QObject* watched, QEvent* event)
if (isVisible() && (type == QEvent::KeyPress || type == QEvent::KeyRelease || type == QEvent::FocusIn)) {
checkCapslockState();
}
if (type == QEvent::FocusIn || type == QEvent::FocusOut || type == QEvent::Hide) {
if (type == QEvent::FocusIn || type == QEvent::FocusOut) {
osUtils->setUserInputProtection(type == QEvent::FocusIn);
}
}

View File

@@ -48,14 +48,12 @@ SearchWidget::SearchWidget(QWidget* parent)
new QShortcut(Qt::CTRL + Qt::Key_J, this, SLOT(toggleHelp()), nullptr, Qt::WidgetWithChildrenShortcut);
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(updateSaveButtonVisibility()));
connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp()));
connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu()));
connect(m_ui->saveIcon, &QAction::triggered, this, [this] { emit saveSearch(m_ui->searchEdit->text()); });
connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
connect(m_ui->searchEdit, &QLineEdit::returnPressed, this, &SearchWidget::onReturnPressed);
m_ui->searchEdit->setPlaceholderText(tr("Search (%1)…", "Search placeholder text, %1 is the keyboard shortcut")
.arg(QKeySequence(QKeySequence::Find).toString(QKeySequence::NativeText)));
@@ -71,12 +69,6 @@ SearchWidget::SearchWidget(QWidget* parent)
m_actionLimitGroup->setCheckable(true);
m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
m_actionWaitForEnter = m_searchMenu->addAction(
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::SearchWaitForEnter, state); });
m_actionWaitForEnter->setObjectName("actionSearchWaitForEnter");
m_actionWaitForEnter->setCheckable(true);
m_actionWaitForEnter->setChecked(config()->get(Config::SearchWaitForEnter).toBool());
m_ui->searchIcon->setIcon(icons()->icon("system-search"));
m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);
@@ -156,17 +148,17 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
mx.connect(SIGNAL(requestSearch(QString)), this, SLOT(performRequestedSearch(QString)));
mx.connect(SIGNAL(requestSearch(QString)), m_ui->searchEdit, SLOT(setText(QString)));
mx.connect(SIGNAL(clearSearch()), this, SLOT(clearSearch()));
mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(focusSearch()));
mx.connect(this, SIGNAL(enterPressed()), SLOT(switchToEntryEdit()));
mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
}
void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
{
if (dbWidget) {
if (dbWidget != nullptr) {
// Set current search text from this database
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
// Enforce search policy
@@ -179,15 +171,18 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
void SearchWidget::startSearchTimer()
{
if (m_actionWaitForEnter->isChecked()) {
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
} else {
m_searchTimer->start(500);
}
m_searchTimer->start(100);
}
void SearchWidget::startSearch()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
m_ui->saveIcon->setVisible(true);
search(m_ui->searchEdit->text());
}
@@ -205,18 +200,18 @@ void SearchWidget::updateCaseSensitive()
emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
}
void SearchWidget::updateLimitGroup()
{
config()->set(Config::SearchLimitGroup, m_actionLimitGroup->isChecked());
emit limitGroupChanged(m_actionLimitGroup->isChecked());
}
void SearchWidget::setCaseSensitive(bool state)
{
m_actionCaseSensitive->setChecked(state);
updateCaseSensitive();
}
void SearchWidget::updateLimitGroup()
{
config()->set(Config::SearchLimitGroup, m_actionLimitGroup->isChecked());
emit limitGroupChanged(m_actionLimitGroup->isChecked());
}
void SearchWidget::setLimitGroup(bool state)
{
m_actionLimitGroup->setChecked(state);
@@ -249,28 +244,3 @@ void SearchWidget::showSearchMenu()
{
m_searchMenu->exec(m_ui->searchEdit->mapToGlobal(m_ui->searchEdit->rect().bottomLeft()));
}
void SearchWidget::onReturnPressed()
{
if (m_actionWaitForEnter->isChecked()) {
m_ui->saveIcon->setVisible(true);
emit search(m_ui->searchEdit->text());
} else {
emit enterPressed();
}
}
void SearchWidget::performRequestedSearch(const QString& text)
{
// This method handles saved searches - it should set the text and immediately trigger search
// without any delay, regardless of the "Press Enter to search" setting
m_ui->searchEdit->setText(text);
m_ui->saveIcon->setVisible(!text.isEmpty());
emit search(text);
}
void SearchWidget::updateSaveButtonVisibility()
{
// Show save button whenever there's non-empty text in the search field
m_ui->saveIcon->setVisible(!m_ui->searchEdit->text().isEmpty());
}

View File

@@ -68,7 +68,6 @@ public slots:
void clearSearch();
private slots:
void onReturnPressed();
void startSearchTimer();
void startSearch();
void updateCaseSensitive();
@@ -76,8 +75,6 @@ private slots:
void toggleHelp();
void showSearchMenu();
void resetSearchClearTimer();
void performRequestedSearch(const QString& text);
void updateSaveButtonVisibility();
private:
const QScopedPointer<Ui::SearchWidget> m_ui;
@@ -86,7 +83,6 @@ private:
QTimer* m_clearSearchTimer;
QAction* m_actionCaseSensitive;
QAction* m_actionLimitGroup;
QAction* m_actionWaitForEnter;
QMenu* m_searchMenu;
};

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