Compare commits
33 Commits
fix/cli-sh
...
fix/cli-un
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a1982ee0 | ||
|
|
8d59090243 | ||
|
|
9e8a966c23 | ||
|
|
448165613d | ||
|
|
93423eda30 | ||
|
|
56b63a9e0f | ||
|
|
2c2b686593 | ||
|
|
7c2fd5e3e9 | ||
|
|
7ec0f1f5a8 | ||
|
|
76b2f377df | ||
|
|
20c65fbd1e | ||
|
|
74326616c5 | ||
|
|
634a5b34f1 | ||
|
|
8c7cc90363 | ||
|
|
217ee01572 | ||
|
|
7a5cd6105c | ||
|
|
64078933ab | ||
|
|
e5fbab38d8 | ||
|
|
e2cf37a91f | ||
|
|
f62ea95499 | ||
|
|
b5f4e98925 | ||
|
|
20aefd0c7a | ||
|
|
9baf77cbc4 | ||
|
|
5dfcc72f98 | ||
|
|
f2a4cc7e66 | ||
|
|
c4b4be48a5 | ||
|
|
2c3a1a03cb | ||
|
|
3c7c3b0a5f | ||
|
|
eac95df000 | ||
|
|
db0f091536 | ||
|
|
5cb6ad6335 | ||
|
|
f32ed71dfc | ||
|
|
8a32b3bc5e |
@@ -54,6 +54,7 @@ IncludeCategories:
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertNewlineAtEOF: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
|
||||
7
.github/CONTRIBUTING.md
vendored
@@ -15,6 +15,7 @@ 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)
|
||||
|
||||
@@ -74,6 +75,10 @@ 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.
|
||||
@@ -82,7 +87,7 @@ All pull requests must comply with the above requirements and with the [stylegui
|
||||
|
||||
### Translations
|
||||
|
||||
Translations are managed on [Transifex](https://www.transifex.com/keepassxc/keepassxc/) which offers a web interface.
|
||||
Translations are managed on [Transifex](https://explore.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:
|
||||
|
||||
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,12 +1,13 @@
|
||||
[NOTE]: # ( Describe your changes in detail, why is this change required? )
|
||||
[NOTE]: # ( Explain large or complex code modifications. )
|
||||
[NOTE]: # ( Describe your changes in detail. 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. )
|
||||
|
||||
38
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
29
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
1
COPYING
@@ -223,6 +223,7 @@ 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
|
||||
|
||||
@@ -56,6 +56,10 @@ 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).
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -36,6 +37,8 @@ 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=*]
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 48 KiB |
BIN
docs/images/browser_integration_additional_attribute.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/images/browser_integration_clear_sites.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 77 KiB |
BIN
docs/images/secretservice_access_dialog.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/images/secretservice_database_settings.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/images/secretservice_enable_settings.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
@@ -4,3 +4,4 @@ KeePassXC Team <team@keepassxc.org>
|
||||
:stylesheet: ../styles/dark.css
|
||||
:icons: font
|
||||
:toc: left
|
||||
:experimental:
|
||||
|
||||
@@ -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 `Tab` key, then types the password of the entry and finally presses the `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 kbd:[Tab] key, then types the password of the entry and finally presses the kbd:[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 `+` 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 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)*.
|
||||
+
|
||||
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 `{` or `}`, respectively
|
||||
|{LEFTBRACE}, {RIGHTBRACE} |Press kbd:[{] or kbd:[}], respectively
|
||||
|{<KEY> X} |Repeat <KEY> 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 _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 kbd:[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 `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 kbd:[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.
|
||||
|
||||
|
||||
@@ -107,12 +107,34 @@ 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 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.
|
||||
=== 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
|
||||
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
|
||||
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)*.
|
||||
|
||||
|
||||
@@ -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 _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 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.
|
||||
|
||||
.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 Ctrl+N). The following screen appears:
|
||||
1. Navigate to Entries > New Entry (or press kbd:[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 `Enter`, click the edit toolbar icon, or right-click and select Edit Entry from the menu.
|
||||
2. Press kbd:[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 `Delete` button on your keyboard.
|
||||
1. Select the entry you want to delete and press the kbd:[Del] 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 `Delete` 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 kbd:[Del] 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 `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 kbd:[Ctrl+K].
|
||||
+
|
||||
.Clone entry from context menu
|
||||
image::clone_entry.png[]
|
||||
|
||||
@@ -3,52 +3,62 @@ include::.sharedheader[]
|
||||
:imagesdir: ../images
|
||||
|
||||
// tag::content[]
|
||||
NOTE: On macOS please substitute `Ctrl` with `Cmd` (aka `⌘`).
|
||||
NOTE: On macOS please substitute kbd:[Ctrl] with kbd:[Cmd] (AKA kbd:[⌘]).
|
||||
|
||||
[grid=rows, frame=none, width=75%]
|
||||
|===
|
||||
|Action | Keyboard Shortcut
|
||||
|
||||
|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 + /
|
||||
|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]
|
||||
|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 + <n>], where kbd:[<n>] 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 + /]
|
||||
|===
|
||||
// end::content[]
|
||||
|
||||
@@ -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 (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.
|
||||
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.
|
||||
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 (Ctrl + R) to generate a new random passphrase.
|
||||
7. Click the Clipboard button (Ctrl + C) to copy the passphrase to the clipboard.
|
||||
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.
|
||||
// end::content[]
|
||||
|
||||
@@ -77,8 +77,8 @@ Examples: +
|
||||
|Press the corresponding keyboard key
|
||||
|
||||
|{UP}, {DOWN}, {LEFT}, {RIGHT} |Press the corresponding arrow key
|
||||
|{F1}, {F2}, ..., {F16} |Press F1, F2, etc.
|
||||
|{LEFTBRACE}, {RIGHTBRACE} |Press `{` or `}`, respectively
|
||||
|{F1}, {F2}, ..., {F16} |Press kbd:[F1], kbd:[F2], etc.
|
||||
|{LEFTBRACE}, {RIGHTBRACE} |Press kbd:[{] or kbd:[}], respectively
|
||||
|{<KEY> X} |Repeat <KEY> 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
|
||||
|
||||
|+ |SHIFT
|
||||
|^ |CTRL
|
||||
|% |ALT
|
||||
|# |WIN/CMD
|
||||
|+ |kbd:[Shift]
|
||||
|^ |kbd:[Ctrl]
|
||||
|% |kbd:[Alt]
|
||||
|# |kbd:[Win]/kbd:[Cmd]
|
||||
|===
|
||||
*Text Conversions:*
|
||||
|
||||
|
||||
48
docs/topics/SecretService.adoc
Normal file
@@ -0,0 +1,48 @@
|
||||
= 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[]
|
||||
@@ -18,7 +18,16 @@ 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: 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.
|
||||
[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"_.
|
||||
====
|
||||
|
||||
=== 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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 602 B |
@@ -92,6 +92,7 @@
|
||||
<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>
|
||||
|
||||
@@ -573,6 +573,10 @@
|
||||
<source>Font size selection</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Skip confirmation for main window Auto-Type actions</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ApplicationSettingsWidgetSecurity</name>
|
||||
@@ -663,6 +667,17 @@
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AttachmentWidget</name>
|
||||
<message>
|
||||
<source>Attachment Viewer</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unknown attachment type</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AutoType</name>
|
||||
<message>
|
||||
@@ -713,6 +728,10 @@
|
||||
<source>Invalid placeholder: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Entry has invalid TOTP settings</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AutoTypeAssociationsModel</name>
|
||||
@@ -781,15 +800,6 @@
|
||||
<source>Double click a row to perform Auto-Type or find an entry using the search:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><p>You can use advanced search queries to find any entry in your open databases. The following shortcuts are useful:<br/>
|
||||
Ctrl+F - Toggle database search<br/>
|
||||
Ctrl+1 - Type username<br/>
|
||||
Ctrl+2 - Type password<br/>
|
||||
Ctrl+3 - Type TOTP<br/>
|
||||
Ctrl+4 - Use Virtual Keyboard (Windows Only)</p></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Search all open databases</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -834,6 +844,24 @@ Ctrl+4 - Use Virtual Keyboard (Windows Only)</p></source>
|
||||
<source>Use Virtual Keyboard</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><p>You can use advanced search queries to find any entry in your open databases. The following shortcuts are useful:<br/>
|
||||
Ctrl+F - Toggle database search<br/>
|
||||
Ctrl+1 - Type username<br/>
|
||||
Ctrl+2 - Type password<br/>
|
||||
Ctrl+3 - Type TOTP<br/>
|
||||
Ctrl+4 - Type URL<br/>
|
||||
Ctrl+5 - Use Virtual Keyboard (Windows Only)</p></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Type {URL}</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Copy URL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserAccessControlDialog</name>
|
||||
@@ -2661,10 +2689,6 @@ This is definitely a bug, please report it to the developers.</source>
|
||||
<source>No Results</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enter a unique name or overwrite an existing search from the list:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -2833,6 +2857,17 @@ Disable safe saves and try again?</source>
|
||||
<source>Failed to save backup database: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Edit: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditEntryWidget</name>
|
||||
@@ -3904,21 +3939,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File name</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File contents...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsModel</name>
|
||||
<message>
|
||||
@@ -3944,10 +3964,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||
<source>Add new attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Add</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remove selected attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -3968,10 +3984,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||
<source>Save selected attachment to disk</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -4065,16 +4077,28 @@ Error: %1</source>
|
||||
Would you like to overwrite the existing attachment?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Preview</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to preview an attachment: Attachment not found</source>
|
||||
<source>Edit</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New Text Document</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Add file…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Load from Disk…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
@@ -4640,6 +4664,13 @@ You can enable the DuckDuckGo website icon service in the security section of th
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ImageAttachmentsWidget</name>
|
||||
<message>
|
||||
<source>Zoom:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ImportWizard</name>
|
||||
<message>
|
||||
@@ -6466,25 +6497,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Attachment name cannot be empty</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Attachment with the same name already exists</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New entry attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NixUtils</name>
|
||||
<message>
|
||||
@@ -7122,14 +7134,6 @@ The following data is missing:
|
||||
<comment>Password quality</comment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Confirm Delete Wordlist</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you really want to delete the wordlist "%1"?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to delete wordlist</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -7183,6 +7187,18 @@ Do you want to overwrite it?</source>
|
||||
<source>Excluded characters: "0", "1", "l", "I", "O", "|", "﹒", "B", "8", "G", "6"</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Warning: the chosen wordlist is smaller than the minimum recommended size!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Confirm Remove Wordlist</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you really want to remove the wordlist "%1"?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasswordWidget</name>
|
||||
@@ -7253,15 +7269,15 @@ Do you want to overwrite it?</source>
|
||||
<context>
|
||||
<name>PreviewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Preview entry attachment</source>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No preview available</source>
|
||||
<source>Preview: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image format not supported</source>
|
||||
<source>Save…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
@@ -9197,10 +9213,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Shortcut %1 conflicts with '%2'. Overwrite shortcut?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot generate valid passphrases because the wordlist is too short</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Encrypted files are not supported.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -9248,6 +9260,24 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Tags</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Warning: the chosen wordlist is smaller than the minimum recommended size!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Invalid Step</source>
|
||||
<comment>TOTP</comment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Invalid Digits</source>
|
||||
<comment>TOTP</comment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fit</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
@@ -9938,6 +9968,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Limit search to selected group</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press Enter to search</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsClientModel</name>
|
||||
@@ -10158,6 +10192,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Weak Passwords</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>TOTP Entries</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TagView</name>
|
||||
@@ -10178,6 +10216,24 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TextAttachmentsEditWidget</name>
|
||||
<message>
|
||||
<source>Preview</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TextAttachmentsPreviewWidget</name>
|
||||
<message>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Type:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TotpDialog</name>
|
||||
<message>
|
||||
@@ -10292,6 +10348,10 @@ Example: JBSWY3DPEHPK3PXP</source>
|
||||
<source>Are you sure you want to delete TOTP settings for this entry?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Error: secret key is invalid</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>URLEdit</name>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
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>
|
||||
|
||||
@@ -159,8 +159,14 @@ set(gui_SOURCES
|
||||
gui/entry/EntryAttachmentsModel.cpp
|
||||
gui/entry/EntryAttachmentsWidget.cpp
|
||||
gui/entry/EntryAttributesModel.cpp
|
||||
gui/entry/NewEntryAttachmentsDialog.cpp
|
||||
gui/entry/EditEntryAttachmentsDialog.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
|
||||
|
||||
@@ -637,11 +637,17 @@ 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 {};
|
||||
}
|
||||
} else if (placeholder.startsWith("pickchars")) {
|
||||
// Reset to the original capture to preserve case
|
||||
placeholder = match.captured(3);
|
||||
|
||||
@@ -37,6 +37,7 @@ enum MENU_FIELD
|
||||
USERNAME = 1,
|
||||
PASSWORD,
|
||||
TOTP,
|
||||
URL,
|
||||
};
|
||||
|
||||
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
|
||||
@@ -264,7 +265,8 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
|
||||
|
||||
bool hasUsername = !match.first->username().isEmpty();
|
||||
bool hasPassword = !match.first->password().isEmpty();
|
||||
bool hasTotp = match.first->hasTotp();
|
||||
bool hasTotp = match.first->hasValidTotp();
|
||||
bool hasUrl = !match.first->url().isEmpty();
|
||||
|
||||
for (auto action : m_actionMenu->actions()) {
|
||||
auto prop = action->property(MENU_FIELD_PROP_NAME);
|
||||
@@ -279,6 +281,9 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
|
||||
case MENU_FIELD::TOTP:
|
||||
action->setEnabled(hasTotp);
|
||||
break;
|
||||
case MENU_FIELD::URL:
|
||||
action->setEnabled(hasUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,15 +295,19 @@ 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);
|
||||
@@ -324,10 +333,18 @@ 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_4);
|
||||
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_5);
|
||||
connect(typeVirtualAction, &QAction::triggered, this, [&] {
|
||||
m_virtualMode = true;
|
||||
activateCurrentMatch();
|
||||
@@ -364,17 +381,29 @@ 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)
|
||||
|
||||
@@ -56,7 +56,8 @@ Ctrl+F - Toggle database search<br/>
|
||||
Ctrl+1 - Type username<br/>
|
||||
Ctrl+2 - Type password<br/>
|
||||
Ctrl+3 - Type TOTP<br/>
|
||||
Ctrl+4 - Use Virtual Keyboard (Windows Only)</p></string>
|
||||
Ctrl+4 - Type URL<br/>
|
||||
Ctrl+5 - Use Virtual Keyboard (Windows Only)</p></string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QToolButton {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* 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
|
||||
@@ -71,7 +71,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
|
||||
}
|
||||
|
||||
const auto authenticatorAttachment = credentialCreationOptions["authenticatorAttachment"];
|
||||
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toObject();
|
||||
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toString();
|
||||
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()->getBase64FromJson(clientDataJson);
|
||||
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataJson.toUtf8());
|
||||
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"].toObject();
|
||||
const auto clientDataArray = QJsonDocument(clientDataJson).toJson(QJsonDocument::Compact);
|
||||
const auto clientDataJson = assertionOptions["clientDataJson"].toString();
|
||||
const auto clientDataArray = clientDataJson.toUtf8();
|
||||
|
||||
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()->getBase64FromJson(clientDataJson);
|
||||
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
|
||||
responseObject["clientExtensionResults"] = assertionOptions["clientExtensionResults"];
|
||||
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
|
||||
responseObject["userHandle"] = userHandle;
|
||||
|
||||
@@ -330,6 +330,7 @@ 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;
|
||||
@@ -1191,7 +1192,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
|
||||
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
|
||||
res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
|
||||
|
||||
if (entry->hasTotp()) {
|
||||
if (entry->hasValidTotp()) {
|
||||
res["totp"] = entry->totp();
|
||||
}
|
||||
|
||||
@@ -1571,11 +1572,11 @@ bool BrowserService::handleURLWithWildcards(const QUrl& entryQUrl, const QString
|
||||
}
|
||||
|
||||
// Escape illegal characters
|
||||
auto re = firstPart.replace(QRegularExpression(R"(([!\^\$\+\-\(\)@<>]))"), "\\\\1");
|
||||
auto re = Tools::escapeRegex(firstPart);
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* 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
|
||||
@@ -53,8 +53,8 @@ bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialC
|
||||
{
|
||||
if (!credentialCreationOptions["attestation"].isString()
|
||||
|| credentialCreationOptions["attestation"].toString().isEmpty()
|
||||
|| !credentialCreationOptions["clientDataJSON"].isObject()
|
||||
|| credentialCreationOptions["clientDataJSON"].toObject().isEmpty()
|
||||
|| !credentialCreationOptions["clientDataJSON"].isString()
|
||||
|| credentialCreationOptions["clientDataJSON"].toString().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"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty()
|
||||
if (!assertionOptions["clientDataJson"].isString() || assertionOptions["clientDataJson"].toString().isEmpty()
|
||||
|| !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty()
|
||||
|| !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined()
|
||||
|| !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) {
|
||||
@@ -352,15 +352,11 @@ ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) c
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
|
||||
// Serialization order: https://w3c.github.io/webauthn/#clientdatajson-serialization
|
||||
QString PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
|
||||
{
|
||||
QJsonObject clientData;
|
||||
clientData["challenge"] = publicKey["challenge"];
|
||||
clientData["crossOrigin"] = false;
|
||||
clientData["origin"] = origin;
|
||||
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
|
||||
|
||||
return clientData;
|
||||
return QString("{\"type\":\"%1\",\"challenge\":\"%2\",\"origin\":\"%3\",\"crossOrigin\":false}")
|
||||
.arg((get ? QString("webauthn.get") : QString("webauthn.create")), publicKey["challenge"].toString(), origin);
|
||||
}
|
||||
|
||||
QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* 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
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const;
|
||||
bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const;
|
||||
ExtensionResult buildExtensionData(QJsonObject& extensionObject) const;
|
||||
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
|
||||
QString 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;
|
||||
|
||||
@@ -68,11 +68,9 @@ int Diceware::execute(const QStringList& arguments)
|
||||
dicewareGenerator.setWordList(wordListFile);
|
||||
}
|
||||
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
QString password = dicewareGenerator.generatePassphrase();
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Utils
|
||||
const QString& yubiKeySlot,
|
||||
bool quiet)
|
||||
{
|
||||
auto& err = quiet ? DEVNULL : STDERR;
|
||||
auto& err = STDERR;
|
||||
auto compositeKey = QSharedPointer<CompositeKey>::create();
|
||||
|
||||
QFileInfo dbFileInfo(databaseFilename);
|
||||
|
||||
@@ -147,6 +147,7 @@ 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}},
|
||||
|
||||
@@ -98,6 +98,7 @@ public:
|
||||
GUI_CompactMode,
|
||||
GUI_CheckForUpdates,
|
||||
GUI_CheckForUpdatesIncludeBetas,
|
||||
SearchWaitForEnter,
|
||||
GUI_ShowExpiredEntriesOnDatabaseUnlock,
|
||||
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
|
||||
GUI_FontSizeOffset,
|
||||
@@ -128,6 +129,7 @@ public:
|
||||
Security_HidePasswordPreviewPanel,
|
||||
Security_HideTotpPreviewPanel,
|
||||
Security_AutoTypeAsk,
|
||||
Security_AutoTypeSkipMainWindowConfirmation,
|
||||
Security_IconDownloadFallback,
|
||||
Security_NoConfirmMoveEntryToRecycleBin,
|
||||
Security_EnableCopyOnDoubleClick,
|
||||
|
||||
@@ -826,7 +826,12 @@ void Database::updateTagList()
|
||||
}
|
||||
|
||||
m_tagList = tagSet.values();
|
||||
m_tagList.sort();
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
std::sort(m_tagList.begin(), m_tagList.end(), collator);
|
||||
|
||||
emit tagListUpdated();
|
||||
}
|
||||
|
||||
|
||||
@@ -570,6 +570,12 @@ 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();
|
||||
@@ -581,10 +587,13 @@ void Entry::removePasskey()
|
||||
removeTag(tr("Passkey"));
|
||||
}
|
||||
|
||||
QString Entry::totp() const
|
||||
QString Entry::totp(bool* isValid) const
|
||||
{
|
||||
if (hasTotp()) {
|
||||
return Totp::generateTotp(m_data.totpSettings);
|
||||
return Totp::generateTotp(m_data.totpSettings, isValid);
|
||||
}
|
||||
if (isValid) {
|
||||
*isValid = false;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public:
|
||||
QString password() const;
|
||||
QString notes() const;
|
||||
QString attribute(const QString& key) const;
|
||||
QString totp() const;
|
||||
QString totp(bool* isValid = nullptr) const;
|
||||
QString totpSettingsString() const;
|
||||
QSharedPointer<Totp::Settings> totpSettings() const;
|
||||
Group* previousParentGroup();
|
||||
@@ -126,6 +126,7 @@ public:
|
||||
void removePasskey();
|
||||
|
||||
bool hasTotp() const;
|
||||
bool hasValidTotp() const;
|
||||
bool isExpired() const;
|
||||
bool willExpireInDays(int days) const;
|
||||
void expireNow();
|
||||
|
||||
@@ -221,6 +221,13 @@ 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;
|
||||
@@ -260,6 +267,7 @@ 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
|
||||
|
||||
@@ -41,6 +41,7 @@ public:
|
||||
Group,
|
||||
Tag,
|
||||
Is,
|
||||
Has,
|
||||
Uuid
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1142,6 +1142,24 @@ 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)) {
|
||||
|
||||
@@ -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,6 +94,7 @@ 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;
|
||||
|
||||
@@ -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(true);
|
||||
m_timer->setSingleShot(false);
|
||||
connect(m_timer, SIGNAL(timeout()), SLOT(timeout()));
|
||||
}
|
||||
|
||||
void InactivityTimer::setInactivityTimeout(int inactivityTimeout)
|
||||
{
|
||||
Q_ASSERT(inactivityTimeout > 0);
|
||||
|
||||
m_timer->setInterval(inactivityTimeout);
|
||||
}
|
||||
|
||||
void InactivityTimer::activate()
|
||||
void InactivityTimer::activate(int inactivityTimeout)
|
||||
{
|
||||
if (!m_active) {
|
||||
qApp->installEventFilter(this);
|
||||
}
|
||||
m_active = true;
|
||||
m_resetBlocked = false;
|
||||
m_timer->setInterval(qMax(MIN_TIMEOUT, inactivityTimeout));
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
@@ -54,12 +54,15 @@ void InactivityTimer::deactivate()
|
||||
|
||||
bool InactivityTimer::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
const QEvent::Type type = event->type();
|
||||
const auto type = event->type();
|
||||
// clang-format off
|
||||
if ((type >= QEvent::MouseButtonPress && type <= QEvent::KeyRelease)
|
||||
|| (type >= QEvent::HoverEnter && type <= QEvent::HoverMove)
|
||||
|| (type == QEvent::Wheel)) {
|
||||
if (!m_resetBlocked &&
|
||||
((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
|
||||
|
||||
@@ -73,7 +76,7 @@ void InactivityTimer::timeout()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_active && !m_timer->isActive()) {
|
||||
if (m_active) {
|
||||
emit inactivityDetected();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ class InactivityTimer : public QObject
|
||||
|
||||
public:
|
||||
explicit InactivityTimer(QObject* parent = nullptr);
|
||||
void setInactivityTimeout(int inactivityTimeout);
|
||||
void activate();
|
||||
void activate(int inactivityTimeout);
|
||||
void deactivate();
|
||||
|
||||
signals:
|
||||
@@ -44,7 +43,8 @@ private slots:
|
||||
|
||||
private:
|
||||
QTimer* m_timer;
|
||||
bool m_active;
|
||||
bool m_active = false;
|
||||
bool m_resetBlocked = false;
|
||||
QMutex m_emitMutx;
|
||||
};
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ void PassphraseGenerator::setWordList(const QString& path)
|
||||
|
||||
m_wordlist = wordset.toList();
|
||||
|
||||
if (m_wordlist.size() < m_minimum_wordlist_length) {
|
||||
if (!isWordListValid()) {
|
||||
qWarning("Wordlist is less than minimum acceptable size: %s", qPrintable(path));
|
||||
}
|
||||
}
|
||||
@@ -117,8 +117,7 @@ void PassphraseGenerator::setWordSeparator(const QString& separator)
|
||||
|
||||
QString PassphraseGenerator::generatePassphrase() const
|
||||
{
|
||||
// In case there was an error loading the wordlist
|
||||
if (!isValid() || m_wordlist.empty()) {
|
||||
if (m_wordlist.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -149,7 +148,7 @@ QString PassphraseGenerator::generatePassphrase() const
|
||||
return words.join(m_separator);
|
||||
}
|
||||
|
||||
bool PassphraseGenerator::isValid() const
|
||||
bool PassphraseGenerator::isWordListValid() const
|
||||
{
|
||||
return m_wordCount > 0 && m_wordlist.size() >= m_minimum_wordlist_length;
|
||||
return m_wordlist.size() >= m_minWordListSize;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
void setWordCase(PassphraseWordCase wordCase);
|
||||
void setDefaultWordList();
|
||||
void setWordSeparator(const QString& separator);
|
||||
bool isValid() const;
|
||||
bool isWordListValid() const;
|
||||
|
||||
QString generatePassphrase() const;
|
||||
|
||||
@@ -50,7 +50,7 @@ public:
|
||||
|
||||
private:
|
||||
int m_wordCount;
|
||||
int m_minimum_wordlist_length = 4000;
|
||||
int m_minWordListSize = 1296;
|
||||
PassphraseWordCase m_wordCase;
|
||||
QString m_separator;
|
||||
QList<QString> m_wordlist;
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <QIODevice>
|
||||
#include <QLocale>
|
||||
#include <QMetaProperty>
|
||||
#include <QMimeDatabase>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
@@ -478,29 +479,57 @@ namespace Tools
|
||||
|
||||
MimeType toMimeType(const QString& mimeName)
|
||||
{
|
||||
static QStringList textFormats = {
|
||||
"text/",
|
||||
const static QStringList TextFormats = {"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/soap+xml",
|
||||
"application/x-yaml",
|
||||
"application/protobuf",
|
||||
};
|
||||
static QStringList imageFormats = {"image/"};
|
||||
"application/x-zerosize"};
|
||||
const static QStringList HtmlFormats = {"text/html"};
|
||||
const static QStringList MarkdownFormats = {"text/markdown"};
|
||||
const 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, TextFormats)) {
|
||||
if (isCompatible(mimeName, HtmlFormats)) {
|
||||
return MimeType::Html;
|
||||
} else if (isCompatible(mimeName, MarkdownFormats)) {
|
||||
return MimeType::Markdown;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "core/Global.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QSet>
|
||||
@@ -119,10 +120,16 @@ 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
|
||||
|
||||
@@ -210,12 +210,33 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
|
||||
}
|
||||
}
|
||||
|
||||
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
|
||||
QString Totp::checkValidSettings(const QSharedPointer<Totp::Settings>& settings)
|
||||
{
|
||||
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;
|
||||
@@ -229,9 +250,6 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
|
||||
}
|
||||
|
||||
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
|
||||
if (secret.isNull()) {
|
||||
return QObject::tr("Invalid Key", "TOTP");
|
||||
}
|
||||
|
||||
QCryptographicHash::Algorithm cryptoHash;
|
||||
switch (settings->algorithm) {
|
||||
@@ -274,6 +292,9 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
|
||||
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
|
||||
password /= encoder.alphabet.size();
|
||||
}
|
||||
if (isValid) {
|
||||
*isValid = true;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,10 @@ namespace Totp
|
||||
const QString& title = {},
|
||||
const QString& username = {},
|
||||
bool forceOtp = false);
|
||||
|
||||
QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull);
|
||||
// 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);
|
||||
|
||||
bool hasCustomSettings(const QSharedPointer<Totp::Settings>& settings);
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
*/
|
||||
Argon2Kdf::Argon2Kdf(Type type)
|
||||
: Kdf::Kdf(type == Type::Argon2d ? KeePass2::KDF_ARGON2D : KeePass2::KDF_ARGON2ID)
|
||||
, m_version(0x13)
|
||||
, m_memory(1 << 16)
|
||||
, m_parallelism(static_cast<quint32>(QThread::idealThreadCount()))
|
||||
, m_version(ARGON2_DEFAULT_VERSION)
|
||||
, m_memory(ARGON2_DEFAULT_MEMORY)
|
||||
, m_parallelism(qMin<quint32>(QThread::idealThreadCount(), ARGON2_DEFAULT_PARALLELISM))
|
||||
{
|
||||
m_rounds = 10;
|
||||
m_rounds = ARGON2_DEFAULT_ROUNDS;
|
||||
}
|
||||
|
||||
quint32 Argon2Kdf::version() const
|
||||
@@ -52,7 +52,7 @@ bool Argon2Kdf::setVersion(quint32 version)
|
||||
m_version = version;
|
||||
return true;
|
||||
}
|
||||
m_version = 0x13;
|
||||
m_version = ARGON2_DEFAULT_VERSION;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ bool Argon2Kdf::setMemory(quint64 kibibytes)
|
||||
m_memory = kibibytes;
|
||||
return true;
|
||||
}
|
||||
m_memory = 16;
|
||||
m_memory = ARGON2_DEFAULT_MEMORY;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ bool Argon2Kdf::setParallelism(quint32 threads)
|
||||
m_parallelism = threads;
|
||||
return true;
|
||||
}
|
||||
m_parallelism = 1;
|
||||
m_parallelism = ARGON2_DEFAULT_PARALLELISM;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
|
||||
#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:
|
||||
@@ -47,6 +52,15 @@ 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;
|
||||
|
||||
@@ -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->hasTotp()) {
|
||||
if (m_backend->hasValidTotp()) {
|
||||
attrs[ItemAttributes::TotpKey] = m_backend->totp();
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -73,7 +73,13 @@ namespace
|
||||
}
|
||||
|
||||
if (loginMap.contains("itemEmail")) {
|
||||
entry->attributes()->set("login_email", loginMap.value("itemEmail").toString());
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the entry url(s)
|
||||
|
||||
@@ -129,6 +129,8 @@ 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)));
|
||||
@@ -302,6 +304,9 @@ 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()) {
|
||||
@@ -445,6 +450,8 @@ 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()) {
|
||||
@@ -618,6 +625,11 @@ 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 =
|
||||
|
||||
@@ -63,6 +63,7 @@ private slots:
|
||||
void rememberDatabasesToggled(bool checked);
|
||||
void checkUpdatesToggled(bool checked);
|
||||
void showExpiredEntriesOnDatabaseUnlockToggled(bool checked);
|
||||
void autoTypeAskToggled(bool checked);
|
||||
void selectBackupDirectory();
|
||||
|
||||
private:
|
||||
|
||||
@@ -1157,6 +1157,42 @@
|
||||
</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">
|
||||
|
||||
@@ -877,13 +877,19 @@ 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 (config()->get(Config::Security_AutoTypeAsk).toBool()
|
||||
&& MessageBox::question(
|
||||
if (MessageBox::question(
|
||||
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
|
||||
!= MessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sequence.isEmpty()) {
|
||||
autoType()->performAutoType(currentEntry);
|
||||
@@ -918,6 +924,16 @@ 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();
|
||||
@@ -1527,7 +1543,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
|
||||
}
|
||||
break;
|
||||
case EntryModel::Totp:
|
||||
if (entry->hasTotp()) {
|
||||
if (entry->hasValidTotp()) {
|
||||
setClipboardTextAndMinimize(entry->totp());
|
||||
} else {
|
||||
setupTotp();
|
||||
@@ -2386,7 +2402,7 @@ bool DatabaseWidget::currentEntryHasTotp()
|
||||
if (!currentEntry) {
|
||||
return false;
|
||||
}
|
||||
return currentEntry->hasTotp();
|
||||
return currentEntry->hasValidTotp();
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_SSHAGENT
|
||||
|
||||
@@ -214,6 +214,8 @@ public slots:
|
||||
void performAutoTypePassword();
|
||||
void performAutoTypePasswordEnter();
|
||||
void performAutoTypeTOTP();
|
||||
void performAutoTypeURL();
|
||||
void performAutoTypeURLEnter();
|
||||
void setClipboardTextAndMinimize(const QString& text);
|
||||
void openUrl();
|
||||
void downloadSelectedFavicons();
|
||||
|
||||
@@ -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() == widget) {
|
||||
if (scrollArea && (scrollArea == widget || scrollArea->widget() == widget)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,7 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
|
||||
|
||||
m_ui->entryTotpLabel->installEventFilter(this);
|
||||
|
||||
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->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotp, 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)));
|
||||
@@ -95,6 +94,8 @@ 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();
|
||||
});
|
||||
@@ -259,9 +260,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->entryTotpLabel->hide();
|
||||
m_ui->entryTotpProgress->hide();
|
||||
m_ui->entryTotp->hide();
|
||||
m_ui->entryTotpButton->setChecked(false);
|
||||
m_ui->entryTotpLabel->clear();
|
||||
m_totpTimer.stop();
|
||||
@@ -546,16 +547,23 @@ void EntryPreviewWidget::updateGroupSharingTab()
|
||||
void EntryPreviewWidget::updateTotpLabel()
|
||||
{
|
||||
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
|
||||
auto totpCode = m_currentEntry->totp();
|
||||
bool isValid = false;
|
||||
auto totpCode = m_currentEntry->totp(&isValid);
|
||||
if (isValid) {
|
||||
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->entryTotpLabel->clear();
|
||||
m_totpTimer.stop();
|
||||
}
|
||||
|
||||
m_ui->entryTotpProgress->setVisible(isValid);
|
||||
m_ui->entryTotpLabel->setText(totpCode);
|
||||
} else {
|
||||
m_ui->entryTotp->setVisible(false);
|
||||
m_totpTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,16 +110,34 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<widget class="QWidget" name="entryTotp" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</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>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
@@ -130,7 +148,7 @@
|
||||
<string notr="true">1234567</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
<set>Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -151,6 +169,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="entryTotpButton">
|
||||
|
||||
@@ -38,7 +38,6 @@ 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);
|
||||
|
||||
@@ -172,6 +172,8 @@ 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) {
|
||||
@@ -273,7 +275,7 @@ MainWindow::MainWindow()
|
||||
m_ui->actionAllowScreenCapture->setVisible(osUtils->canPreventScreenCapture());
|
||||
|
||||
m_inactivityTimer = new InactivityTimer(this);
|
||||
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
|
||||
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockAllDatabases()));
|
||||
applySettingsChanges();
|
||||
|
||||
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
|
||||
@@ -387,6 +389,8 @@ 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"));
|
||||
@@ -526,6 +530,9 @@ 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
|
||||
@@ -821,6 +828,8 @@ 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
|
||||
@@ -831,31 +840,30 @@ void MainWindow::updateSetTagsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Add known database tags as actions and set checked if
|
||||
// a selected entry has that tag
|
||||
// Remove missing tags
|
||||
const auto tagList = dbWidget->database()->tagList();
|
||||
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);
|
||||
for (const auto action : m_ui->menuTags->actions()) {
|
||||
if (!tagList.contains(action->text()) || !action->isEnabled()) {
|
||||
delete action;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove missing tags
|
||||
for (const auto action : m_ui->menuTags->actions()) {
|
||||
if (!tagList.contains(action->text())) {
|
||||
action->deleteLater();
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -975,6 +983,8 @@ 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());
|
||||
@@ -1016,7 +1026,7 @@ void MainWindow::updateMenuActionState()
|
||||
m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && groupHasEntries && !inRecycleBin);
|
||||
|
||||
// Database Menu
|
||||
m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave());
|
||||
m_ui->actionDatabaseSave->setEnabled(databaseUnlocked && m_ui->tabWidget->canSave());
|
||||
m_ui->actionDatabaseSaveAs->setEnabled(databaseUnlocked);
|
||||
m_ui->actionDatabaseSaveBackup->setEnabled(databaseUnlocked);
|
||||
m_ui->actionDatabaseClose->setEnabled(dbWidget);
|
||||
@@ -1314,6 +1324,11 @@ 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)
|
||||
@@ -1650,14 +1665,9 @@ 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()) {
|
||||
m_inactivityTimer->activate();
|
||||
auto timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000;
|
||||
m_inactivityTimer->activate(timeout);
|
||||
} else {
|
||||
m_inactivityTimer->deactivate();
|
||||
}
|
||||
@@ -1827,13 +1837,6 @@ void MainWindow::closeModalWindow()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::lockDatabasesAfterInactivity()
|
||||
{
|
||||
if (!m_ui->tabWidget->lockDatabases()) {
|
||||
m_inactivityTimer->activate();
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::isTrayIconEnabled() const
|
||||
{
|
||||
return m_trayIcon && m_trayIcon->isVisible();
|
||||
@@ -1888,7 +1891,7 @@ void MainWindow::bringToFront()
|
||||
void MainWindow::handleScreenLock()
|
||||
{
|
||||
if (config()->get(Config::Security_LockDatabaseScreenLock).toBool()) {
|
||||
lockDatabasesAfterInactivity();
|
||||
lockAllDatabases();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1938,7 +1941,7 @@ void MainWindow::closeAllDatabases()
|
||||
|
||||
void MainWindow::lockAllDatabases()
|
||||
{
|
||||
lockDatabasesAfterInactivity();
|
||||
m_ui->tabWidget->lockDatabases();
|
||||
}
|
||||
|
||||
void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint)
|
||||
|
||||
@@ -140,7 +140,6 @@ private slots:
|
||||
void applySettingsChanges();
|
||||
void trayIconTriggered(QSystemTrayIcon::ActivationReason reason);
|
||||
void processTrayIconTrigger();
|
||||
void lockDatabasesAfterInactivity();
|
||||
void handleScreenLock();
|
||||
void showErrorMessage(const QString& message);
|
||||
void selectNextDatabaseTab();
|
||||
|
||||
@@ -859,6 +859,34 @@
|
||||
<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 &Favicon</string>
|
||||
|
||||
@@ -91,7 +91,9 @@ MessageBox::Button MessageBox::messageBox(QWidget* parent,
|
||||
msgBox.setTextFormat(Qt::RichText);
|
||||
msgBox.setIcon(icon);
|
||||
msgBox.setWindowTitle(title);
|
||||
msgBox.setText(text);
|
||||
// Replace newlines with HTML line breaks
|
||||
auto fixedText = text;
|
||||
msgBox.setText(fixedText.replace("\n", "<br>"));
|
||||
|
||||
if (m_overrideParent) {
|
||||
// Force the creation of the QWindow, without this windowHandle() will return nullptr
|
||||
|
||||
@@ -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(deleteWordList()));
|
||||
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(removeCustomWordList()));
|
||||
connect(m_ui->buttonAddWordList, SIGNAL(clicked()), SLOT(addWordList()));
|
||||
connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed()));
|
||||
|
||||
@@ -115,6 +115,11 @@ 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();
|
||||
}
|
||||
|
||||
@@ -257,10 +262,8 @@ void PasswordGeneratorWidget::regeneratePassword()
|
||||
m_ui->editNewPassword->setText(m_passwordGenerator->generatePassword());
|
||||
}
|
||||
} else {
|
||||
if (m_dicewareGenerator->isValid()) {
|
||||
m_ui->editNewPassword->setText(m_dicewareGenerator->generatePassphrase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password)
|
||||
@@ -379,33 +382,28 @@ bool PasswordGeneratorWidget::isPasswordGenerated() const
|
||||
return m_passwordGenerated;
|
||||
}
|
||||
|
||||
void PasswordGeneratorWidget::deleteWordList()
|
||||
void PasswordGeneratorWidget::removeCustomWordList()
|
||||
{
|
||||
if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(m_ui->comboBoxWordList->currentData().toString());
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto wordlist = m_ui->comboBoxWordList->currentText();
|
||||
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,
|
||||
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::Delete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.remove()) {
|
||||
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());
|
||||
return;
|
||||
}
|
||||
|
||||
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
|
||||
updateGenerator();
|
||||
}
|
||||
}
|
||||
|
||||
void PasswordGeneratorWidget::addWordList()
|
||||
@@ -589,11 +587,7 @@ void PasswordGeneratorWidget::updateGenerator()
|
||||
}
|
||||
m_passwordGenerator->setFlags(flags);
|
||||
|
||||
if (m_passwordGenerator->isValid()) {
|
||||
m_ui->buttonGenerate->setEnabled(true);
|
||||
} else {
|
||||
m_ui->buttonGenerate->setEnabled(false);
|
||||
}
|
||||
m_ui->buttonGenerate->setEnabled(m_passwordGenerator->isValid());
|
||||
} else {
|
||||
m_dicewareGenerator->setWordCase(
|
||||
static_cast<PassphraseGenerator::PassphraseWordCase>(m_ui->wordCaseComboBox->currentData().toInt()));
|
||||
@@ -610,11 +604,8 @@ void PasswordGeneratorWidget::updateGenerator()
|
||||
|
||||
m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text());
|
||||
|
||||
if (m_dicewareGenerator->isValid()) {
|
||||
m_ui->labelWordListWarning->setVisible(!m_dicewareGenerator->isWordListValid());
|
||||
m_ui->buttonGenerate->setEnabled(true);
|
||||
} else {
|
||||
m_ui->buttonGenerate->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
regeneratePassword();
|
||||
|
||||
@@ -67,7 +67,7 @@ public slots:
|
||||
void applyPassword();
|
||||
void copyPassword();
|
||||
void setPasswordVisible(bool visible);
|
||||
void deleteWordList();
|
||||
void removeCustomWordList();
|
||||
void addWordList();
|
||||
|
||||
protected:
|
||||
|
||||
@@ -769,7 +769,113 @@ 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>
|
||||
@@ -814,61 +920,7 @@ QProgressBar::chunk {
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<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">
|
||||
<item row="4" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QComboBox" name="wordCaseComboBox"/>
|
||||
@@ -888,62 +940,23 @@ QProgressBar::chunk {
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="1" alignment="Qt::AlignRight">
|
||||
<widget class="QLabel" name="labelWordCount">
|
||||
<item row="3" column="1" alignment="Qt::AlignRight">
|
||||
<widget class="QLabel" name="labelWordSeparator">
|
||||
<property name="text">
|
||||
<string>Word Count:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>spinBoxLength</cstring>
|
||||
<string>Word Separator:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="labelWordListWarning">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</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>Wordlist:</string>
|
||||
<string>Warning: the chosen wordlist is smaller than the minimum recommended size!</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -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) {
|
||||
if (type == QEvent::FocusIn || type == QEvent::FocusOut || type == QEvent::Hide) {
|
||||
osUtils->setUserInputProtection(type == QEvent::FocusIn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ SearchWidget::SearchWidget(QWidget* parent)
|
||||
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)));
|
||||
@@ -69,6 +70,12 @@ 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);
|
||||
|
||||
@@ -153,12 +160,12 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
||||
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(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
|
||||
mx.connect(this, SIGNAL(enterPressed()), SLOT(switchToEntryEdit()));
|
||||
}
|
||||
|
||||
void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
|
||||
{
|
||||
if (dbWidget != nullptr) {
|
||||
if (dbWidget) {
|
||||
// Set current search text from this database
|
||||
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
|
||||
// Enforce search policy
|
||||
@@ -171,18 +178,15 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
|
||||
|
||||
void SearchWidget::startSearchTimer()
|
||||
{
|
||||
if (!m_searchTimer->isActive()) {
|
||||
if (m_actionWaitForEnter->isChecked()) {
|
||||
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());
|
||||
}
|
||||
@@ -200,18 +204,18 @@ void SearchWidget::updateCaseSensitive()
|
||||
emit caseSensitiveChanged(m_actionCaseSensitive->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::setCaseSensitive(bool state)
|
||||
{
|
||||
m_actionCaseSensitive->setChecked(state);
|
||||
updateCaseSensitive();
|
||||
}
|
||||
|
||||
void SearchWidget::setLimitGroup(bool state)
|
||||
{
|
||||
m_actionLimitGroup->setChecked(state);
|
||||
@@ -244,3 +248,12 @@ void SearchWidget::showSearchMenu()
|
||||
{
|
||||
m_searchMenu->exec(m_ui->searchEdit->mapToGlobal(m_ui->searchEdit->rect().bottomLeft()));
|
||||
}
|
||||
|
||||
void SearchWidget::onReturnPressed()
|
||||
{
|
||||
if (m_actionWaitForEnter->isChecked()) {
|
||||
emit search(m_ui->searchEdit->text());
|
||||
} else {
|
||||
emit enterPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ public slots:
|
||||
void clearSearch();
|
||||
|
||||
private slots:
|
||||
void onReturnPressed();
|
||||
void startSearchTimer();
|
||||
void startSearch();
|
||||
void updateCaseSensitive();
|
||||
@@ -83,6 +84,7 @@ private:
|
||||
QTimer* m_clearSearchTimer;
|
||||
QAction* m_actionCaseSensitive;
|
||||
QAction* m_actionLimitGroup;
|
||||
QAction* m_actionWaitForEnter;
|
||||
QMenu* m_searchMenu;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
|
||||
m_step = m_entry->totpSettings()->step;
|
||||
resetCounter();
|
||||
updateProgressBar();
|
||||
updateSeconds();
|
||||
|
||||
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
|
||||
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
|
||||
@@ -88,10 +89,15 @@ void TotpDialog::updateSeconds()
|
||||
|
||||
void TotpDialog::updateTotp()
|
||||
{
|
||||
QString totpCode = m_entry->totp();
|
||||
QString firstHalf = totpCode.left(totpCode.size() / 2);
|
||||
QString secondHalf = totpCode.mid(totpCode.size() / 2);
|
||||
m_ui->totpLabel->setText(firstHalf + " " + secondHalf);
|
||||
bool isValid = false;
|
||||
QString totpCode = m_entry->totp(&isValid);
|
||||
if (isValid) {
|
||||
totpCode.insert(totpCode.size() / 2, " ");
|
||||
}
|
||||
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid);
|
||||
m_ui->progressBar->setVisible(isValid);
|
||||
m_ui->timerLabel->setVisible(isValid);
|
||||
m_ui->totpLabel->setText(totpCode);
|
||||
}
|
||||
|
||||
void TotpDialog::resetCounter()
|
||||
|
||||
@@ -108,6 +108,7 @@ void TotpSetupDialog::init()
|
||||
m_ui->algorithmComboBox->addItem(item.first, item.second);
|
||||
}
|
||||
m_ui->algorithmComboBox->setCurrentIndex(0);
|
||||
m_ui->invalidKeyLabel->setVisible(false);
|
||||
|
||||
// Read entry totp settings
|
||||
auto settings = m_entry->totpSettings();
|
||||
@@ -127,5 +128,8 @@ void TotpSetupDialog::init()
|
||||
m_ui->algorithmComboBox->setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
auto error = Totp::checkValidSettings(settings);
|
||||
m_ui->invalidKeyLabel->setVisible(!error.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,22 @@
|
||||
<string>Setup TOTP</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="invalidKeyLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Error: secret key is invalid</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="leftMargin">
|
||||
@@ -210,6 +226,7 @@
|
||||
<zorder>customSettingsGroup</zorder>
|
||||
<zorder>buttonBox</zorder>
|
||||
<zorder>groupBox</zorder>
|
||||
<zorder>invalidKeyLabel</zorder>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>seedEdit</tabstop>
|
||||
|
||||
@@ -171,8 +171,8 @@ bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const
|
||||
url.remove(0, 1);
|
||||
url.remove(url.length() - 1, 1);
|
||||
} else {
|
||||
// Do not allow URL with just wildcards, or double wildcards, or no separator (.)
|
||||
if (url.length() == url.count("*") || url.contains("**") || url.contains("*.*") || !url.contains(".")) {
|
||||
// Do not allow URL with just wildcards, or double wildcards
|
||||
if (url.length() == url.count("*") || url.contains("**") || url.contains("*.*")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ DatabaseSettingsDialog::~DatabaseSettingsDialog() = default;
|
||||
|
||||
void DatabaseSettingsDialog::load(const QSharedPointer<Database>& db)
|
||||
{
|
||||
// Default to the main page on load
|
||||
setCurrentPage(0);
|
||||
setHeadline(tr("Database Settings: %1").arg(db->canonicalFilePath()));
|
||||
|
||||
m_generalWidget->loadSettings(db);
|
||||
|
||||
@@ -159,12 +159,7 @@ void DatabaseSettingsWidgetEncryption::initialize()
|
||||
// Set up KDF algorithms
|
||||
loadKdfAlgorithms();
|
||||
|
||||
// Perform Benchmark if requested
|
||||
if (isNewDatabase) {
|
||||
if (IS_ARGON2(m_ui->kdfComboBox->currentData())) {
|
||||
m_ui->memorySpinBox->setValue(16);
|
||||
m_ui->parallelismSpinBox->setValue(2);
|
||||
}
|
||||
benchmarkTransformRounds();
|
||||
}
|
||||
|
||||
@@ -225,7 +220,7 @@ void DatabaseSettingsWidgetEncryption::loadKdfParameters()
|
||||
// Set Argon2 parameters
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
m_ui->transformRoundsSpinBox->setValue(argon2Kdf->rounds());
|
||||
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory()) / (1 << 10));
|
||||
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(argon2Kdf->memory()));
|
||||
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||
} else if (!dbIsArgon2 && !kdfIsArgon2) {
|
||||
// Set AES KDF parameters
|
||||
@@ -233,8 +228,8 @@ void DatabaseSettingsWidgetEncryption::loadKdfParameters()
|
||||
} else {
|
||||
// Set reasonable defaults and then benchmark
|
||||
if (kdfIsArgon2) {
|
||||
m_ui->memorySpinBox->setValue(16);
|
||||
m_ui->parallelismSpinBox->setValue(2);
|
||||
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(ARGON2_DEFAULT_MEMORY));
|
||||
m_ui->parallelismSpinBox->setValue(ARGON2_DEFAULT_PARALLELISM);
|
||||
}
|
||||
benchmarkTransformRounds();
|
||||
}
|
||||
@@ -343,7 +338,7 @@ bool DatabaseSettingsWidgetEncryption::saveSettings()
|
||||
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
||||
if (IS_ARGON2(kdf->uuid())) {
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10));
|
||||
argon2Kdf->setMemory(Argon2Kdf::toKibibytes(m_ui->memorySpinBox->value()));
|
||||
argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()));
|
||||
}
|
||||
|
||||
@@ -377,8 +372,8 @@ void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs)
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
// Set a small static number of rounds for the benchmark
|
||||
argon2Kdf->setRounds(4);
|
||||
if (!argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10))) {
|
||||
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory() / (1 << 10)));
|
||||
if (!argon2Kdf->setMemory(Argon2Kdf::toKibibytes(m_ui->memorySpinBox->value()))) {
|
||||
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(argon2Kdf->memory()));
|
||||
}
|
||||
if (!argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()))) {
|
||||
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||
|
||||
52
src/gui/entry/EditEntryAttachmentsDialog.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 "EditEntryAttachmentsDialog.h"
|
||||
#include "ui_EditEntryAttachmentsDialog.h"
|
||||
|
||||
#include <core/EntryAttachments.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QMimeDatabase>
|
||||
#include <QPushButton>
|
||||
|
||||
EditEntryAttachmentsDialog::EditEntryAttachmentsDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::EditEntryAttachmentsDialog)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
||||
connect(m_ui->dialogButtons, &QDialogButtonBox::accepted, this, &EditEntryAttachmentsDialog::accept);
|
||||
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &EditEntryAttachmentsDialog::reject);
|
||||
}
|
||||
|
||||
EditEntryAttachmentsDialog::~EditEntryAttachmentsDialog() = default;
|
||||
|
||||
void EditEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
|
||||
{
|
||||
setWindowTitle(tr("Edit: %1").arg(attachment.name));
|
||||
|
||||
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadWrite);
|
||||
}
|
||||
|
||||
attachments::Attachment EditEntryAttachmentsDialog::getAttachment() const
|
||||
{
|
||||
return m_ui->attachmentWidget->getAttachment();
|
||||
}
|
||||
@@ -17,32 +17,29 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "attachments/AttachmentTypes.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
class EditEntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class QByteArray;
|
||||
class EntryAttachments;
|
||||
|
||||
class NewEntryAttachmentsDialog : public QDialog
|
||||
class EditEntryAttachmentsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent = nullptr);
|
||||
~NewEntryAttachmentsDialog() override;
|
||||
explicit EditEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||
~EditEntryAttachmentsDialog() override;
|
||||
|
||||
private slots:
|
||||
void saveAttachment();
|
||||
void fileNameTextChanged(const QString& fileName);
|
||||
void setAttachment(attachments::Attachment attachment);
|
||||
attachments::Attachment getAttachment() const;
|
||||
|
||||
private:
|
||||
bool validateFileName(const QString& fileName, QString& error) const;
|
||||
|
||||
QPointer<EntryAttachments> m_attachments;
|
||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
||||
QScopedPointer<Ui::EditEntryAttachmentsDialog> m_ui;
|
||||
};
|
||||
46
src/gui/entry/EditEntryAttachmentsDialog.ui
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>EditEntryAttachmentsDialog</class>
|
||||
<widget class="QDialog" name="EditEntryAttachmentsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>447</width>
|
||||
<height>424</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AttachmentWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>EntryAttachmentsDialog</class>
|
||||
<widget class="QDialog" name="EntryAttachmentsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>402</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="titleEdit">
|
||||
<property name="placeholderText">
|
||||
<string>File name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="errorLabel">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #FF9696</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="attachmentTextEdit">
|
||||
<property name="placeholderText">
|
||||
<string>File contents...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -130,6 +130,15 @@ QString EntryAttachmentsModel::keyByIndex(const QModelIndex& index) const
|
||||
return m_entryAttachments->keys().at(index.row());
|
||||
}
|
||||
|
||||
int EntryAttachmentsModel::rowByKey(const QString& key) const
|
||||
{
|
||||
if (!m_entryAttachments) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return m_entryAttachments->keys().indexOf(key);
|
||||
}
|
||||
|
||||
void EntryAttachmentsModel::attachmentChange(const QString& key)
|
||||
{
|
||||
int row = m_entryAttachments->keys().indexOf(key);
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
QString keyByIndex(const QModelIndex& index) const;
|
||||
int rowByKey(const QString& key) const;
|
||||
|
||||
private slots:
|
||||
void attachmentChange(const QString& key);
|
||||
|
||||
@@ -17,23 +17,41 @@
|
||||
|
||||
#include "EntryAttachmentsWidget.h"
|
||||
|
||||
#include "EditEntryAttachmentsDialog.h"
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "NewEntryAttachmentsDialog.h"
|
||||
#include "PreviewEntryAttachmentsDialog.h"
|
||||
#include "ui_EntryAttachmentsWidget.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDropEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "core/EntryAttachments.h"
|
||||
#include "core/Tools.h"
|
||||
#include "gui/FileDialog.h"
|
||||
#include "gui/MessageBox.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr const char* DefaultName = "New Attachment";
|
||||
constexpr const char* Suffix = ".txt";
|
||||
|
||||
QString generateUniqueName(const QString& name, const QStringList& existingNames)
|
||||
{
|
||||
uint64_t i = 0;
|
||||
QString newName = QStringLiteral("%1%2").arg(name).arg(Suffix);
|
||||
while (existingNames.contains(newName)) {
|
||||
newName = QStringLiteral("%1_%2%3").arg(name).arg(++i).arg(Suffix);
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_ui(new Ui::EntryAttachmentsWidget)
|
||||
@@ -67,14 +85,30 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||
// clang-format on
|
||||
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
|
||||
|
||||
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(previewSelectedAttachment()));
|
||||
connect(m_ui->attachmentsView, &QAbstractItemView::doubleClicked, [this](const QModelIndex&) {
|
||||
m_readOnly ? previewSelectedAttachment() : editSelectedAttachment();
|
||||
});
|
||||
|
||||
connect(m_ui->attachmentsView->itemDelegate(), &QAbstractItemDelegate::commitData, [this](QWidget* editor) {
|
||||
if (auto lineEdit = qobject_cast<QLineEdit*>(editor)) {
|
||||
auto index = m_attachmentsModel->rowByKey(lineEdit->text());
|
||||
m_ui->attachmentsView->setCurrentIndex(m_attachmentsModel->index(index, 0));
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
|
||||
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
|
||||
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
|
||||
connect(m_ui->newAttachmentButton, SIGNAL(clicked()), SLOT(newAttachments()));
|
||||
connect(m_ui->editAttachmentButton, SIGNAL(clicked()), SLOT(editSelectedAttachment()));
|
||||
connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment()));
|
||||
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
|
||||
|
||||
auto addButtonMenu = new QMenu(this);
|
||||
addButtonMenu->addAction(tr("New Text Document"), this, &EntryAttachmentsWidget::newAttachments);
|
||||
addButtonMenu->addAction(tr("Load from Disk…"), this, QOverload<>::of(&EntryAttachmentsWidget::insertAttachments));
|
||||
|
||||
m_ui->addAttachmentButton->setMenu(addButtonMenu);
|
||||
|
||||
updateButtonsVisible();
|
||||
updateButtonsEnabled();
|
||||
}
|
||||
@@ -175,19 +209,34 @@ void EntryAttachmentsWidget::newAttachments()
|
||||
return;
|
||||
}
|
||||
|
||||
NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this);
|
||||
if (newEntryDialog.exec() == QDialog::Accepted) {
|
||||
emit widgetUpdated();
|
||||
}
|
||||
// Create a temporary file to allow the user to edit the attachment
|
||||
auto newFileName = generateUniqueName(DefaultName, m_entryAttachments->keys());
|
||||
m_entryAttachments->set(newFileName, QByteArray());
|
||||
|
||||
auto currentIndex = m_attachmentsModel->index(m_attachmentsModel->rowByKey(newFileName), 0);
|
||||
m_ui->attachmentsView->setCurrentIndex(currentIndex);
|
||||
m_ui->attachmentsView->edit(currentIndex);
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
|
||||
const auto index = m_ui->attachmentsView->selectionModel()->selectedIndexes().first();
|
||||
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||
if (!selectionModel) {
|
||||
qWarning() << "Failed to preview an attachment: No selection model";
|
||||
return;
|
||||
}
|
||||
|
||||
auto indexes = selectionModel->selectedIndexes();
|
||||
if (indexes.empty()) {
|
||||
qWarning() << "Failed to edit an attachment: No attachment selected";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = indexes.first();
|
||||
if (!index.isValid()) {
|
||||
qWarning() << tr("Failed to preview an attachment: Attachment not found");
|
||||
qWarning() << "Failed to preview an attachment: Attachment not found";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,7 +247,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||
auto data = m_entryAttachments->value(name);
|
||||
|
||||
PreviewEntryAttachmentsDialog previewDialog(this);
|
||||
previewDialog.setAttachment(name, data);
|
||||
previewDialog.setAttachment({name, data});
|
||||
|
||||
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
|
||||
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
|
||||
@@ -208,7 +257,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||
&previewDialog,
|
||||
[&previewDialog, &name, this](const QString& key) {
|
||||
if (key == name) {
|
||||
previewDialog.setAttachment(name, m_entryAttachments->value(name));
|
||||
previewDialog.setAttachment({name, m_entryAttachments->value(name)});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -218,6 +267,51 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::editSelectedAttachment()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
|
||||
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||
if (!selectionModel) {
|
||||
qWarning() << "Failed to edit an attachment: No selection model";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto selectedIndexes = selectionModel->selectedIndexes();
|
||||
if (selectedIndexes.isEmpty()) {
|
||||
qWarning() << "Failed to edit an attachment: No attachment selected";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = selectedIndexes.first();
|
||||
|
||||
if (!index.isValid()) {
|
||||
qWarning() << "Failed to edit an attachment: Attachment not found";
|
||||
return;
|
||||
}
|
||||
|
||||
// Set selection to the first
|
||||
m_ui->attachmentsView->setCurrentIndex(index);
|
||||
|
||||
auto name = m_attachmentsModel->keyByIndex(index);
|
||||
auto data = m_entryAttachments->value(name);
|
||||
|
||||
EditEntryAttachmentsDialog editDialog(this);
|
||||
editDialog.setAttachment({name, data});
|
||||
|
||||
if (editDialog.exec() == QDialog::Accepted) {
|
||||
auto attachment = editDialog.getAttachment();
|
||||
|
||||
// Edit dialog cannot change the name of the attachment
|
||||
if (attachment.name == name) {
|
||||
m_entryAttachments->set(attachment.name, attachment.data);
|
||||
}
|
||||
}
|
||||
|
||||
// Set focus back to the widget to allow keyboard navigation
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::removeSelectedAttachments()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
@@ -346,35 +440,38 @@ void EntryAttachmentsWidget::openSelectedAttachments()
|
||||
|
||||
void EntryAttachmentsWidget::updateButtonsEnabled()
|
||||
{
|
||||
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
|
||||
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||
const bool hasSelection = selectionModel && selectionModel->hasSelection();
|
||||
|
||||
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
|
||||
m_ui->newAttachmentButton->setEnabled(!m_readOnly);
|
||||
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||
|
||||
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||
if (const auto indexes = selectionModel ? selectionModel->selectedIndexes() : QModelIndexList{}; !indexes.empty()) {
|
||||
auto mimeType = Tools::getMimeType(m_entryAttachments->value(m_attachmentsModel->keyByIndex(indexes.first())));
|
||||
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly && Tools::isTextMimeType(mimeType));
|
||||
}
|
||||
|
||||
m_ui->saveAttachmentButton->setEnabled(hasSelection);
|
||||
m_ui->previewAttachmentButton->setEnabled(hasSelection);
|
||||
m_ui->openAttachmentButton->setEnabled(hasSelection);
|
||||
|
||||
updateSpacers();
|
||||
updateLinesVisibility();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateSpacers()
|
||||
void EntryAttachmentsWidget::updateLinesVisibility()
|
||||
{
|
||||
if (m_buttonsVisible && !m_readOnly) {
|
||||
m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding);
|
||||
} else {
|
||||
m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
}
|
||||
m_ui->editPreviewLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->previewRemoveLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateButtonsVisible()
|
||||
{
|
||||
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->newAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->editAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
|
||||
updateSpacers();
|
||||
updateLinesVisibility();
|
||||
}
|
||||
|
||||
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
|
||||
|
||||
@@ -58,6 +58,7 @@ signals:
|
||||
private slots:
|
||||
void insertAttachments();
|
||||
void newAttachments();
|
||||
void editSelectedAttachment();
|
||||
void previewSelectedAttachment();
|
||||
void removeSelectedAttachments();
|
||||
void saveSelectedAttachments();
|
||||
@@ -68,7 +69,7 @@ private slots:
|
||||
void attachmentModifiedExternally(const QString& key, const QString& filePath);
|
||||
|
||||
private:
|
||||
void updateSpacers();
|
||||
void updateLinesVisibility();
|
||||
|
||||
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>337</width>
|
||||
<height>258</height>
|
||||
<width>332</width>
|
||||
<height>312</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -47,7 +47,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="actionsWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0,0,0,1,0,0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0,0,0,0,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@@ -60,16 +60,6 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="newAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addAttachmentButton">
|
||||
<property name="enabled">
|
||||
@@ -79,22 +69,26 @@
|
||||
<string>Add new attachment</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
<string>Add file…</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="previewVSpacer">
|
||||
<widget class="QPushButton" name="editAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="editPreviewLine">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="previewAttachmentButton">
|
||||
@@ -128,22 +122,19 @@
|
||||
<string>Save selected attachment to disk</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
<string>Save…</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<widget class="Line" name="previewRemoveLine">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>173</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeAttachmentButton">
|
||||
@@ -164,7 +155,7 @@
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -297,7 +297,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
|
||||
break;
|
||||
case Totp:
|
||||
if (entry->hasTotp()) {
|
||||
return icons()->icon("totp");
|
||||
return entry->hasValidTotp() ? icons()->icon("totp") : icons()->icon("totp-invalid");
|
||||
}
|
||||
break;
|
||||
case PasswordStrength:
|
||||
|
||||
@@ -1,90 +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 "NewEntryAttachmentsDialog.h"
|
||||
#include "core/EntryAttachments.h"
|
||||
#include "ui_EntryAttachmentsDialog.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
NewEntryAttachmentsDialog::NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_attachments(std::move(attachments))
|
||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
||||
{
|
||||
Q_ASSERT(m_attachments);
|
||||
|
||||
m_ui->setupUi(this);
|
||||
|
||||
setWindowTitle(tr("New entry attachment"));
|
||||
|
||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
||||
connect(m_ui->dialogButtons, SIGNAL(accepted()), this, SLOT(saveAttachment()));
|
||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
connect(m_ui->titleEdit, SIGNAL(textChanged(const QString&)), this, SLOT(fileNameTextChanged(const QString&)));
|
||||
|
||||
fileNameTextChanged(m_ui->titleEdit->text());
|
||||
}
|
||||
|
||||
NewEntryAttachmentsDialog::~NewEntryAttachmentsDialog() = default;
|
||||
|
||||
bool NewEntryAttachmentsDialog::validateFileName(const QString& fileName, QString& error) const
|
||||
{
|
||||
if (fileName.isEmpty()) {
|
||||
error = tr("Attachment name cannot be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_attachments->hasKey(fileName)) {
|
||||
error = tr("Attachment with the same name already exists");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NewEntryAttachmentsDialog::saveAttachment()
|
||||
{
|
||||
auto fileName = m_ui->titleEdit->text();
|
||||
auto text = m_ui->attachmentTextEdit->toPlainText().toUtf8();
|
||||
|
||||
QString error;
|
||||
if (!validateFileName(fileName, error)) {
|
||||
QMessageBox::warning(this, tr("Save attachment"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
m_attachments->set(fileName, text);
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void NewEntryAttachmentsDialog::fileNameTextChanged(const QString& fileName)
|
||||
{
|
||||
QString error;
|
||||
bool valid = validateFileName(fileName, error);
|
||||
|
||||
m_ui->errorLabel->setText(error);
|
||||
m_ui->errorLabel->setVisible(!valid);
|
||||
|
||||
auto okButton = m_ui->dialogButtons->button(QDialogButtonBox::Ok);
|
||||
if (okButton) {
|
||||
okButton->setDisabled(!valid);
|
||||
}
|
||||
}
|
||||
@@ -16,117 +16,48 @@
|
||||
*/
|
||||
|
||||
#include "PreviewEntryAttachmentsDialog.h"
|
||||
#include "ui_EntryAttachmentsDialog.h"
|
||||
#include "ui_PreviewEntryAttachmentsDialog.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QMimeDatabase>
|
||||
#include <QPushButton>
|
||||
#include <QTextCursor>
|
||||
#include <QtDebug>
|
||||
|
||||
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
||||
, m_ui(new Ui::PreviewEntryAttachmentsDialog)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
setWindowTitle(tr("Preview entry attachment"));
|
||||
// Disable the help button in the title bar
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
// Set to read-only
|
||||
m_ui->titleEdit->setReadOnly(true);
|
||||
m_ui->attachmentTextEdit->setReadOnly(true);
|
||||
m_ui->errorLabel->setVisible(false);
|
||||
|
||||
// Initialize dialog buttons
|
||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
|
||||
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
|
||||
closeButton->setDefault(true);
|
||||
|
||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
auto saveButton = m_ui->dialogButtons->button(QDialogButtonBox::Save);
|
||||
saveButton->setText(tr("Save…"));
|
||||
|
||||
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &PreviewEntryAttachmentsDialog::reject);
|
||||
connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
|
||||
auto pressedButton = m_ui->dialogButtons->standardButton(button);
|
||||
|
||||
const auto attachment = m_ui->attachmentWidget->getAttachment();
|
||||
if (pressedButton == QDialogButtonBox::Open) {
|
||||
emit openAttachment(m_name);
|
||||
emit openAttachment(attachment.name);
|
||||
} else if (pressedButton == QDialogButtonBox::Save) {
|
||||
emit saveAttachment(m_name);
|
||||
emit saveAttachment(attachment.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
|
||||
|
||||
void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data)
|
||||
void PreviewEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
|
||||
{
|
||||
m_name = name;
|
||||
m_ui->titleEdit->setText(m_name);
|
||||
setWindowTitle(tr("Preview: %1").arg(attachment.name));
|
||||
|
||||
m_type = attachmentType(data);
|
||||
m_data = data;
|
||||
m_imageCache = QImage();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::update()
|
||||
{
|
||||
if (m_type == Tools::MimeType::Unknown) {
|
||||
updateTextAttachment(tr("No preview available").toUtf8());
|
||||
} else if (m_type == Tools::MimeType::Image) {
|
||||
updateImageAttachment(m_data);
|
||||
} else if (m_type == Tools::MimeType::PlainText) {
|
||||
updateTextAttachment(m_data);
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::updateTextAttachment(const QByteArray& data)
|
||||
{
|
||||
m_ui->attachmentTextEdit->setPlainText(QString::fromUtf8(data));
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QByteArray& data)
|
||||
{
|
||||
if (m_imageCache.isNull() && !m_imageCache.loadFromData(data)) {
|
||||
updateTextAttachment(tr("Image format not supported").toUtf8());
|
||||
return;
|
||||
}
|
||||
|
||||
updateImageAttachment(m_imageCache);
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QImage& image)
|
||||
{
|
||||
m_ui->attachmentTextEdit->clear();
|
||||
auto cursor = m_ui->attachmentTextEdit->textCursor();
|
||||
|
||||
cursor.insertImage(image.scaled(calculateImageSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
QSize PreviewEntryAttachmentsDialog::calculateImageSize()
|
||||
{
|
||||
// Scale the image to the contents rect minus another set of margins to avoid scrollbars
|
||||
auto margins = m_ui->attachmentTextEdit->contentsMargins();
|
||||
auto size = m_ui->attachmentTextEdit->contentsRect().size();
|
||||
size.setWidth(size.width() - margins.left() - margins.right());
|
||||
size.setHeight(size.height() - margins.top() - margins.bottom());
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
Tools::MimeType PreviewEntryAttachmentsDialog::attachmentType(const QByteArray& data) const
|
||||
{
|
||||
QMimeDatabase mimeDb{};
|
||||
const auto mime = mimeDb.mimeTypeForData(data);
|
||||
|
||||
return Tools::toMimeType(mime.name());
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QDialog::resizeEvent(event);
|
||||
|
||||
if (m_type == Tools::MimeType::Image) {
|
||||
update();
|
||||
}
|
||||
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadOnly);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/Tools.h"
|
||||
#include "attachments/AttachmentTypes.h"
|
||||
|
||||
#include <core/Tools.h>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
class PreviewEntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class PreviewEntryAttachmentsDialog : public QDialog
|
||||
@@ -35,29 +37,12 @@ public:
|
||||
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||
~PreviewEntryAttachmentsDialog() override;
|
||||
|
||||
void setAttachment(const QString& name, const QByteArray& data);
|
||||
void setAttachment(attachments::Attachment attachment);
|
||||
|
||||
signals:
|
||||
void openAttachment(const QString& name);
|
||||
void saveAttachment(const QString& name);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
Tools::MimeType attachmentType(const QByteArray& data) const;
|
||||
|
||||
void update();
|
||||
void updateTextAttachment(const QByteArray& data);
|
||||
void updateImageAttachment(const QByteArray& data);
|
||||
void updateImageAttachment(const QImage& data);
|
||||
|
||||
QSize calculateImageSize();
|
||||
|
||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
||||
|
||||
QString m_name;
|
||||
QByteArray m_data;
|
||||
QImage m_imageCache;
|
||||
Tools::MimeType m_type{Tools::MimeType::Unknown};
|
||||
QScopedPointer<Ui::PreviewEntryAttachmentsDialog> m_ui;
|
||||
};
|
||||
|
||||
58
src/gui/entry/PreviewEntryAttachmentsDialog.ui
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PreviewEntryAttachmentsDialog</class>
|
||||
<widget class="QDialog" name="PreviewEntryAttachmentsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>557</width>
|
||||
<height>454</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
|
||||
<property name="leftMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AttachmentWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
37
src/gui/entry/attachments/AttachmentTypes.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
namespace attachments
|
||||
{
|
||||
struct Attachment
|
||||
{
|
||||
QString name;
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
enum class OpenMode
|
||||
{
|
||||
ReadOnly,
|
||||
ReadWrite
|
||||
};
|
||||
|
||||
} // namespace attachments
|
||||