Branch data Line data Source code
1 : : #include "data/SettingsCollector.h"
2 : :
3 : : #include <QDir>
4 : : #include <QFile>
5 : : #include <QHash>
6 : : #include <QLoggingCategory>
7 : : #include <QRegularExpression>
8 : : #include <QSet>
9 : : #include <QSettings>
10 : : #include <QVariantMap>
11 : :
12 : : #include "data/CalendarStore.h"
13 : : #include "data/DavCredentials.h"
14 : : #include "data/MailCache.h"
15 : : #include "ui/FolderSubscriptionDialog.h"
16 : :
17 [ + + + - : 51 : Q_LOGGING_CATEGORY(lcSettingsSync, "mailjd.settingssync")
+ - - - ]
18 : :
19 : : namespace {
20 : :
21 : : constexpr int kMaxDavAccounts = 16;
22 : : constexpr int kMaxDavSelections = 256;
23 : : constexpr int kMaxSyncedMapEntries = 4096;
24 : :
25 : 5 : const QRegularExpression &iconNamePattern() {
26 : : static const QRegularExpression pattern(
27 [ + + + - : 9 : QStringLiteral("^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$"));
+ - - - ]
28 : 5 : return pattern;
29 : : }
30 : :
31 : 8 : const QRegularExpression &colorPattern() {
32 : : static const QRegularExpression pattern(
33 : 8 : QStringLiteral("^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|"
34 [ + + + - : 16 : "[0-9A-Fa-f]{8})$"));
+ - - - ]
35 : 8 : return pattern;
36 : : }
37 : :
38 : 196 : QString normalizedCategory(const QString &category) {
39 [ + + + + : 783 : if (category == QStringLiteral("carddavAccounts") ||
+ - + + ]
40 [ + + + + : 391 : category == QStringLiteral("caldavAccounts")) {
+ - ]
41 : 2 : return QStringLiteral("davAccounts");
42 : : }
43 : 194 : return category;
44 : : }
45 : :
46 : 19 : QStringList effectiveRemoteCategories(const SyncPayload &payload,
47 : : const QSettings &settings) {
48 : : const QStringList configured =
49 : : settings
50 [ + - ]: 57 : .value(QStringLiteral("sync/categories"),
51 [ + - ]: 38 : SyncPayload::allCategories())
52 [ + - ]: 19 : .toStringList();
53 : 19 : QSet<QString> localPolicy;
54 [ + + ]: 111 : for (const QString &category : configured)
55 [ + - + - ]: 92 : localPolicy.insert(normalizedCategory(category));
56 : :
57 : : // Privacy policy and the external-content whitelist are intentionally not
58 : : // remotely applicable. The IMAP message is only a transport container and
59 : : // does not establish the sender as an authorized device.
60 : : const QSet<QString> remotelyApplicable = {
61 : 19 : QStringLiteral("folderIcons"), QStringLiteral("folderColors"),
62 : 19 : QStringLiteral("calendarColors"), QStringLiteral("hiddenFolders"),
63 [ + + - - ]: 133 : QStringLiteral("davAccounts"), QStringLiteral("general")};
64 : :
65 : 19 : QStringList result;
66 [ + + ]: 60 : for (const QString &rawCategory : payload.enabledCategories) {
67 [ + - ]: 41 : const QString category = normalizedCategory(rawCategory);
68 [ + - ]: 76 : if (localPolicy.contains(category) &&
69 [ + + + + ]: 76 : remotelyApplicable.contains(category) &&
70 [ + + ]: 35 : !result.contains(category)) {
71 [ + - ]: 34 : result.append(category);
72 : : }
73 : 41 : }
74 : 19 : return result;
75 [ + - - - : 152 : }
- - ]
76 : :
77 : 34 : bool safeText(const QString &value, qsizetype maxLength) {
78 [ + - - + : 34 : if (value.isEmpty() || value.size() > maxLength)
- + ]
79 : 0 : return false;
80 [ + + ]: 263 : for (const QChar ch : value) {
81 [ + - - + ]: 229 : if (ch == QChar::Null || (ch.category() == QChar::Other_Control &&
82 [ - - - + ]: 229 : ch != QLatin1Char('\t')))
83 : 0 : return false;
84 : : }
85 : 34 : return true;
86 : : }
87 : :
88 : 9 : QStringList safeDavSelections(const QStringList &values) {
89 : 9 : QStringList result;
90 [ + - ]: 9 : result.reserve(qMin(values.size(), qsizetype{kMaxDavSelections}));
91 [ + + ]: 14 : for (const QString &value : values) {
92 [ - + ]: 5 : if (result.size() >= kMaxDavSelections)
93 : 0 : break;
94 [ + - + - : 5 : if (safeText(value, 4096) && !result.contains(value))
+ - ]
95 [ + - ]: 5 : result.append(value);
96 : : }
97 : 9 : return result;
98 : 0 : }
99 : :
100 : 6 : bool validDavAccount(const SyncPayload::CardDavAccount &account) {
101 [ + - - - ]: 12 : return safeText(account.id.trimmed(), 128) &&
102 [ + - + - : 18 : safeText(account.username.trimmed(), 1024) &&
+ - + - -
- ]
103 [ + - + - : 18 : !DavCredentials::canonicalOrigin(account.serverUrl).isEmpty();
+ - + - -
- ]
104 : : }
105 : :
106 : 3 : bool sameDavIdentity(const QVariantMap &local,
107 : : const SyncPayload::CardDavAccount &remote) {
108 [ + - + - : 9 : return local.value(QStringLiteral("id")).toString().trimmed() ==
+ - + - +
- + - + -
+ - - - -
- - - - -
- - - - ]
109 [ + - + - : 6 : remote.id.trimmed() &&
- - ]
110 [ + - + - : 9 : local.value(QStringLiteral("username")).toString().trimmed() ==
+ - + - +
- + - + -
+ - - - -
- - - - -
- - - - ]
111 [ + - + - : 12 : remote.username.trimmed() &&
+ - + + +
- + - -
- ]
112 [ + - - - ]: 6 : DavCredentials::canonicalOrigin(
113 [ + - + - : 12 : local.value(QStringLiteral("serverUrl")).toString()) ==
+ - + - +
- + - + -
- - - - -
- - - -
- ]
114 [ + - + - : 12 : DavCredentials::canonicalOrigin(remote.serverUrl);
+ - + - -
- ]
115 : : }
116 : :
117 : 13 : QList<QVariantMap> readSettingsArray(QSettings &settings,
118 : : const QString &name) {
119 : 13 : QList<QVariantMap> result;
120 [ + - ]: 13 : const int count = settings.beginReadArray(name);
121 [ + - ]: 13 : result.reserve(count);
122 [ + + ]: 17 : for (int i = 0; i < count; ++i) {
123 [ + - ]: 4 : settings.setArrayIndex(i);
124 : 4 : QVariantMap fields;
125 [ + - + - : 19 : for (const QString &key : settings.childKeys())
+ - + + ]
126 [ + - + - ]: 19 : fields.insert(key, settings.value(key));
127 [ + - ]: 4 : result.append(fields);
128 : 4 : }
129 [ + - ]: 13 : settings.endArray();
130 : 13 : return result;
131 : 0 : }
132 : :
133 : 7 : void writeSettingsArray(QSettings &settings, const QString &name,
134 : : const QList<QVariantMap> &items) {
135 [ + - ]: 7 : settings.remove(name);
136 [ + - ]: 7 : settings.beginWriteArray(name, items.size());
137 [ + + ]: 15 : for (int i = 0; i < items.size(); ++i) {
138 : 8 : settings.setArrayIndex(i);
139 : 8 : const QVariantMap &fields = items[i];
140 [ + - + - : 44 : for (auto it = fields.cbegin(); it != fields.cend(); ++it)
+ + ]
141 [ + - ]: 36 : settings.setValue(it.key(), it.value());
142 : : }
143 : 7 : settings.endArray();
144 : 7 : }
145 : :
146 : : } // namespace
147 : :
148 : : // ═══════════════════════════════════════════════════════
149 : : // collectLocal – gather all local settings into SyncPayload
150 : : // ═══════════════════════════════════════════════════════
151 : :
152 : 12 : SyncPayload SettingsCollector::collectLocal(MailCache *cache,
153 : : const QString &configDir,
154 : : CalendarStore *calStore) {
155 [ + - ]: 12 : SyncPayload p;
156 : 12 : p.version = 1;
157 [ + - ]: 12 : p.lastModified = QDateTime::currentDateTimeUtc();
158 : :
159 [ + - ]: 12 : QSettings s;
160 : :
161 : : // clientId
162 [ + - + - ]: 12 : p.clientId = s.value(QStringLiteral("sync/clientId")).toString();
163 : :
164 : : // --- folderIcons ---
165 : : // Use allKeys() instead of childKeys() because QSettings treats '/' as
166 : : // a group separator. Folder paths with '/' delimiter (e.g. "Work/Projects")
167 : : // become nested subgroups that childKeys() would miss.
168 [ + - ]: 12 : s.beginGroup(QStringLiteral("folder/icon"));
169 [ + - ]: 12 : const auto iconKeys = s.allKeys();
170 [ + + ]: 17 : for (const auto &key : iconKeys) {
171 [ + - + - : 5 : p.folderIcons[key] = s.value(key).toString();
+ - ]
172 : : }
173 [ + - ]: 12 : s.endGroup();
174 : :
175 : : // --- folderColors ---
176 [ + - ]: 12 : s.beginGroup(QStringLiteral("folder/color"));
177 [ + - ]: 12 : const auto colorKeys = s.allKeys();
178 [ + + ]: 17 : for (const auto &key : colorKeys) {
179 [ + - + - : 5 : p.folderColors[key] = s.value(key).toString();
+ - ]
180 : : }
181 [ + - ]: 12 : s.endGroup();
182 : :
183 : : // --- hiddenFolders ---
184 [ + - ]: 12 : p.hiddenFolders = FolderSubscriptionDialog::loadHidden(configDir);
185 : :
186 : : // The external-content whitelist is intentionally local-only. A visible
187 : : // From header is spoofable, and an unsigned sync message must not expand
188 : : // the set of messages that may load remote resources automatically.
189 : :
190 : : // --- carddavAccounts ---
191 [ + - ]: 12 : int count = s.beginReadArray(QStringLiteral("carddav/accounts"));
192 [ + + ]: 14 : for (int i = 0; i < count; ++i) {
193 [ + - ]: 2 : s.setArrayIndex(i);
194 : 2 : SyncPayload::CardDavAccount acc;
195 [ + - + - ]: 2 : acc.id = s.value(QStringLiteral("id")).toString();
196 [ + - + - ]: 2 : acc.serverUrl = s.value(QStringLiteral("serverUrl")).toString();
197 [ + - + - ]: 2 : acc.username = s.value(QStringLiteral("username")).toString();
198 : : // NOTE: password intentionally NOT collected (security)
199 [ + - + - ]: 2 : acc.selectedBooks = s.value(QStringLiteral("selectedBooks")).toStringList();
200 [ + - ]: 2 : p.carddavAccounts.append(acc);
201 : 2 : }
202 [ + - ]: 12 : s.endArray();
203 : :
204 : : // --- caldavConfigs (T-334: references CardDAV accounts) ---
205 [ + - ]: 12 : int caCount = s.beginReadArray(QStringLiteral("caldav/configs"));
206 [ + + ]: 15 : for (int i = 0; i < caCount; ++i) {
207 [ + - ]: 3 : s.setArrayIndex(i);
208 : 3 : SyncPayload::CalDavSyncConfig cfg;
209 [ + - + - ]: 3 : cfg.carddavAccountId = s.value(QStringLiteral("carddavAccountId")).toString();
210 [ + - + - ]: 3 : cfg.selectedCalendars = s.value(QStringLiteral("selectedCalendars")).toStringList();
211 [ + - + - ]: 3 : cfg.readOnlyCalendars = s.value(QStringLiteral("readOnlyCalendars")).toStringList();
212 [ + - + - ]: 6 : cfg.syncIntervalMinutes = s.value(QStringLiteral("syncIntervalMin"), 15).toInt();
213 [ + - ]: 3 : p.caldavConfigs.append(cfg);
214 : 3 : }
215 [ + - ]: 12 : s.endArray();
216 : :
217 : : // --- calendarColors (from CalendarStore SQLite) ---
218 [ + + ]: 12 : if (calStore) {
219 [ + - ]: 8 : const auto calendars = calStore->allCalendars();
220 [ + + ]: 15 : for (const auto &cal : calendars) {
221 [ + - ]: 7 : QString color = calStore->calendarColor(cal.path);
222 [ + + ]: 7 : if (!color.isEmpty())
223 [ + - ]: 6 : p.calendarColors[cal.path] = color;
224 : 7 : }
225 : 8 : }
226 : :
227 : : // --- general ---
228 : : p.general.defaultView =
229 [ + - ]: 36 : s.value(QStringLiteral("view/defaultMode"), QStringLiteral("text"))
230 [ + - ]: 24 : .toString();
231 : : p.general.externalContent =
232 [ + - ]: 36 : s.value(QStringLiteral("view/externalContent"), QStringLiteral("block"))
233 [ + - ]: 24 : .toString();
234 : : p.general.language =
235 [ + - ]: 36 : s.value(QStringLiteral("i18n/language"), QStringLiteral("auto"))
236 [ + - ]: 12 : .toString();
237 : 12 : p.general.carddavSyncInterval =
238 [ + - + - ]: 24 : s.value(QStringLiteral("carddav/syncIntervalMin"), 0).toInt();
239 : 12 : p.general.caldavSyncInterval =
240 [ + - + - ]: 24 : s.value(QStringLiteral("caldav/syncIntervalMin"), 15).toInt();
241 : :
242 : : // --- enabledCategories ---
243 : : const QStringList configuredCategories =
244 [ + - + - ]: 36 : s.value(QStringLiteral("sync/categories"), SyncPayload::allCategories())
245 [ + - ]: 12 : .toStringList();
246 [ + + ]: 75 : for (const QString &rawCategory : configuredCategories) {
247 [ + - ]: 63 : const QString category = normalizedCategory(rawCategory);
248 [ + - + - : 126 : if (SyncPayload::allCategories().contains(category) &&
+ - + - -
- ]
249 [ + - ]: 63 : !p.enabledCategories.contains(category)) {
250 [ + - ]: 63 : p.enabledCategories.append(category);
251 : : }
252 : 63 : }
253 : :
254 : 12 : return p;
255 : 12 : }
256 : :
257 : : // ═══════════════════════════════════════════════════════
258 : : // applyRemote – apply a remote SyncPayload to local settings
259 : : // ═══════════════════════════════════════════════════════
260 : :
261 : 19 : QStringList SettingsCollector::applyRemote(const SyncPayload &payload,
262 : : MailCache *cache,
263 : : const QString &configDir,
264 : : CalendarStore *calStore) {
265 [ + - ]: 19 : QSettings s;
266 [ + - ]: 19 : const QStringList cats = effectiveRemoteCategories(payload, s);
267 : 19 : QStringList applied;
268 : :
269 [ + - + - : 38 : qCInfo(lcSettingsSync) << "Accepted remote settings categories:"
+ - + + ]
270 [ + - + - ]: 19 : << cats.join(QStringLiteral(", "));
271 : :
272 : : // --- folderIcons ---
273 [ + + + - : 44 : if (cats.contains(QStringLiteral("folderIcons")) &&
+ - + + -
- - - ]
274 [ + - + - ]: 6 : payload.folderIcons.size() <= kMaxSyncedMapEntries) {
275 : : // Clear existing folder icons
276 [ + - ]: 6 : s.beginGroup(QStringLiteral("folder/icon"));
277 [ + - ]: 6 : s.remove(QString()); // removes all keys in the group
278 [ + - ]: 6 : s.endGroup();
279 : : // Write new ones
280 [ + - ]: 6 : for (auto it = payload.folderIcons.constBegin();
281 [ + - + + ]: 11 : it != payload.folderIcons.constEnd(); ++it) {
282 [ + - ]: 10 : if (safeText(it.key(), 4096) &&
283 [ + - + - : 10 : iconNamePattern().match(it.value()).hasMatch()) {
+ - + - +
- + - -
- ]
284 [ + - + - ]: 10 : s.setValue(QStringLiteral("folder/icon/") + it.key(), it.value());
285 : : } else {
286 [ # # # # : 0 : qCWarning(lcSettingsSync) << "Rejected invalid folder icon mapping for"
# # # # ]
287 [ # # ]: 0 : << it.key();
288 : : }
289 : : }
290 [ + - + - : 12 : qCInfo(lcSettingsSync) << "Applied" << payload.folderIcons.size()
+ - + - +
- + + ]
291 [ + - ]: 6 : << "folder icons";
292 [ + - ]: 6 : applied.append(QStringLiteral("folderIcons"));
293 : : }
294 : :
295 : : // --- folderColors ---
296 [ + + + - : 44 : if (cats.contains(QStringLiteral("folderColors")) &&
+ - + + -
- - - ]
297 [ + - + - ]: 6 : payload.folderColors.size() <= kMaxSyncedMapEntries) {
298 [ + - ]: 6 : s.beginGroup(QStringLiteral("folder/color"));
299 [ + - ]: 6 : s.remove(QString());
300 [ + - ]: 6 : s.endGroup();
301 [ + - ]: 6 : for (auto it = payload.folderColors.constBegin();
302 [ + - + + ]: 11 : it != payload.folderColors.constEnd(); ++it) {
303 [ + - ]: 10 : if (safeText(it.key(), 4096) &&
304 [ + - + - : 10 : colorPattern().match(it.value()).hasMatch()) {
+ - + - +
- + - -
- ]
305 [ + - + - ]: 10 : s.setValue(QStringLiteral("folder/color/") + it.key(), it.value());
306 : : } else {
307 [ # # # # : 0 : qCWarning(lcSettingsSync) << "Rejected invalid folder color mapping for"
# # # # ]
308 [ # # ]: 0 : << it.key();
309 : : }
310 : : }
311 [ + - + - : 12 : qCInfo(lcSettingsSync) << "Applied" << payload.folderColors.size()
+ - + - +
- + + ]
312 [ + - ]: 6 : << "folder colors";
313 [ + - ]: 6 : applied.append(QStringLiteral("folderColors"));
314 : : }
315 : :
316 : : // --- hiddenFolders ---
317 [ + + ]: 19 : if (cats.contains(QStringLiteral("hiddenFolders"))) {
318 : 5 : QStringList hidden;
319 [ + - ]: 5 : hidden.reserve(qMin(payload.hiddenFolders.size(), qsizetype{4096}));
320 [ + + ]: 9 : for (const QString &folder : payload.hiddenFolders) {
321 [ - + ]: 4 : if (hidden.size() >= 4096)
322 : 0 : break;
323 [ + - + - : 4 : if (safeText(folder, 4096) && !hidden.contains(folder))
+ - ]
324 [ + - ]: 4 : hidden.append(folder);
325 : : }
326 [ + - ]: 5 : FolderSubscriptionDialog::saveHidden(configDir, hidden);
327 [ + - + - : 10 : qCInfo(lcSettingsSync) << "Applied" << hidden.size()
+ - + - +
+ ]
328 [ + - ]: 5 : << "hidden folders";
329 [ + - ]: 5 : applied.append(QStringLiteral("hiddenFolders"));
330 : 5 : }
331 : :
332 : : Q_UNUSED(cache);
333 : :
334 : : // --- carddavAccounts ---
335 : 19 : QSet<QString> acceptedDavIds;
336 [ + + ]: 19 : if (cats.contains(QStringLiteral("davAccounts"))) {
337 : : QList<QVariantMap> localAccounts =
338 [ + - ]: 8 : readSettingsArray(s, QStringLiteral("carddav/accounts"));
339 : 8 : QHash<QString, int> localById;
340 [ + + ]: 12 : for (int i = 0; i < localAccounts.size(); ++i)
341 [ + - + - : 8 : localById.insert(localAccounts[i].value(QStringLiteral("id")).toString(),
+ - + - ]
342 : : i);
343 : :
344 : 8 : bool changed = false;
345 : 8 : QSet<QString> seenRemoteIds;
346 [ + + ]: 14 : for (const auto &remote : payload.carddavAccounts) {
347 [ + - ]: 6 : const QString remoteId = remote.id.trimmed();
348 [ + - + - : 6 : if (!validDavAccount(remote) || seenRemoteIds.contains(remoteId)) {
- + - + ]
349 [ # # # # : 0 : qCWarning(lcSettingsSync) << "Rejected invalid or duplicate DAV account"
# # # # ]
350 [ # # ]: 0 : << remoteId;
351 : 0 : continue;
352 : 0 : }
353 [ + - ]: 6 : seenRemoteIds.insert(remoteId);
354 : :
355 : 6 : const auto localIt = localById.constFind(remoteId);
356 [ + + ]: 6 : if (localIt != localById.cend()) {
357 [ + - ]: 3 : QVariantMap &local = localAccounts[*localIt];
358 [ + - + + ]: 3 : if (!sameDavIdentity(local, remote)) {
359 [ + - + - : 2 : qCWarning(lcSettingsSync)
+ + ]
360 [ + - + - ]: 1 : << "Rejected remote DAV identity change for account" << remoteId;
361 : 1 : continue;
362 : 1 : }
363 [ + - ]: 4 : local.insert(QStringLiteral("selectedBooks"),
364 [ + - ]: 4 : safeDavSelections(remote.selectedBooks));
365 [ + - ]: 2 : acceptedDavIds.insert(remoteId);
366 : 2 : changed = true;
367 : 2 : continue;
368 : 2 : }
369 : :
370 [ - + ]: 3 : if (localAccounts.size() >= kMaxDavAccounts) {
371 [ # # # # : 0 : qCWarning(lcSettingsSync) << "DAV account limit reached";
# # # # ]
372 : 0 : break;
373 : : }
374 : 3 : QVariantMap pending;
375 [ + - ]: 6 : pending.insert(QStringLiteral("id"), remoteId);
376 [ + - + - ]: 6 : pending.insert(QStringLiteral("serverUrl"), remote.serverUrl.trimmed());
377 [ + - + - ]: 6 : pending.insert(QStringLiteral("username"), remote.username.trimmed());
378 [ + - ]: 6 : pending.insert(QStringLiteral("selectedBooks"),
379 [ + - ]: 6 : safeDavSelections(remote.selectedBooks));
380 [ + - ]: 6 : pending.insert(QStringLiteral("requiresLocalAuthorization"), true);
381 [ + - ]: 3 : localAccounts.append(pending);
382 [ + - ]: 3 : localById.insert(remoteId, localAccounts.size() - 1);
383 [ + - ]: 3 : acceptedDavIds.insert(remoteId);
384 : 3 : changed = true;
385 [ + + - ]: 6 : }
386 : :
387 [ + + ]: 8 : if (changed) {
388 [ + - ]: 5 : writeSettingsArray(s, QStringLiteral("carddav/accounts"), localAccounts);
389 [ + - ]: 5 : applied.append(QStringLiteral("davAccounts"));
390 [ + - + - : 10 : qCInfo(lcSettingsSync) << "Merged" << acceptedDavIds.size()
+ - + - +
+ ]
391 : : << "DAV account selections without changing"
392 [ + - ]: 5 : " local endpoint identities";
393 : : }
394 : 8 : }
395 : :
396 : : // --- caldavConfigs (T-334: calendar selection per CardDAV account) ---
397 [ + + + - : 46 : if (cats.contains(QStringLiteral("davAccounts")) &&
+ - + + ]
398 [ + + ]: 8 : !acceptedDavIds.isEmpty()) {
399 : : QList<QVariantMap> localConfigs =
400 [ + - ]: 5 : readSettingsArray(s, QStringLiteral("caldav/configs"));
401 : 5 : QHash<QString, int> localConfigById;
402 [ - + ]: 5 : for (int i = 0; i < localConfigs.size(); ++i) {
403 [ # # ]: 0 : localConfigById.insert(
404 [ # # ]: 0 : localConfigs[i]
405 [ # # ]: 0 : .value(QStringLiteral("carddavAccountId"))
406 [ # # ]: 0 : .toString(),
407 : : i);
408 : : }
409 : :
410 : 5 : bool configsChanged = false;
411 [ + + ]: 7 : for (const auto &remote : payload.caldavConfigs) {
412 [ + - ]: 2 : const QString accountId = remote.carddavAccountId.trimmed();
413 [ - + ]: 2 : if (!acceptedDavIds.contains(accountId))
414 : 0 : continue;
415 [ + - ]: 2 : const QStringList selected = safeDavSelections(remote.selectedCalendars);
416 [ + - ]: 2 : QStringList readOnly = safeDavSelections(remote.readOnlyCalendars);
417 [ + - + - : 3 : for (auto it = readOnly.begin(); it != readOnly.end();) {
+ + ]
418 [ - + ]: 1 : if (!selected.contains(*it))
419 [ # # ]: 0 : it = readOnly.erase(it);
420 : : else
421 : 1 : ++it;
422 : : }
423 : :
424 : 2 : auto localIt = localConfigById.constFind(accountId);
425 [ + - ]: 2 : if (localIt == localConfigById.cend()) {
426 : 2 : QVariantMap config;
427 [ + - ]: 4 : config.insert(QStringLiteral("carddavAccountId"), accountId);
428 [ + - ]: 4 : config.insert(QStringLiteral("selectedCalendars"), selected);
429 [ + - ]: 4 : config.insert(QStringLiteral("readOnlyCalendars"), readOnly);
430 : : // Scheduling remains a local policy; do not copy the remote interval.
431 [ + - ]: 4 : config.insert(QStringLiteral("syncIntervalMin"), 15);
432 [ + - ]: 2 : localConfigs.append(config);
433 [ + - ]: 2 : localConfigById.insert(accountId, localConfigs.size() - 1);
434 : 2 : } else {
435 [ # # ]: 0 : QVariantMap &config = localConfigs[*localIt];
436 [ # # ]: 0 : config.insert(QStringLiteral("selectedCalendars"), selected);
437 [ # # ]: 0 : config.insert(QStringLiteral("readOnlyCalendars"), readOnly);
438 : : }
439 : 2 : configsChanged = true;
440 [ + - ]: 2 : }
441 [ + + ]: 5 : if (configsChanged)
442 [ + - ]: 2 : writeSettingsArray(s, QStringLiteral("caldav/configs"), localConfigs);
443 : 5 : }
444 : :
445 : : // --- calendarColors (to CalendarStore SQLite) ---
446 [ + + + + : 42 : if (cats.contains(QStringLiteral("calendarColors")) && calStore &&
+ - + - +
+ - - -
- ]
447 [ + - + - ]: 4 : payload.calendarColors.size() <= kMaxSyncedMapEntries) {
448 [ + - ]: 4 : for (auto it = payload.calendarColors.constBegin();
449 [ + - + + ]: 7 : it != payload.calendarColors.constEnd(); ++it) {
450 [ + - ]: 6 : if (safeText(it.key(), 4096) &&
451 [ + - + - : 6 : colorPattern().match(it.value()).hasMatch()) {
+ - + - +
- + - -
- ]
452 [ + - ]: 3 : calStore->setCalendarColor(it.key(), it.value());
453 : : }
454 : : }
455 [ + - + - : 8 : qCInfo(lcSettingsSync) << "Applied" << payload.calendarColors.size()
+ - + - +
- + + ]
456 [ + - ]: 4 : << "calendar colors";
457 [ + - ]: 4 : applied.append(QStringLiteral("calendarColors"));
458 : : }
459 : :
460 : : // --- general ---
461 [ + + ]: 19 : if (cats.contains(QStringLiteral("general"))) {
462 : 3 : bool generalApplied = false;
463 [ + + + - : 11 : if (payload.general.defaultView == QStringLiteral("text") ||
+ - + - ]
464 [ + + + + : 5 : payload.general.defaultView == QStringLiteral("html")) {
+ - ]
465 [ + - ]: 3 : s.setValue(QStringLiteral("view/defaultMode"),
466 : 3 : payload.general.defaultView);
467 : 3 : generalApplied = true;
468 : : }
469 [ + + + - : 8 : if (payload.general.language == QStringLiteral("auto") ||
+ - ]
470 [ + + + - : 13 : payload.general.language == QStringLiteral("en") ||
+ + + - ]
471 [ + + + + : 4 : payload.general.language == QStringLiteral("de")) {
+ + ]
472 [ + - ]: 6 : s.setValue(QStringLiteral("i18n/language"), payload.general.language);
473 : 3 : generalApplied = true;
474 : : }
475 : : // view/externalContent and all network scheduling intervals stay local.
476 [ + - ]: 3 : if (generalApplied) {
477 [ + - ]: 3 : applied.append(QStringLiteral("general"));
478 [ + - + - : 6 : qCInfo(lcSettingsSync) << "Applied non-sensitive general settings";
+ - + + ]
479 : : }
480 : : }
481 : :
482 [ + + ]: 19 : if (!applied.isEmpty()) {
483 [ + - ]: 24 : s.setValue(QStringLiteral("sync/lastSync"),
484 [ + - + - ]: 24 : QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
485 : : }
486 : :
487 [ + - ]: 19 : s.sync();
488 [ + - + - : 19 : if (!QFile::setPermissions(s.fileName(),
+ + ]
489 : : QFileDevice::ReadOwner | QFileDevice::WriteOwner)) {
490 [ + - + - : 4 : qCWarning(lcSettingsSync) << "Failed to restrict settings permissions for"
+ - + + ]
491 [ + - + - ]: 2 : << s.fileName();
492 : : }
493 : 19 : return applied;
494 : 19 : }
495 : :
496 : : // ═══════════════════════════════════════════════════════
497 : : // categoryChanged – diff helper
498 : : // ═══════════════════════════════════════════════════════
499 : :
500 : 49 : bool SettingsCollector::categoryChanged(const QString &category,
501 : : const SyncPayload &local,
502 : : const SyncPayload &remote) {
503 [ + + ]: 49 : if (category == QStringLiteral("folderIcons"))
504 : 9 : return local.folderIcons != remote.folderIcons;
505 [ + + ]: 40 : if (category == QStringLiteral("folderColors"))
506 : 5 : return local.folderColors != remote.folderColors;
507 [ + + ]: 35 : if (category == QStringLiteral("hiddenFolders"))
508 : 4 : return local.hiddenFolders != remote.hiddenFolders;
509 [ + + ]: 31 : if (category == QStringLiteral("externalContentWhitelist")) {
510 [ + + ]: 8 : if (local.externalContentWhitelist.size() !=
511 : 4 : remote.externalContentWhitelist.size())
512 : 1 : return true;
513 [ + + ]: 4 : for (int i = 0; i < local.externalContentWhitelist.size(); ++i) {
514 : 12 : if (local.externalContentWhitelist[i].type !=
515 [ + + + + ]: 5 : remote.externalContentWhitelist[i].type ||
516 [ + + ]: 2 : local.externalContentWhitelist[i].value !=
517 : 2 : remote.externalContentWhitelist[i].value)
518 : 2 : return true;
519 : : }
520 : 1 : return false;
521 : : }
522 [ + + ]: 27 : if (category == QStringLiteral("calendarColors"))
523 : 4 : return local.calendarColors != remote.calendarColors;
524 [ + + + + : 81 : if (category == QStringLiteral("davAccounts") ||
+ - + + ]
525 [ + + + + : 35 : category == QStringLiteral("carddavAccounts")) {
+ - ]
526 [ + + ]: 12 : if (local.carddavAccounts.size() != remote.carddavAccounts.size())
527 : 1 : return true;
528 [ + + ]: 15 : for (int i = 0; i < local.carddavAccounts.size(); ++i) {
529 : 8 : const auto &a = local.carddavAccounts[i];
530 : 8 : const auto &b = remote.carddavAccounts[i];
531 [ + + ]: 15 : if (a.id != b.id || a.serverUrl != b.serverUrl ||
532 [ + + + + : 15 : a.username != b.username || a.selectedBooks != b.selectedBooks)
+ + + + ]
533 : 4 : return true;
534 : : }
535 : : // Also check caldav configs
536 [ + + ]: 7 : if (local.caldavConfigs.size() != remote.caldavConfigs.size())
537 : 1 : return true;
538 [ + + ]: 7 : for (int i = 0; i < local.caldavConfigs.size(); ++i) {
539 : 5 : const auto &a = local.caldavConfigs[i];
540 : 5 : const auto &b = remote.caldavConfigs[i];
541 : 5 : if (a.carddavAccountId != b.carddavAccountId ||
542 [ + + ]: 4 : a.selectedCalendars != b.selectedCalendars ||
543 [ + + + + : 11 : a.readOnlyCalendars != b.readOnlyCalendars ||
+ + ]
544 [ + + ]: 2 : a.syncIntervalMinutes != b.syncIntervalMinutes)
545 : 4 : return true;
546 : : }
547 : 2 : return false;
548 : : }
549 [ + + ]: 11 : if (category == QStringLiteral("general")) {
550 : 9 : return local.general.defaultView != remote.general.defaultView ||
551 [ + + ]: 8 : local.general.externalContent != remote.general.externalContent ||
552 [ + + ]: 7 : local.general.language != remote.general.language ||
553 : 4 : local.general.carddavSyncInterval !=
554 [ + + + + ]: 20 : remote.general.carddavSyncInterval ||
555 : 3 : local.general.caldavSyncInterval !=
556 [ + + ]: 12 : remote.general.caldavSyncInterval;
557 : : }
558 : 2 : return false;
559 : : }
|