presentation/DavAccountSettings.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "DavAccountSettings.h" | ||
| 2 | #include <QCoreApplication> | ||
| 3 | #include "data/DavSelectionList.h" | ||
| 4 | #include "data/DavCredentials.h" | ||
| 5 | #include "data/CredentialStore.h" | ||
| 6 | #include <QPointer> | ||
| 7 | #include <QTimer> | ||
| 8 | #include <QSettings> | ||
| 9 | #include <QUuid> | ||
| 10 | #include <utility> | ||
| 11 | |||
| 12 | 27413 | QList<DavAccountSettings::Account> DavAccountSettings::accounts() { | |
| 13 | 27413 | QList<Account> rows; | |
| 14 |
1/1✓ Branch 2 → 3 taken 27413 times.
|
27413 | QSettings settings; |
| 15 |
1/1✓ Branch 7 → 8 taken 27413 times.
|
27413 | const int count = settings.beginReadArray(QStringLiteral("carddav/accounts")); |
| 16 |
2/2✓ Branch 94 → 11 taken 7611 times.
✓ Branch 94 → 95 taken 27413 times.
|
35024 | for (int i = 0; i < count; ++i) { |
| 17 |
1/1✓ Branch 11 → 12 taken 7611 times.
|
7611 | settings.setArrayIndex(i); |
| 18 | 7611 | Account row; | |
| 19 |
2/2✓ Branch 16 → 17 taken 7611 times.
✓ Branch 17 → 18 taken 7611 times.
|
7611 | row.id = settings.value(QStringLiteral("id")).toString(); |
| 20 |
2/2✓ Branch 27 → 28 taken 7611 times.
✓ Branch 28 → 29 taken 7611 times.
|
7611 | row.serverUrl = settings.value(QStringLiteral("serverUrl")).toString(); |
| 21 |
2/2✓ Branch 38 → 39 taken 7611 times.
✓ Branch 39 → 40 taken 7611 times.
|
7611 | row.username = settings.value(QStringLiteral("username")).toString(); |
| 22 |
2/2✓ Branch 49 → 50 taken 7611 times.
✓ Branch 50 → 51 taken 7611 times.
|
7611 | row.credentialId = settings.value(QStringLiteral("credentialId")).toString(); |
| 23 |
2/2✓ Branch 60 → 61 taken 7611 times.
✓ Branch 61 → 62 taken 7611 times.
|
7611 | row.selectedBooks = davPathList(settings.value(QStringLiteral("selectedBooks"))); |
| 24 |
2/2✓ Branch 72 → 73 taken 7611 times.
✓ Branch 73 → 74 taken 7611 times.
|
15222 | row.requiresLocalAuthorization = settings.value(QStringLiteral("requiresLocalAuthorization"), false).toBool(); |
| 25 |
2/2✓ Branch 82 → 83 taken 7611 times.
✓ Branch 83 → 84 taken 7611 times.
|
7611 | row.mailAccountId = settings.value(QStringLiteral("mailAccountId")).toString(); |
| 26 |
2/3✓ Branch 90 → 91 taken 7611 times.
✗ Branch 90 → 92 not taken.
✓ Branch 91 → 92 taken 7611 times.
|
7611 | if (!row.id.isEmpty()) rows.append(row); |
| 27 | 7611 | } | |
| 28 |
1/1✓ Branch 95 → 96 taken 27413 times.
|
27413 | settings.endArray(); |
| 29 | 27413 | return rows; | |
| 30 | 27413 | } | |
| 31 | |||
| 32 | 12416 | QList<DavAccountSettings::CalendarConfig> DavAccountSettings::calendarConfigs() { | |
| 33 | 12416 | QList<CalendarConfig> rows; | |
| 34 |
1/1✓ Branch 2 → 3 taken 12416 times.
|
12416 | QSettings settings; |
| 35 |
1/1✓ Branch 7 → 8 taken 12416 times.
|
12416 | const int count = settings.beginReadArray(QStringLiteral("caldav/configs")); |
| 36 |
2/2✓ Branch 50 → 11 taken 307 times.
✓ Branch 50 → 51 taken 12416 times.
|
12723 | for (int i = 0; i < count; ++i) { |
| 37 |
1/1✓ Branch 11 → 12 taken 307 times.
|
307 | settings.setArrayIndex(i); |
| 38 | 307 | CalendarConfig row; | |
| 39 |
2/2✓ Branch 16 → 17 taken 307 times.
✓ Branch 17 → 18 taken 307 times.
|
307 | row.accountId = settings.value(QStringLiteral("carddavAccountId")).toString(); |
| 40 |
2/2✓ Branch 27 → 28 taken 307 times.
✓ Branch 28 → 29 taken 307 times.
|
307 | row.selectedCalendars = davPathList(settings.value(QStringLiteral("selectedCalendars"))); |
| 41 |
2/2✓ Branch 38 → 39 taken 307 times.
✓ Branch 39 → 40 taken 307 times.
|
307 | row.readOnlyCalendars = davPathList(settings.value(QStringLiteral("readOnlyCalendars"))); |
| 42 |
2/3✓ Branch 46 → 47 taken 307 times.
✗ Branch 46 → 48 not taken.
✓ Branch 47 → 48 taken 307 times.
|
307 | if (!row.accountId.isEmpty()) rows.append(row); |
| 43 | 307 | } | |
| 44 |
1/1✓ Branch 51 → 52 taken 12416 times.
|
12416 | settings.endArray(); |
| 45 | 12416 | return rows; | |
| 46 | 12416 | } | |
| 47 | |||
| 48 | 35 | bool DavAccountSettings::storeAccounts(const QList<Account> &rows) { | |
| 49 |
2/2✓ Branch 2 → 3 taken 35 times.
✓ Branch 3 → 4 taken 35 times.
|
35 | return persist(rows, calendarConfigs()); |
| 50 | } | ||
| 51 | |||
| 52 | 55 | bool DavAccountSettings::persist(const QList<Account> &rows, const QList<CalendarConfig> &calendars) { | |
| 53 |
1/1✓ Branch 2 → 3 taken 55 times.
|
55 | const auto previous = accounts(); |
| 54 |
1/1✓ Branch 3 → 4 taken 55 times.
|
55 | const auto previousCalendars = calendarConfigs(); |
| 55 |
1/1✓ Branch 4 → 5 taken 55 times.
|
55 | QSettings settings; |
| 56 | 56 | const auto write = [&settings](const QList<Account> &values, const QList<CalendarConfig> &configs) { | |
| 57 |
1/1✓ Branch 6 → 7 taken 56 times.
|
112 | settings.remove(QStringLiteral("carddav/accounts")); |
| 58 |
1/1✓ Branch 14 → 15 taken 56 times.
|
112 | settings.beginWriteArray(QStringLiteral("carddav/accounts"), values.size()); |
| 59 |
2/2✓ Branch 115 → 18 taken 57 times.
✓ Branch 115 → 116 taken 56 times.
|
113 | for (int i = 0; i < values.size(); ++i) { |
| 60 | 57 | settings.setArrayIndex(i); | |
| 61 |
1/1✓ Branch 25 → 26 taken 57 times.
|
114 | settings.setValue(QStringLiteral("id"), values.at(i).id); |
| 62 |
1/1✓ Branch 35 → 36 taken 57 times.
|
114 | settings.setValue(QStringLiteral("serverUrl"), values.at(i).serverUrl); |
| 63 |
1/1✓ Branch 45 → 46 taken 57 times.
|
114 | settings.setValue(QStringLiteral("username"), values.at(i).username); |
| 64 |
3/3✓ Branch 51 → 52 taken 56 times.
✓ Branch 51 → 60 taken 1 time.
✓ Branch 56 → 57 taken 56 times.
|
113 | if (values.at(i).credentialId.isEmpty()) settings.remove(QStringLiteral("credentialId")); |
| 65 |
1/1✓ Branch 66 → 67 taken 1 time.
|
2 | else settings.setValue(QStringLiteral("credentialId"), values.at(i).credentialId); |
| 66 |
1/1✓ Branch 77 → 78 taken 57 times.
|
114 | settings.setValue(QStringLiteral("selectedBooks"), values.at(i).selectedBooks); |
| 67 |
1/1✓ Branch 87 → 88 taken 57 times.
|
114 | settings.setValue(QStringLiteral("requiresLocalAuthorization"), values.at(i).requiresLocalAuthorization); |
| 68 |
3/3✓ Branch 93 → 94 taken 53 times.
✓ Branch 93 → 102 taken 4 times.
✓ Branch 98 → 99 taken 53 times.
|
110 | if (values.at(i).mailAccountId.isEmpty()) settings.remove(QStringLiteral("mailAccountId")); |
| 69 |
1/1✓ Branch 108 → 109 taken 4 times.
|
8 | else settings.setValue(QStringLiteral("mailAccountId"), values.at(i).mailAccountId); |
| 70 | } | ||
| 71 | 56 | settings.endArray(); | |
| 72 |
1/1✓ Branch 121 → 122 taken 56 times.
|
112 | settings.remove(QStringLiteral("caldav/configs")); |
| 73 |
1/1✓ Branch 129 → 130 taken 56 times.
|
112 | settings.beginWriteArray(QStringLiteral("caldav/configs"), configs.size()); |
| 74 |
2/2✓ Branch 166 → 133 taken 14 times.
✓ Branch 166 → 167 taken 56 times.
|
70 | for (int i = 0; i < configs.size(); ++i) { |
| 75 | 14 | settings.setArrayIndex(i); | |
| 76 |
1/1✓ Branch 140 → 141 taken 14 times.
|
28 | settings.setValue(QStringLiteral("carddavAccountId"), configs.at(i).accountId); |
| 77 |
1/1✓ Branch 150 → 151 taken 14 times.
|
28 | settings.setValue(QStringLiteral("selectedCalendars"), configs.at(i).selectedCalendars); |
| 78 |
1/1✓ Branch 160 → 161 taken 14 times.
|
28 | settings.setValue(QStringLiteral("readOnlyCalendars"), configs.at(i).readOnlyCalendars); |
| 79 | } | ||
| 80 | 56 | settings.endArray(); | |
| 81 | 56 | settings.sync(); | |
| 82 | 56 | }; | |
| 83 |
1/1✓ Branch 5 → 6 taken 55 times.
|
55 | write(rows, calendars); |
| 84 |
3/3✓ Branch 6 → 7 taken 55 times.
✓ Branch 7 → 8 taken 54 times.
✓ Branch 7 → 9 taken 1 time.
|
55 | if (settings.status() == QSettings::NoError) return true; |
| 85 | // QSettings also shares an in-process cache. Restore that snapshot after | ||
| 86 | // failed persistence, so a later reader cannot mistake it for saved data. | ||
| 87 |
1/1✓ Branch 9 → 10 taken 1 time.
|
1 | write(previous, previousCalendars); |
| 88 | 1 | return false; | |
| 89 | 55 | } | |
| 90 | |||
| 91 | 14 | bool DavAccountSettings::storeSelections(const Account &expected, const CalendarConfig &expectedCalendar, | |
| 92 | const std::optional<QStringList> &books, | ||
| 93 | const std::optional<QStringList> &calendars) { | ||
| 94 |
6/10✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 7 taken 3 times.
✓ Branch 6 → 7 taken 11 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 14 times.
✗ Branch 8 → 13 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 14 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 14 times.
|
14 | if ((books && books->size() > 256) || (calendars && calendars->size() > 256)) return false; |
| 95 |
1/1✓ Branch 16 → 17 taken 14 times.
|
14 | auto rows = accounts(); |
| 96 |
1/1✓ Branch 17 → 18 taken 14 times.
|
14 | auto configs = calendarConfigs(); |
| 97 | 14 | int index = -1; | |
| 98 |
2/2✓ Branch 28 → 19 taken 15 times.
✓ Branch 28 → 29 taken 14 times.
|
29 | for (int i = 0; i < rows.size(); ++i) { |
| 99 |
2/2✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 14 times.
|
15 | if (rows.at(i).id != expected.id) continue; |
| 100 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 14 times.
|
14 | if (index >= 0) return false; |
| 101 | 14 | index = i; | |
| 102 | } | ||
| 103 |
5/9✓ Branch 29 → 30 taken 14 times.
✗ Branch 29 → 34 not taken.
✓ Branch 31 → 32 taken 14 times.
✓ Branch 32 → 33 taken 14 times.
✗ Branch 32 → 34 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 14 times.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 14 times.
|
14 | if (index < 0 || rows.at(index) != expected || expected.requiresLocalAuthorization) return false; |
| 104 | 14 | CalendarConfig current{expected.id, {}, {}}; | |
| 105 | 14 | int configIndex = -1; | |
| 106 |
2/2✓ Branch 50 → 40 taken 4 times.
✓ Branch 50 → 51 taken 14 times.
|
18 | for (int i = 0; i < configs.size(); ++i) { |
| 107 |
1/2✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 4 times.
|
4 | if (configs.at(i).accountId != expected.id) continue; |
| 108 |
1/2✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 4 times.
|
4 | if (configIndex >= 0) return false; |
| 109 | 4 | configIndex = i; | |
| 110 | 4 | current = configs.at(i); | |
| 111 | } | ||
| 112 |
2/3✓ Branch 51 → 52 taken 14 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 14 times.
|
14 | if (current != expectedCalendar) return false; |
| 113 |
3/3✓ Branch 55 → 56 taken 11 times.
✓ Branch 55 → 59 taken 3 times.
✓ Branch 57 → 58 taken 11 times.
|
14 | if (books) rows[index].selectedBooks = *books; |
| 114 |
1/2✓ Branch 60 → 61 taken 14 times.
✗ Branch 60 → 67 not taken.
|
14 | if (calendars) { |
| 115 | 14 | current.selectedCalendars = *calendars; | |
| 116 |
3/3✓ Branch 63 → 64 taken 10 times.
✓ Branch 63 → 65 taken 4 times.
✓ Branch 64 → 67 taken 10 times.
|
14 | if (configIndex < 0) configs.append(current); |
| 117 |
1/1✓ Branch 65 → 66 taken 4 times.
|
4 | else configs[configIndex] = current; |
| 118 | } | ||
| 119 |
1/1✓ Branch 67 → 68 taken 14 times.
|
14 | return persist(rows, configs); |
| 120 | 14 | } | |
| 121 | |||
| 122 | 4 | QString DavAccountSettings::linkedTo(const QString &mailAccountId) { | |
| 123 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
|
4 | if (mailAccountId.isEmpty()) return {}; |
| 124 |
5/5✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 16 → 9 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
|
4 | for (const Account &account : accounts()) |
| 125 |
4/4✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 22 taken 1 time.
|
4 | if (account.mailAccountId == mailAccountId) return account.id; |
| 126 | 2 | return {}; | |
| 127 | } | ||
| 128 | |||
| 129 | 3 | bool DavAccountSettings::setMailAccountLink(const QString &davAccountId, const QString &mailAccountId) { | |
| 130 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | auto rows = accounts(); |
| 131 | 3 | bool found = false; | |
| 132 |
4/4✓ Branch 3 → 4 taken 3 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
|
6 | for (auto &row : rows) { |
| 133 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 2 times.
|
3 | if (row.id != davAccountId) continue; |
| 134 | 2 | row.mailAccountId = mailAccountId; | |
| 135 | 2 | found = true; | |
| 136 | } | ||
| 137 |
4/5✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 19 not taken.
|
6 | return found && storeAccounts(rows); |
| 138 | 3 | } | |
| 139 | |||
| 140 | 6 | std::optional<DavAccountSettings::Account> DavAccountSettings::removeMetadata(const QString &id) { | |
| 141 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | auto rows = accounts(); |
| 142 | 6 | std::optional<Account> removed; | |
| 143 |
2/2✓ Branch 15 → 4 taken 7 times.
✓ Branch 15 → 16 taken 6 times.
|
13 | for (int i = 0; i < rows.size(); ++i) { |
| 144 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 6 times.
|
7 | if (rows.at(i).id != id) continue; |
| 145 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
|
6 | if (removed) return std::nullopt; |
| 146 | 6 | removed = rows.at(i); | |
| 147 | } | ||
| 148 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 6 times.
|
6 | if (!removed) return std::nullopt; |
| 149 |
1/1✓ Branch 19 → 20 taken 6 times.
|
13 | rows.removeIf([&id](const Account &row) { return row.id == id; }); |
| 150 |
1/1✓ Branch 20 → 21 taken 6 times.
|
6 | auto configs = calendarConfigs(); |
| 151 |
1/1✓ Branch 21 → 22 taken 6 times.
|
8 | configs.removeIf([&id](const CalendarConfig &row) { return row.accountId == id; }); |
| 152 |
3/4✓ Branch 22 → 23 taken 6 times.
✓ Branch 23 → 24 taken 6 times.
✗ Branch 23 → 25 not taken.
✓ Branch 24 → 26 taken 6 times.
|
6 | return persist(rows, configs) ? removed : std::nullopt; |
| 153 | 6 | } | |
| 154 | |||
| 155 | 23 | QVariantMap DavAccountSettings::save(const QVariantMap &fields, const PasswordWriter &passwordWriter) { | |
| 156 |
2/2✓ Branch 6 → 7 taken 23 times.
✓ Branch 7 → 8 taken 23 times.
|
46 | const QString id = fields.value(QStringLiteral("id")).toString(); |
| 157 |
2/2✓ Branch 13 → 14 taken 19 times.
✓ Branch 13 → 15 taken 4 times.
|
23 | const PasswordWriter writer = passwordWriter ? passwordWriter : PasswordWriter( |
| 158 | ✗ | [](const QString &accountId, const QString &serverUrl, | |
| 159 | const QString &username, const QByteArray &password, QString *error) { | ||
| 160 |
1/1✓ Branch 3 → 4 taken 1 time.
|
2 | return DavCredentials::writePasswordBlocking( |
| 161 | 2 | accountId, serverUrl, username, password, error); | |
| 162 |
1/1✓ Branch 14 → 16 taken 19 times.
|
23 | }); |
| 163 |
3/3✓ Branch 20 → 21 taken 23 times.
✓ Branch 21 → 22 taken 23 times.
✓ Branch 22 → 23 taken 23 times.
|
46 | QString serverUrl = fields.value(QStringLiteral("serverUrl")).toString().trimmed(); |
| 164 |
3/3✓ Branch 32 → 33 taken 23 times.
✓ Branch 33 → 34 taken 23 times.
✓ Branch 34 → 35 taken 23 times.
|
46 | const QString username = fields.value(QStringLiteral("username")).toString().trimmed(); |
| 165 |
2/2✓ Branch 44 → 45 taken 23 times.
✓ Branch 45 → 46 taken 23 times.
|
46 | const QString password = fields.value(QStringLiteral("password")).toString(); |
| 166 |
2/4✓ Branch 56 → 57 taken 23 times.
✓ Branch 56 → 58 taken 23 times.
✗ Branch 335 → 336 not taken.
✗ Branch 335 → 337 not taken.
|
92 | QVariantMap rejected{{QStringLiteral("ok"), false}}; |
| 167 |
2/2✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 74 taken 22 times.
|
23 | if (username.isEmpty()) { |
| 168 |
2/2✓ Branch 62 → 63 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
|
2 | rejected.insert(QStringLiteral("error"), tr("Enter a user name.")); |
| 169 | 1 | return rejected; | |
| 170 | } | ||
| 171 | // The canonical https-origin check is the same boundary the keyring and | ||
| 172 | // the DAV clients use; it rejects plain http, embedded credentials and | ||
| 173 | // malformed URLs before anything is persisted. | ||
| 174 |
1/1✓ Branch 74 → 75 taken 22 times.
|
22 | const QString canonical = DavCredentials::canonicalOrigin(serverUrl); |
| 175 |
2/2✓ Branch 76 → 77 taken 3 times.
✓ Branch 76 → 89 taken 19 times.
|
22 | if (canonical.isEmpty()) { |
| 176 |
1/1✓ Branch 82 → 83 taken 3 times.
|
6 | rejected.insert(QStringLiteral("error"), |
| 177 |
1/1✓ Branch 77 → 78 taken 3 times.
|
6 | tr("Enter a server address starting with https://")); |
| 178 | 3 | return rejected; | |
| 179 | } | ||
| 180 | 19 | serverUrl = canonical; | |
| 181 | |||
| 182 | 19 | bool updating = false; | |
| 183 | 19 | bool credentialBoundaryChanged = false; | |
| 184 |
1/1✓ Branch 90 → 91 taken 19 times.
|
19 | const auto original = accounts(); |
| 185 | 19 | QList<Account> rows = original; | |
| 186 | 19 | Account row; | |
| 187 | 19 | row.id = id; row.serverUrl = serverUrl; row.username = username; | |
| 188 |
4/4✓ Branch 95 → 96 taken 19 times.
✓ Branch 96 → 97 taken 19 times.
✓ Branch 122 → 98 taken 7 times.
✓ Branch 122 → 123 taken 12 times.
|
19 | for (auto &existing : rows) { |
| 189 |
3/6✓ Branch 100 → 101 taken 7 times.
✗ Branch 100 → 104 not taken.
✓ Branch 102 → 103 taken 7 times.
✗ Branch 102 → 104 not taken.
✓ Branch 105 → 106 taken 7 times.
✗ Branch 105 → 120 not taken.
|
7 | if (existing.id == id && !id.isEmpty()) { |
| 190 |
6/9✓ Branch 106 → 107 taken 7 times.
✓ Branch 108 → 109 taken 6 times.
✓ Branch 108 → 111 taken 1 time.
✓ Branch 110 → 111 taken 1 time.
✓ Branch 110 → 112 taken 5 times.
✓ Branch 113 → 114 taken 7 times.
✗ Branch 113 → 115 not taken.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 377 not taken.
|
7 | credentialBoundaryChanged = DavCredentials::canonicalOrigin(existing.serverUrl) != serverUrl || existing.username != username; |
| 191 | 7 | row.selectedBooks = existing.selectedBooks; | |
| 192 | 7 | row.credentialId = existing.credentialId; | |
| 193 | 7 | row.requiresLocalAuthorization = existing.requiresLocalAuthorization; | |
| 194 | 7 | row.mailAccountId = existing.mailAccountId; | |
| 195 | 7 | existing = row; | |
| 196 | 7 | updating = true; | |
| 197 | 7 | break; | |
| 198 | } | ||
| 199 | } | ||
| 200 | // S106-237: an id that names no stored account is refused like the | ||
| 201 | // asynchronous save does — minting a new identity dropped the caller's. | ||
| 202 |
6/6✓ Branch 123 → 124 taken 12 times.
✓ Branch 123 → 127 taken 7 times.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 127 taken 11 times.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 128 → 141 taken 18 times.
|
19 | if (!updating && !id.isEmpty()) { |
| 203 |
1/1✓ Branch 134 → 135 taken 1 time.
|
2 | rejected.insert(QStringLiteral("error"), |
| 204 |
1/1✓ Branch 129 → 130 taken 1 time.
|
2 | QCoreApplication::translate("DavAsyncSave", "The DAV account no longer exists.")); |
| 205 | 1 | return rejected; | |
| 206 | } | ||
| 207 |
2/2✓ Branch 141 → 142 taken 11 times.
✓ Branch 141 → 159 taken 7 times.
|
18 | if (!updating) { |
| 208 |
2/2✓ Branch 142 → 143 taken 11 times.
✓ Branch 143 → 144 taken 11 times.
|
11 | row.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 209 |
2/2✓ Branch 150 → 151 taken 11 times.
✓ Branch 151 → 152 taken 11 times.
|
22 | row.mailAccountId = fields.value(QStringLiteral("mailAccountId")).toString(); |
| 210 |
1/1✓ Branch 158 → 159 taken 11 times.
|
11 | rows.append(row); |
| 211 | } | ||
| 212 | |||
| 213 | // A secret is required for new accounts; an empty password on update | ||
| 214 | // keeps the stored keyring entry (the user edited other fields only). | ||
| 215 |
2/2✓ Branch 160 → 161 taken 11 times.
✓ Branch 160 → 194 taken 7 times.
|
18 | if (!password.isEmpty()) { |
| 216 | // The synchronous compatibility API writes the endpoint-bound key. A | ||
| 217 | // versioned id belongs only to the staged asynchronous path. | ||
| 218 | 11 | row.credentialId.clear(); | |
| 219 | 11 | QString error; | |
| 220 |
4/4✓ Branch 162 → 163 taken 11 times.
✓ Branch 163 → 164 taken 11 times.
✓ Branch 165 → 166 taken 2 times.
✓ Branch 165 → 178 taken 9 times.
|
11 | if (!writer(row.id, serverUrl, username, password.toUtf8(), &error)) { |
| 221 |
1/1✓ Branch 171 → 172 taken 2 times.
|
4 | rejected.insert(QStringLiteral("error"), |
| 222 |
1/1✓ Branch 166 → 167 taken 2 times.
|
4 | tr("The password could not be stored in the keyring.")); |
| 223 | 2 | return rejected; | |
| 224 | } | ||
| 225 |
4/4✓ Branch 178 → 179 taken 9 times.
✓ Branch 179 → 180 taken 9 times.
✓ Branch 187 → 181 taken 10 times.
✓ Branch 187 → 188 taken 9 times.
|
19 | for (auto &entry : rows) |
| 226 |
2/2✓ Branch 183 → 184 taken 9 times.
✓ Branch 183 → 185 taken 1 time.
|
10 | if (entry.id == row.id) entry.requiresLocalAuthorization = false; |
| 227 |
4/4✓ Branch 190 → 191 taken 9 times.
✓ Branch 190 → 193 taken 2 times.
✓ Branch 194 → 195 taken 2 times.
✓ Branch 194 → 207 taken 5 times.
|
18 | } else if (!updating) { |
| 228 |
2/2✓ Branch 195 → 196 taken 2 times.
✓ Branch 200 → 201 taken 2 times.
|
4 | rejected.insert(QStringLiteral("error"), tr("Enter a password.")); |
| 229 | 2 | return rejected; | |
| 230 |
2/2✓ Branch 207 → 208 taken 2 times.
✓ Branch 207 → 220 taken 3 times.
|
5 | } else if (credentialBoundaryChanged) { |
| 231 |
2/2✓ Branch 208 → 209 taken 2 times.
✓ Branch 213 → 214 taken 2 times.
|
4 | rejected.insert(QStringLiteral("error"), tr("Enter a password when changing the server or user name.")); |
| 232 | 2 | return rejected; | |
| 233 | } | ||
| 234 | |||
| 235 |
4/4✓ Branch 220 → 221 taken 12 times.
✓ Branch 221 → 222 taken 12 times.
✓ Branch 223 → 224 taken 1 time.
✓ Branch 223 → 236 taken 11 times.
|
12 | if (accounts() != original) { |
| 236 |
2/2✓ Branch 224 → 225 taken 1 time.
✓ Branch 229 → 230 taken 1 time.
|
2 | rejected.insert(QStringLiteral("error"), tr("DAV settings changed while credentials were being stored. Try again.")); |
| 237 | 1 | return rejected; | |
| 238 | } | ||
| 239 |
3/3✓ Branch 236 → 237 taken 11 times.
✓ Branch 237 → 238 taken 1 time.
✓ Branch 237 → 250 taken 10 times.
|
11 | if (!storeAccounts(rows)) { |
| 240 |
2/2✓ Branch 238 → 239 taken 1 time.
✓ Branch 243 → 244 taken 1 time.
|
2 | rejected.insert(QStringLiteral("error"), tr("The DAV settings could not be saved.")); |
| 241 | 1 | return rejected; | |
| 242 | } | ||
| 243 |
2/4✓ Branch 260 → 261 taken 20 times.
✓ Branch 260 → 262 taken 10 times.
✗ Branch 478 → 479 not taken.
✗ Branch 478 → 480 not taken.
|
60 | return {{QStringLiteral("ok"), true}, {QStringLiteral("id"), row.id}}; |
| 244 | 132 | } | |
| 245 | |||
| 246 | class DavAsyncSave final : public QObject { | ||
| 247 | ✗ | Q_DECLARE_TR_FUNCTIONS(DavAsyncSave) | |
| 248 | public: | ||
| 249 | 1 | DavAsyncSave(QObject *owner, QVariantMap fields, | |
| 250 | DavAccountSettings::AsyncSaveCallback callback, | ||
| 251 | DavAccountSettings::AsyncPasswordWriter writer) | ||
| 252 | 1 | : QObject(owner), m_fields(std::move(fields)), m_callback(std::move(callback)), | |
| 253 |
4/7✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
|
2 | m_writer(std::move(writer)), m_store(new CredentialStore(this)) { |
| 254 |
1/1✓ Branch 18 → 19 taken 1 time.
|
1 | m_deadline.setSingleShot(true); |
| 255 |
1/1✓ Branch 19 → 20 taken 1 time.
|
1 | m_deadline.setInterval(30000); |
| 256 | 1 | connect(&m_deadline, &QTimer::timeout, this, | |
| 257 |
1/3✗ Branch 2 → 3 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 20 → 21 taken 1 time.
|
1 | [this] { finish(false, tr("Timed out while saving the DAV password.")); }); |
| 258 |
1/1✓ Branch 22 → 23 taken 1 time.
|
2 | QTimer::singleShot(0, this, [this] { begin(); }); |
| 259 | 1 | } | |
| 260 | 2 | ~DavAsyncSave() override { | |
| 261 |
2/6✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 1 time.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 23 taken 1 time.
|
1 | if (!m_finished && !m_stagedKey.isEmpty()) |
| 262 | ✗ | CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), | |
| 263 | ✗ | {{QStringLiteral("carddav"), m_stagedKey}}, {}); | |
| 264 | 2 | } | |
| 265 | |||
| 266 | private: | ||
| 267 | 1 | void begin() { | |
| 268 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | m_original = DavAccountSettings::accounts(); |
| 269 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
|
2 | const QString requestedId = m_fields.value(QStringLiteral("id")).toString(); |
| 270 |
3/3✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
|
2 | QString serverUrl = m_fields.value(QStringLiteral("serverUrl")).toString().trimmed(); |
| 271 |
3/3✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
|
2 | const QString username = m_fields.value(QStringLiteral("username")).toString().trimmed(); |
| 272 |
3/3✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
|
2 | const QByteArray password = m_fields.value(QStringLiteral("password")).toString().toUtf8(); |
| 273 |
1/4✗ Branch 52 → 53 not taken.
✓ Branch 52 → 57 taken 1 time.
✗ Branch 53 → 54 not taken.
✗ Branch 54 → 55 not taken.
|
1 | if (username.isEmpty()) { finish(false, tr("Enter a user name.")); return; } |
| 274 |
1/1✓ Branch 57 → 58 taken 1 time.
|
1 | serverUrl = DavCredentials::canonicalOrigin(serverUrl); |
| 275 |
1/2✗ Branch 61 → 62 not taken.
✓ Branch 61 → 66 taken 1 time.
|
1 | if (serverUrl.isEmpty()) { |
| 276 | ✗ | finish(false, tr("Enter a server address starting with https://")); | |
| 277 | ✗ | return; | |
| 278 | } | ||
| 279 | |||
| 280 | 1 | int index = -1; | |
| 281 |
1/2✗ Branch 84 → 67 not taken.
✓ Branch 84 → 85 taken 1 time.
|
1 | for (int i = 0; i < m_original.size(); ++i) { |
| 282 | ✗ | if (m_original.at(i).id != requestedId || requestedId.isEmpty()) continue; | |
| 283 | ✗ | if (index >= 0) { finish(false, tr("The DAV account identity is stored more than once.")); return; } | |
| 284 | ✗ | index = i; | |
| 285 | } | ||
| 286 |
2/6✗ Branch 86 → 87 not taken.
✓ Branch 86 → 89 taken 1 time.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 95 taken 1 time.
|
1 | if (!requestedId.isEmpty() && index < 0) { |
| 287 | ✗ | finish(false, tr("The DAV account no longer exists.")); | |
| 288 | ✗ | return; | |
| 289 | } | ||
| 290 |
1/2✗ Branch 95 → 96 not taken.
✓ Branch 95 → 98 taken 1 time.
|
1 | if (index >= 0) m_previous = m_original.at(index); |
| 291 |
1/2✗ Branch 98 → 99 not taken.
✓ Branch 98 → 100 taken 1 time.
|
1 | m_row = index >= 0 ? m_previous : DavAccountSettings::Account{}; |
| 292 |
1/2✓ Branch 104 → 105 taken 1 time.
✗ Branch 104 → 122 not taken.
|
1 | if (m_row.id.isEmpty()) { |
| 293 |
2/2✓ Branch 105 → 106 taken 1 time.
✓ Branch 106 → 107 taken 1 time.
|
1 | m_row.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 294 |
2/2✓ Branch 113 → 114 taken 1 time.
✓ Branch 114 → 115 taken 1 time.
|
2 | m_row.mailAccountId = m_fields.value(QStringLiteral("mailAccountId")).toString(); |
| 295 | } | ||
| 296 |
1/2✗ Branch 122 → 123 not taken.
✓ Branch 122 → 129 taken 1 time.
|
1 | const bool boundaryChanged = index >= 0 && |
| 297 | ✗ | (DavCredentials::canonicalOrigin(m_previous.serverUrl) != serverUrl || | |
| 298 |
1/4✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 132 taken 1 time.
|
1 | m_previous.username != username); |
| 299 |
2/8✗ Branch 133 → 134 not taken.
✓ Branch 133 → 136 taken 1 time.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 142 taken 1 time.
✗ Branch 138 → 139 not taken.
✗ Branch 139 → 140 not taken.
|
1 | if (password.isEmpty() && index < 0) { finish(false, tr("Enter a password.")); return; } |
| 300 |
2/6✗ Branch 143 → 144 not taken.
✓ Branch 143 → 146 taken 1 time.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
✗ Branch 147 → 148 not taken.
✓ Branch 147 → 152 taken 1 time.
|
1 | if (password.isEmpty() && boundaryChanged) { |
| 301 | ✗ | finish(false, tr("Enter a password when changing the server or user name.")); | |
| 302 | ✗ | return; | |
| 303 | } | ||
| 304 | 1 | m_row.serverUrl = serverUrl; | |
| 305 | 1 | m_row.username = username; | |
| 306 |
1/3✗ Branch 155 → 156 not taken.
✓ Branch 155 → 158 taken 1 time.
✗ Branch 156 → 157 not taken.
|
1 | if (password.isEmpty()) { publish(); return; } |
| 307 | |||
| 308 |
2/2✓ Branch 158 → 159 taken 1 time.
✓ Branch 159 → 160 taken 1 time.
|
1 | m_row.credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 309 | 1 | m_row.requiresLocalAuthorization = false; | |
| 310 | 1 | m_stagedKey = DavCredentials::accountName(m_row.id, m_row.serverUrl, | |
| 311 |
1/1✓ Branch 162 → 163 taken 1 time.
|
1 | m_row.username, m_row.credentialId); |
| 312 |
1/1✓ Branch 165 → 166 taken 1 time.
|
1 | m_deadline.start(); |
| 313 | 1 | const auto completion = [this](bool success, const QString &error) { | |
| 314 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
|
1 | if (m_finished) return; |
| 315 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 1 time.
|
1 | if (!success) { |
| 316 | ✗ | finish(false, error.isEmpty() | |
| 317 | ✗ | ? tr("The password could not be stored in the keyring.") : error); | |
| 318 | ✗ | return; | |
| 319 | } | ||
| 320 | 1 | publish(); | |
| 321 | 1 | }; | |
| 322 |
2/3✓ Branch 167 → 168 taken 1 time.
✗ Branch 167 → 172 not taken.
✓ Branch 169 → 170 taken 1 time.
|
1 | if (m_writer) m_writer(this, m_stagedKey, password, completion); |
| 323 | ✗ | else m_store->writePassword(QStringLiteral("carddav"), m_stagedKey, | |
| 324 | password, completion); | ||
| 325 | 1 | } | |
| 326 | |||
| 327 | 1 | void publish() { | |
| 328 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | m_deadline.stop(); |
| 329 |
3/4✓ Branch 3 → 4 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 11 taken 1 time.
|
1 | if (DavAccountSettings::accounts() != m_original) { |
| 330 | ✗ | finish(false, tr("DAV settings changed while credentials were being stored. Try again.")); | |
| 331 | ✗ | return; | |
| 332 | } | ||
| 333 | 1 | auto rows = m_original; | |
| 334 | 1 | int index = -1; | |
| 335 |
1/2✗ Branch 19 → 13 not taken.
✓ Branch 19 → 20 taken 1 time.
|
1 | for (int i = 0; i < rows.size(); ++i) |
| 336 | ✗ | if (rows.at(i).id == m_row.id) { index = i; break; } | |
| 337 |
2/4✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 22 not taken.
✓ Branch 21 → 24 taken 1 time.
✗ Branch 22 → 23 not taken.
|
1 | if (index < 0) rows.append(m_row); else rows[index] = m_row; |
| 338 |
2/3✓ Branch 24 → 25 taken 1 time.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 30 taken 1 time.
|
1 | if (!DavAccountSettings::storeAccounts(rows)) { |
| 339 | ✗ | finish(false, tr("The DAV settings could not be saved.")); | |
| 340 | ✗ | return; | |
| 341 | } | ||
| 342 | 1 | const QString staged = std::exchange(m_stagedKey, {}); | |
| 343 | 1 | m_finished = true; | |
| 344 | 1 | const auto callback = std::move(m_callback); | |
| 345 | // S106-236 (INV-09): the superseded secret goes also when it lived in | ||
| 346 | // the older id/v2-digest or legacy form (no credential id) — it used to | ||
| 347 | // stay in the keyring after the account moved to a credential id. | ||
| 348 |
2/6✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 1 time.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 47 taken 1 time.
|
1 | if (!m_previous.id.isEmpty() && m_previous.credentialId != m_row.credentialId) { |
| 349 | ✗ | DavCredentials::deletePasswordAsync(QCoreApplication::instance(), | |
| 350 | ✗ | m_previous.id, m_previous.serverUrl, m_previous.username, {}, | |
| 351 | ✗ | m_previous.credentialId); | |
| 352 | } | ||
| 353 | Q_UNUSED(staged) | ||
| 354 |
4/7✓ Branch 48 → 49 taken 1 time.
✗ Branch 48 → 68 not taken.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 1 time.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
|
6 | if (callback) callback({{QStringLiteral("ok"), true}, |
| 355 | 2 | {QStringLiteral("id"), m_row.id}}); | |
| 356 |
1/1✓ Branch 68 → 69 taken 1 time.
|
1 | deleteLater(); |
| 357 | 5 | } | |
| 358 | |||
| 359 | ✗ | void finish(bool success, QString error) { | |
| 360 | ✗ | if (m_finished) return; | |
| 361 | ✗ | m_finished = true; | |
| 362 | ✗ | m_deadline.stop(); | |
| 363 | // A failed keyring write reports from inside the finished signal of a | ||
| 364 | // job owned by m_store; deleting the store synchronously freed that job | ||
| 365 | // while it was still emitting (use-after-free, crash on a later timer; | ||
| 366 | // Sprint 100 SB-14). Remaining jobs are dropped with the store after | ||
| 367 | // the current event; their callbacks see m_finished and return. | ||
| 368 | ✗ | if (m_store) m_store->deleteLater(); | |
| 369 | ✗ | m_store = nullptr; | |
| 370 | ✗ | const QString staged = std::exchange(m_stagedKey, {}); | |
| 371 | ✗ | const auto callback = std::move(m_callback); | |
| 372 | ✗ | const QVariantMap result{{QStringLiteral("ok"), success}, | |
| 373 | ✗ | {QStringLiteral("error"), error}}; | |
| 374 | ✗ | if (staged.isEmpty()) { | |
| 375 | ✗ | if (callback) callback(result); | |
| 376 | ✗ | deleteLater(); | |
| 377 | ✗ | return; | |
| 378 | } | ||
| 379 | ✗ | CredentialStore::deletePasswordsAsync(this, | |
| 380 | ✗ | {{QStringLiteral("carddav"), staged}}, | |
| 381 | ✗ | [this, callback, result](bool) { | |
| 382 | ✗ | if (callback) callback(result); | |
| 383 | ✗ | deleteLater(); | |
| 384 | ✗ | }); | |
| 385 | ✗ | } | |
| 386 | |||
| 387 | QVariantMap m_fields; | ||
| 388 | DavAccountSettings::AsyncSaveCallback m_callback; | ||
| 389 | DavAccountSettings::AsyncPasswordWriter m_writer; | ||
| 390 | CredentialStore *m_store; | ||
| 391 | QTimer m_deadline; | ||
| 392 | QList<DavAccountSettings::Account> m_original; | ||
| 393 | DavAccountSettings::Account m_previous; | ||
| 394 | DavAccountSettings::Account m_row; | ||
| 395 | QString m_stagedKey; | ||
| 396 | bool m_finished = false; | ||
| 397 | }; | ||
| 398 | |||
| 399 | 1 | QObject *DavAccountSettings::saveAsync(QObject *owner, const QVariantMap &fields, | |
| 400 | AsyncSaveCallback callback, | ||
| 401 | const AsyncPasswordWriter &passwordWriter) { | ||
| 402 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
|
1 | if (!owner) return nullptr; |
| 403 |
3/6✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
|
1 | return new DavAsyncSave(owner, fields, std::move(callback), passwordWriter); |
| 404 | } | ||
| 405 |