Repository: keepassxreboot/keepassxc (C++)

Issue:
"Maximum depth of replacement has been reached" from unknown placeholder (curly bracket pairs in passwords/notes/...)
## Expected Behavior
No errors printed in the console

## Current Behavior
Several error messages like this are shown:
```
Maximum depth of replacement has been reached. Entry uuid: <REDACTED>
```

## Possible Solution
Need brainstorming :) Maybe the same unknown placeholder shouldn't be processed more than once?

## Steps to Reproduce (for bugs)
1. Create an entry with password "{foo}"
2. Click on another entry and click the just created entry

## Context
Several long generated passwords contain the pattern {foo}, leading to quite a few error messages in the console, which is somewhat annoying

## Debug Info
KeePassXC - 版本 2.3.1-snapshot
Build Type: Snapshot
修訂：46e8e3d

函式庫：
- Qt 5.10.1
- libgcrypt 1.8.2

作業系統：Arch Linux
處裡器架構：x86_64
核心：linux 4.15.8-1-ARCH

已啟用的擴充元件：
- Auto-Type

BTW, I don't remember I got such a message when I was reporting https://github.com/keepassxreboot/keepassxc/issues/1137
Reference in Note causes crash
## Overview
One reference in a note of an entry works fine.
Adding another reference in this note causes KeePassXC to crash:
"unknown software exception" (0x40000015) appeared at position 0x00007FFFD68D4F5.

## Steps to Reproduce
Attached are 2 keepass DBs: "Ref1.kdbx" and "Ref2.kdbx" - no password.
1. Open "Ref1.kdbx"
2. Edit note of Ref1 / Bank / B1.
  - It's note contains a reference to note Ref1 / Bank / C1:  "{REF:N@I:ABD457C6174F7C4B9B70FA53C73FB694}"
3. The 2nd reference is prepared but not active "{ REF:N@I:ABD4"...
4. Delete the blank past the second "{" - making the second refrence become active.
5. Update entry
6. Waiting circle icon appears - exception windows shows up (after some ten seconds).

## Expected Behavior
Save the entry and DB.

## Actual Behavior
Exception windows shows up (after some ten seconds).

## Context
See attached DBs
- "Ref1.kdbx" may be edited to cause the exception
- "Ref2.kdbx" cannot be opened and causes the exception

[NOTE]: # ( Paste debug info from Help → About here )
KeePassXC - Version 2.6.6 (using the portable version)
Revision: 9c108b9

Qt 5.15.2
Diagnosemodus ist deaktiviert.

Betriebssystem: Windows 10 Version 2009
CPU-Architektur: x86_64
Kernel: winnt 10.0.22000

Aktivierte Erweiterungen:
- Auto-Type
- Browser-Integration
- SSH-Agent
- KeeShare (bestätigtes und unbestätigtes Teilen)
- YubiKey

Kryptographische Bibliotheken:
- libgcrypt 1.9.2-unknown


[NOTE]: # ( Pick choices based on your environment )
Operating System: Windows 11

[Ref1_2.zip](https://github.com/keepassxreboot/keepassxc/files/7589781/Ref1_2.zip)





Discussion: This part of the code will be refactored in 2.4, I will keep the issue open to remember about it
This one actually freezed my OS, I was unable to control it unless rebooted. This is the boot log that caused system freeze:

```
May 18 17:35:58 p0laroid keepassxc[4982]: QObject::startTimer: Timers cannot have negative intervals
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:14 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
May 18 17:36:19 p0laroid keepassxc[4982]: Maximum depth of replacement has been reached. Entry uuid:
```
I didn't paste the `uuid`'s as well as I don't think that's ok...
@dminca do some of your entries/fields have a strange pattern of `{}`?
@TheZ3ro yes they have. My db has also been opened with KeeWeb and that one had the option to add references to usernames. I think that's the issue, I gotta hunt the entries down and replace them one by one.
@dminca If you can, paste some example of those reference so I can reproduce your problem (obviously remove sensisitive data)
Sure, no problem.

This is a username reference:

```sh
{REF:U@I:F268D4F0E0247219A14F9A525474001D}
```

This is a password reference:

```sh
{REF:P@I:E0251A47FF33684BBE4E19113A12FF7F}
```
I think I have the same problem. I am new to keepassxc and before I used keepass2 with firefox and the addon "Kee". In every entry, which I added from firefox, are fields with {...}.

e.g.:
```
Name: KPRPC JSON
{"version":1,"formFieldList":[{"name":"data[Account][password]","displayName":"KeePass password","value":"{PASSWORD}","type":"FFTpassword","id":"AccountPassword","page":1},{"name":"data[Account][username]","displayName":"KeePass username","value":"{USERNAME}","type":"FFTusername","id":"AccountUsername","page":1}],"alwaysAutoFill":false,"neverAutoFill":false,"alwaysAutoSubmit":false,"neverAutoSubmit":false,"priority":0,"altURLs":[],"hide":false,"blockHostnameOnlyMatch":false,"blockDomainOnlyMatch":false}
```

Maybe that helps.
Hi,
I also add the error message "Maximum depth of replacement has been reached" reported above even if my entries do not contains {}.

This message disappeared, but I still have this one:
```
$ keepassxc
QObject::startTimer: Timers cannot have negative intervals
$
```
For me, this error only occurred on entries that had "Auto-Type:" or "Auto-Type-Window:" lines in the notes. Once I eliminated these from those entries, the errors stopped. It had nothing to do with username or password entries.
I'm still seeing this with the latest Linux AppImage (2.4.3). I do have a bunch of entries that have JSON in the comments field. The annoying part is that the search box clears after a second or so, while spamming the terminal with `Maximum depth of replacement has been reached. Entry uuid: {<some uuid>}` for several different uuids.
I'm seeing the same message `Maximum depth of replacement has been reached. Entry uuid: {<uuid>}` in `journalctl` for one specific uuid. It's not causing me any noticeable problems. I haven't figured out the cause though.
Same problem here with keepassxc 2.5.2. It is due to an entry with json. 

My password or my Notes contain something like that : 
> '{"http-basic":{"repo.packagist.com":{"username":"fdsf","password":"ffsfdsf"}}}'

Is there a workaround ? 
Don't store json in the notes. Put it in a text atrachment.
> Don't store json in the notes. Put it in a text atrachment.

Thanks for your answer. It could be an answer but I am using "keepassxc" with [chezmoi](https://github.com/twpayne/chezmoi) and it seems that we can't retrieve attachement from keepassxc-cli (cf. https://github.com/keepassxreboot/keepassxc/pull/5538).
Some other useful tips to workaround this issue:

* To find out passwords that may trigger this warning, you can search with regex (ex: `*password:".*\{.*\}.*"`)
~~I created a script to find all passwords that may trigger this warning. To use it, export the database as XML via a command like `keepassxc-cli export --key-file foo.key foo.kdbx > foo.xml` (**note that the output file contains passwords in plaintexts**), and run `python issue1741-workaround.py foo.xml`~~

<details>
<summary>
<del>issue1741-workaround.py</del>
</summary>

```Python
import re
import sys
import textwrap
import xml.etree.ElementTree as ET

def main():
    # Find passwords that may cause unwanted warnings
    # https://github.com/keepassxreboot/keepassxc/issues/1741
    dom = ET.parse(sys.argv[1])
    for group in dom.findall('.//Group'):
        for entry in group.findall('./Entry'):
            attrs = {}
            for attr in entry.findall('./String'):
                key = attr.find('Key').text
                value = attr.find('Value').text
                if not value:
                    continue
                attrs[key] = value
            if re.match(r'\{.*\}', attrs.get('Password', '')):
                print(textwrap.dedent(f'''
                    Title: {attrs.get('Title') or '(None)'}
                    Username: {attrs.get('UserName') or '(None)'}
                    Password: {attrs['Password']}
                '''))

if __name__ == '__main__':
    main()
```

</details>

* If you're as lucky as me and control the code that consumes JSON credentials, you can also use JSON lists instead JSON objects. For example,
```JSON
[["aws", "token1"],["cloudflare","token2"]]
```
instead of
```JSON
{"aws":"token1","cloudflare":"token2"}
```

* In Password Generator, there is a useful field "Do not include". I put curly brackets in that field `{}`, so future passwords will never trigger this warning
It is a nice coincidence that this very KeePassXC Wiki has entries which look like JSON and trigger the error messages...

Is there a list of forbidden character combinations in Notes? 

```
https://github.com/keepassxreboot/keepassxc.wiki.git

...

Special Key	Code
+	{+}
%	{%}
^	{^}
~	{~}
(, )	{(}, {)}
[, ]	{[}, {]}
{, }	{{}, {}}

...
```

Those codes are for Auto-Type 
I needed to put JSON into the Password field, and now the logs are flooded with this Error Message.
would be nice if the Password Field is ignored by any replacement Logic.
KeepassXC: 2.6.6
> Don't store json in the notes. Put it in a text atrachment.

Stop storing json in notes is a workaround, not a solution. 

Users should be able to store any text in notes. We need to solve the issue instead of giving a workaround. 
I've also been experiencing this issue. I discovered that deleting everything in the Keyring folder solved the issue. Maybe the autogenerated entries in there created the right conditions for this error?
Thanks for the hint @xlash123. When searching for certain keywords, Keepassxc would freeze for about 30 seconds and show the following messages on the terminal:

```
...
Maximum depth of replacement has been reached. Entry uuid: {__SOME_UUID__}
```
When the freeze ends:
```
Failed to read proxy message:  "garbage at the end of the document"
```
  Deleting the entry with the corresponding UUID (which had the title `...Production.RETAIL.User...`) solved my issue.
I recommend you store JSON as file attachments instead of notes.
Why is JSON parsed at all? KPXC should not care what the content of the fields is, JSON or otherwise. If it is needed for REF handling, pass it through a regex first, to make sure it is actually a reference and not JSON data.

Having JSON in fields makes sense in some use cases, particularly passwords stored through Secret Service, where a client application may want to store structured data in some of its secrets.
If you want to store structured text fields, you should be using individual string fields for that. For anything else, use custom data.
The Secret Service API does not support multiple (protected) string fields or custom data.
https://specifications.freedesktop.org/secret-service/latest/ch14.html#type-Secret
https://specifications.freedesktop.org/secret-service/latest/re02.html#org.freedesktop.Secret.Collection.CreateItem 
There is only one value field in the `Secret` struct. It does support multiple additional attributes, but those are only intended for lookups. They're not meant to store data, let alone data that should be protected (keep in mind that KPXC isn't the only Secret Service provider; calls made by client applications would be the same as for other providers).

The only spec-compatible way to store structured protected data is either shove it all in the single value field (which I think goes to Password in KPXC), or use multiple entries. This is up to each client application to decide, and is entirely out of the user's control. It is quite likely that some applications will choose to use the structured value approach. I think I recall Chromium doing something like that?

Note also the `content_type` field of the `Secret` struct: this may very well be set to "application/json".

And regardless, KPXC should really not try to parse arbitrary JSON, no matter what is the source or use case of it.
We do not parse JSON. We only detect a reference if the text matches a pretty strict reference pattern.

https://github.com/keepassxreboot/keepassxc/blob/61f922179b8ef1446cc85385e391c83f86f204ce/src/core/Entry.cpp#L1141

```
QRegularExpressionMatch EntryAttributes::matchReference(const QString& text)
{
    static QRegularExpression referenceRegExp(
        "\\{REF:(?<WantedField>[TUPANI])@(?<SearchIn>[TUPANIO]):(?<SearchText>[^}]+)\\}",
        QRegularExpression::CaseInsensitiveOption);


    return referenceRegExp.match(text);
}
```
This can happen even without Notes and without JSON anywhere. I just started using keepassxc. With a brand-new install (from the Debian sid repo). I created a database, added two test entries with generated passwords and no Notes, then noticed my terminal was full of these lines, and they just kept coming as long as keepassxc was running. Every time I changed desktop back to the desktop with the keepassxc window, I'd notice several more "Maximum depth" lines on the terminal.

But I can't reproduce it. I removed ~/.config/Passwords.kdbx and ~/.config/keepassxc and started over, following the exact same steps, and this time I didn't see anything on the terminal except "QWidget::setWindowModified: The window title does not contain a '[*]' placeholder" (which happens repeatably when clicking Done on the password dialog for creating the database). So I don't know what was causing the "Maximum depth" errors.

Is it possible that they're triggered by something in a generated password, and that's why they only happen sometimes? I didn't save the generated passwords from that first run, unfortunately.
Ooooh possible that a generated password could cause it, especially if you use curly braces in the password. 
I mentioned a tip earlier (https://github.com/keepassxreboot/keepassxc/issues/1741#issuecomment-917577331)

> In Password Generator, there is a useful field "Do not include". I put curly brackets in that field `{}`, so future passwords will never trigger this warning

Maybe the password generator can be changed to avoid such passwords by default.
Nah, the better fix is to overhaul the references code. It is quite primitive and wasteful right now. It also doesn't need to even throw these warnings to the console unless in debug mode since they are meaningless. 
Ah, ignore my naive comment :)

Looking into reference/placeholder codes, this pull request might be an issue: https://github.com/keepassxreboot/keepassxc/pull/1651. It allows `resolvePlaceholder` to handle placeholders not at the begin or end, but unknown placeholders are also resolved recursively (and fail). As `resolveMultiplePlaceholders` can already resolve placeholders not at the begin or end, how about reverting that change and replace `resolvePlaceholder` usage with `resolveMultiplePlaceholders`?
Same issue here: `keepassxc-cli show -sa password passwords.kdbx skype/microsoft-account` and a password containing curly braces produces `Maximum depth of replacement has been reached. Entry uuid:[redacted]` and then prints the password. This does not happen with simpler passwords. This luckily does not affect piping stdout to `| qrencode -t utf8`.
i also see this flooding many dozens to hundreds of times (maybe more) in console, and searching for the relevent code I see two possible matches:

- https://github.com/keepassxreboot/keepassxc/blob/dff2f186ce0e063df5842731631c0f8e8c2af0e6/src/core/Entry.cpp#L1014
- https://github.com/keepassxreboot/keepassxc/blob/dff2f186ce0e063df5842731631c0f8e8c2af0e6/src/core/Entry.cpp#L1036

Briefly investigating to figure out how to reproduce, left clicking (default), or using up/down arrows to scroll through password entries (in the right main pane) shows variable 0, 1, or 3 lines in console per selection. I'll have to investigate more later if I have time to figure out what specifically in the entries is responsible for showing the console output, but for now I'm just mentioning here that I see this issue as well.
@jasonkhanlar I updated the issue title to make it clear about the cause.

Confirmed this is an infinite loop situation. Based on my limited debugging it appears that since both notes reference each other they enter into a hall-of-mirrors type replacement situation. This is supposed to be prevented by the recursion checks, but I think because there are TWO references in the notes it finds an edge case that is not accounted for. References in general need to be refactored, this is a good case to correct when that happens.
My resolution:
Fresh install of KeepassXC did the trick. Forget what the previous installation was, so might have been an upgrade.
I had copied the massive text from Notes into the password field as well, just for fun. I also recently added Password Strength to my displayed columns, but that entry with the massive Notes has been displayed with Password Strength without crashing Keepass before so maybe adding that column is unrelated. After fresh install, having the massive text also in the Password field causes the UI to behave very weirdly when I sorted by Modified descending, but other sorts were fine.
Deleting the massive text from the Password field solves this UI problem too.



Fix (patch):
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index 49f3dfb5d9..605c266a54 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -28,12 +28,18 @@
 
 #include <QDir>
 #include <QRegularExpression>
+#include <QStringBuilder>
 #include <QUrl>
 
 const int Entry::DefaultIconNumber = 0;
-const int Entry::ResolveMaximumDepth = 10;
-const QString Entry::AutoTypeSequenceUsername = "{USERNAME}{ENTER}";
-const QString Entry::AutoTypeSequencePassword = "{PASSWORD}{ENTER}";
+
+namespace
+{
+    const int ResolveMaximumDepth = 10;
+    const QString AutoTypeSequenceUsername = "{USERNAME}{ENTER}";
+    const QString AutoTypeSequencePassword = "{PASSWORD}{ENTER}";
+    const QRegularExpression TagDelimiterRegex(R"([,;\t])");
+} // namespace
 
 Entry::Entry()
     : m_attributes(new EntryAttributes(this))
@@ -432,14 +438,11 @@ QString Entry::attribute(const QString& key) const
 int Entry::size() const
 {
     int size = 0;
-    const QRegularExpression delimiter(",|:|;");
-
-    size += this->attributes()->attributesSize();
-    size += this->autoTypeAssociations()->associationsSize();
-    size += this->attachments()->attachmentsSize();
-    size += this->customData()->dataSize();
-    const QStringList tags = this->tags().split(delimiter, QString::SkipEmptyParts);
-    for (const QString& tag : tags) {
+    size += attributes()->attributesSize();
+    size += autoTypeAssociations()->associationsSize();
+    size += attachments()->attachmentsSize();
+    size += customData()->dataSize();
+    for (const QString& tag : tags().split(TagDelimiterRegex, QString::SkipEmptyParts)) {
         size += tag.toUtf8().size();
     }
 
@@ -482,8 +485,7 @@ bool Entry::isAttributeReferenceOf(const QString& key, const QUuid& uuid) const
 
 bool Entry::hasReferences() const
 {
-    const QList<QString> keyList = EntryAttributes::DefaultAttributes;
-    for (const QString& key : keyList) {
+    for (const QString& key : EntryAttributes::DefaultAttributes) {
         if (m_attributes->isReference(key)) {
             return true;
         }
@@ -493,8 +495,7 @@ bool Entry::hasReferences() const
 
 bool Entry::hasReferencesTo(const QUuid& uuid) const
 {
-    const QList<QString> keyList = EntryAttributes::DefaultAttributes;
-    for (const QString& key : keyList) {
+    for (const QString& key : EntryAttributes::DefaultAttributes) {
         if (isAttributeReferenceOf(key, uuid)) {
             return true;
         }
@@ -670,11 +671,10 @@ void Entry::setOverrideUrl(const QString& url)
 
 void Entry::setTags(const QString& tags)
 {
-    static QRegExp rx("(\\,|\\t|\\;)");
-    auto taglist = tags.split(rx, QString::SkipEmptyParts);
+    auto taglist = tags.split(TagDelimiterRegex, QString::SkipEmptyParts);
     // Trim whitespace before/after tag text
-    for (auto itr = taglist.begin(); itr != taglist.end(); ++itr) {
-        *itr = itr->trimmed();
+    for (auto& tag : taglist) {
+        tag = tag.trimmed();
     }
     // Remove duplicates
     auto tagSet = QSet<QString>::fromList(taglist);
@@ -687,7 +687,7 @@ void Entry::setTags(const QString& tags)
 void Entry::addTag(const QString& tag)
 {
     auto cleanTag = tag.trimmed();
-    cleanTag.remove(QRegExp("(\\,|\\t|\\;)"));
+    cleanTag.remove(TagDelimiterRegex);
 
     auto taglist = m_data.tags;
     if (!taglist.contains(cleanTag)) {
@@ -700,7 +700,7 @@ void Entry::addTag(const QString& tag)
 void Entry::removeTag(const QString& tag)
 {
     auto cleanTag = tag.trimmed();
-    cleanTag.remove(QRegExp("(\\,|\\t|\\;)"));
+    cleanTag.remove(TagDelimiterRegex);
 
     auto taglist = m_data.tags;
     if (taglist.removeAll(tag) > 0) {
@@ -1014,25 +1014,23 @@ void Entry::updateModifiedSinceBegin()
 
 QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const
 {
-    static QRegularExpression placeholderRegEx("(\\{[^\\}]+?\\})", QRegularExpression::CaseInsensitiveOption);
+    static const QRegularExpression placeholderRegEx(R"(\{[^}]+\})");
 
     if (maxDepth <= 0) {
         qWarning("Maximum depth of replacement has been reached. Entry uuid: %s", uuid().toString().toLatin1().data());
         return str;
     }
 
-    QString result = str;
+    QString result;
     auto matches = placeholderRegEx.globalMatch(str);
+    int capEnd = 0;
     while (matches.hasNext()) {
-        auto match = matches.next();
-        const auto found = match.captured(1);
-        result.replace(found, resolvePlaceholderRecursive(found, maxDepth - 1));
-    }
-
-    if (result != str) {
-        result = resolveMultiplePlaceholdersRecursive(result, maxDepth - 1);
+        const auto match = matches.next();
+        result += str.midRef(capEnd, match.capturedStart() - capEnd);
+        result += resolvePlaceholderRecursive(match.captured(), maxDepth - 1);
+        capEnd = match.capturedEnd();
     }
-
+    result += str.rightRef(str.length() - capEnd);
     return result;
 }
 
@@ -1046,32 +1044,20 @@ QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDe
     const PlaceholderType typeOfPlaceholder = placeholderType(placeholder);
     switch (typeOfPlaceholder) {
     case PlaceholderType::NotPlaceholder:
-    case PlaceholderType::Unknown:
         return resolveMultiplePlaceholdersRecursive(placeholder, maxDepth - 1);
+    case PlaceholderType::Unknown: {
+        return "{" % resolveMultiplePlaceholdersRecursive(placeholder.mid(1, placeholder.length() - 2), maxDepth - 1)
+               % "}";
+    }
     case PlaceholderType::Title:
-        if (placeholderType(title()) == PlaceholderType::Title) {
-            return title();
-        }
         return resolveMultiplePlaceholdersRecursive(title(), maxDepth - 1);
     case PlaceholderType::UserName:
-        if (placeholderType(username()) == PlaceholderType::UserName) {
-            return username();
-        }
         return resolveMultiplePlaceholdersRecursive(username(), maxDepth - 1);
     case PlaceholderType::Password:
-        if (placeholderType(password()) == PlaceholderType::Password) {
-            return password();
-        }
         return resolveMultiplePlaceholdersRecursive(password(), maxDepth - 1);
     case PlaceholderType::Notes:
-        if (placeholderType(notes()) == PlaceholderType::Notes) {
-            return notes();
-        }
         return resolveMultiplePlaceholdersRecursive(notes(), maxDepth - 1);
     case PlaceholderType::Url:
-        if (placeholderType(url()) == PlaceholderType::Url) {
-            return url();
-        }
         return resolveMultiplePlaceholdersRecursive(url(), maxDepth - 1);
     case PlaceholderType::DbDir: {
         QFileInfo fileInfo(database()->filePath());
@@ -1121,60 +1107,45 @@ QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDe
 
 QString Entry::resolveDateTimePlaceholder(Entry::PlaceholderType placeholderType) const
 {
-    QDateTime time = Clock::currentDateTime();
-    QDateTime time_utc = Clock::currentDateTimeUtc();
-    QString date_formatted{};
+    const QDateTime time = Clock::currentDateTime();
+    const QDateTime time_utc = Clock::currentDateTimeUtc();
 
     switch (placeholderType) {
     case PlaceholderType::DateTimeSimple:
-        date_formatted = time.toString("yyyyMMddhhmmss");
-        break;
+        return time.toString("yyyyMMddhhmmss");
     case PlaceholderType::DateTimeYear:
-        date_formatted = time.toString("yyyy");
-        break;
+        return time.toString("yyyy");
     case PlaceholderType::DateTimeMonth:
-        date_formatted = time.toString("MM");
-        break;
+        return time.toString("MM");
     case PlaceholderType::DateTimeDay:
-        date_formatted = time.toString("dd");
-        break;
+        return time.toString("dd");
     case PlaceholderType::DateTimeHour:
-        date_formatted = time.toString("hh");
-        break;
+        return time.toString("hh");
     case PlaceholderType::DateTimeMinute:
-        date_formatted = time.toString("mm");
-        break;
+        return time.toString("mm");
     case PlaceholderType::DateTimeSecond:
-        date_formatted = time.toString("ss");
-        break;
+        return time.toString("ss");
     case PlaceholderType::DateTimeUtcSimple:
-        date_formatted = time_utc.toString("yyyyMMddhhmmss");
-        break;
+        return time_utc.toString("yyyyMMddhhmmss");
     case PlaceholderType::DateTimeUtcYear:
-        date_formatted = time_utc.toString("yyyy");
-        break;
+        return time_utc.toString("yyyy");
     case PlaceholderType::DateTimeUtcMonth:
-        date_formatted = time_utc.toString("MM");
-        break;
+        return time_utc.toString("MM");
     case PlaceholderType::DateTimeUtcDay:
-        date_formatted = time_utc.toString("dd");
-        break;
+        return time_utc.toString("dd");
     case PlaceholderType::DateTimeUtcHour:
-        date_formatted = time_utc.toString("hh");
-        break;
+        return time_utc.toString("hh");
     case PlaceholderType::DateTimeUtcMinute:
-        date_formatted = time_utc.toString("mm");
-        break;
+        return time_utc.toString("mm");
     case PlaceholderType::DateTimeUtcSecond:
-        date_formatted = time_utc.toString("ss");
-        break;
+        return time_utc.toString("ss");
     default: {
         Q_ASSERT_X(false, "Entry::resolveDateTimePlaceholder", "Bad DateTime placeholder type");
         break;
     }
     }
 
-    return date_formatted;
+    return {};
 }
 
 QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const
@@ -1187,7 +1158,7 @@ QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder,
     // resolving references in format: {REF:<WantedField>@<SearchIn>:<SearchText>}
     // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing
 
-    QRegularExpressionMatch match = EntryAttributes::matchReference(placeholder);
+    const QRegularExpressionMatch match = EntryAttributes::matchReference(placeholder);
     if (!match.hasMatch() || !m_group || !m_group->database()) {
         return placeholder;
     }
@@ -1316,9 +1287,7 @@ Database* Entry::database()
 
 QString Entry::maskPasswordPlaceholders(const QString& str) const
 {
-    QString result = str;
-    result.replace(QRegExp("(\\{PASSWORD\\})", Qt::CaseInsensitive, QRegExp::RegExp2), "******");
-    return result;
+    return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive);
 }
 
 Entry* Entry::resolveReference(const QString& str) const
@@ -1436,17 +1405,19 @@ QString Entry::resolveUrl(const QString& url) const
 {
     QString newUrl = url;
 
-    QRegExp fileRegEx("^([a-z]:)?[\\\\/]", Qt::CaseInsensitive, QRegExp::RegExp2);
-    if (fileRegEx.indexIn(newUrl) != -1) {
+    static const QRegularExpression fileRegEx(R"(^(?:[A-Za-z]:)?[\\/])");
+    if (url.contains(fileRegEx)) {
         // Match possible file paths without the scheme and convert it to a file URL
         newUrl = QDir::fromNativeSeparators(newUrl);
         newUrl = QUrl::fromLocalFile(newUrl).toString();
-    } else if (newUrl.startsWith("cmd://")) {
+    } else if (url.startsWith("cmd://")) {
         QStringList cmdList = newUrl.split(" ");
         for (int i = 1; i < cmdList.size(); ++i) {
+            QString& cmd = cmdList[i];
             // Don't pass arguments to the resolveUrl function (they look like URL's)
-            if (!cmdList[i].startsWith("-") && !cmdList[i].startsWith("/")) {
-                return resolveUrl(cmdList[i].remove(QRegExp("'|\"")));
+            if (!cmd.startsWith("-") && !cmd.startsWith("/")) {
+                static const QRegularExpression quotesRegEx("['\"]");
+                return resolveUrl(cmd.remove(quotesRegEx));
             }
         }
 
@@ -1460,13 +1431,13 @@ QString Entry::resolveUrl(const QString& url) const
     }
 
     // Validate the URL
-    QUrl tempUrl = QUrl(newUrl);
+    QUrl tempUrl(newUrl);
     if (tempUrl.isValid()
         && (tempUrl.scheme() == "http" || tempUrl.scheme() == "https" || tempUrl.scheme() == "file")) {
         return tempUrl.url();
     }
 
-    // No valid http URL's found
+    // No valid http URLs found
     return {};
 }
 
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 27d8293e3b..91ca9af062 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -227,9 +227,6 @@ class Entry : public ModifiableObject
     };
 
     static const int DefaultIconNumber;
-    static const int ResolveMaximumDepth;
-    static const QString AutoTypeSequenceUsername;
-    static const QString AutoTypeSequencePassword;
 
     /**
      * Creates a duplicate of this entry except that the returned entry isn't
diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp
index a14ae7dde8..cfdb53d712 100644
--- a/src/core/EntryAttributes.cpp
+++ b/src/core/EntryAttributes.cpp
@@ -309,8 +309,8 @@ bool EntryAttributes::operator!=(const EntryAttributes& other) const
 
 QRegularExpressionMatch EntryAttributes::matchReference(const QString& text)
 {
-    static QRegularExpression referenceRegExp(
-        "\\{REF:(?<WantedField>[TUPANI])@(?<SearchIn>[TUPANIO]):(?<SearchText>[^}]+)\\}",
+    static const QRegularExpression referenceRegExp(
+        R"(\{REF:(?<WantedField>[TUPANI])@(?<SearchIn>[TUPANIO]):(?<SearchText>[^}]+)\})",
         QRegularExpression::CaseInsensitiveOption);
 
     return referenceRegExp.match(text);