Skip to content
API Documentation
BastionGuard™
SettingsStore.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include "SettingsStore.hpp"
22
23#include <glibmm/miscutils.h>
24#include <filesystem>
25#include <fstream>
26#include <unordered_set>
27#include <algorithm>
28#include <locale.h>
29#include <iostream>
30
31namespace fs = std::filesystem;
32
33std::mutex SettingsStore::mtx_;
34bool SettingsStore::loaded_ = false;
35nlohmann::json SettingsStore::cache_ = nlohmann::json::object();
36
37static std::string config_dir_path() {
38 return Glib::get_home_dir() + "/.config/BastionGuard";
39}
40
42 return config_dir_path() + "/settings.json";
43}
44
45nlohmann::json SettingsStore::defaults() {
46 nlohmann::json j;
47 j["security"] = nlohmann::json::object();
48 j["security"]["secure_payments_enabled"] = false;
49
50 j["ui"] = nlohmann::json::object();
51 j["ui"]["language"] = ""; // vuoto = leggi da lang.conf
52
53 return j;
54}
55
57 std::lock_guard<std::mutex> lk(mtx_);
59}
60
62 std::lock_guard<std::mutex> lk(mtx_);
63 loaded_ = false;
64 cache_.clear();
66}
67
69 std::lock_guard<std::mutex> lk(mtx_);
70 cache_ = defaults();
71 loaded_ = true;
73}
74
76 if (loaded_) return;
77
78 cache_ = defaults();
79
81
82 loaded_ = true;
83}
84
86 const auto path = settings_file_path();
87
88 if (!fs::exists(path)) {
89 return true;
90 }
91
92 try {
93 std::ifstream in(path, std::ios::binary);
94 if (!in.good()) return false;
95
96 nlohmann::json disk;
97 in >> disk;
98 cache_.merge_patch(disk);
99
100 return true;
101 } catch (...) {
102 return false;
103 }
104}
105
106bool SettingsStore::write_to_disk_locked(const nlohmann::json& j) {
107 const auto dir = config_dir_path();
108 const auto path = settings_file_path();
109 const auto tmp = path + ".tmp";
110
111 try {
112 fs::create_directories(dir);
113 {
114 std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
115 if (!out.good()) return false;
116 out << j.dump(2);
117 out.flush();
118 if (!out.good()) return false;
119 }
120
121 std::error_code ec;
122 fs::rename(tmp, path, ec);
123 if (ec) {
124 fs::remove(path, ec);
125 ec.clear();
126 fs::rename(tmp, path, ec);
127 if (ec) {
128 fs::remove(tmp, ec);
129 return false;
130 }
131 }
132
133 return true;
134 } catch (...) {
135 return false;
136 }
137}
138
140 std::lock_guard<std::mutex> lk(mtx_);
143}
144
145static std::vector<std::string> split_dotted(const std::string& dotted_key) {
146 std::vector<std::string> parts;
147 std::string cur;
148 for (char c : dotted_key) {
149 if (c == '.') {
150 if (!cur.empty()) parts.push_back(cur);
151 cur.clear();
152 } else {
153 cur.push_back(c);
154 }
155 }
156 if (!cur.empty()) parts.push_back(cur);
157 return parts;
158}
159
160nlohmann::json* SettingsStore::get_or_create_node_locked(nlohmann::json& root, const std::string& dotted_key) {
161 auto parts = split_dotted(dotted_key);
162 if (parts.empty()) return nullptr;
163
164 nlohmann::json* node = &root;
165 for (size_t i = 0; i + 1 < parts.size(); ++i) {
166 const auto& k = parts[i];
167 if (!node->contains(k) || !(*node)[k].is_object()) {
168 (*node)[k] = nlohmann::json::object();
169 }
170 node = &(*node)[k];
171 }
172 return node;
173}
174
175const nlohmann::json* SettingsStore::get_node_locked(const nlohmann::json& root, const std::string& dotted_key) {
176 auto parts = split_dotted(dotted_key);
177 if (parts.empty()) return nullptr;
178
179 const nlohmann::json* node = &root;
180 for (const auto& k : parts) {
181 if (!node->is_object() || !node->contains(k)) return nullptr;
182 node = &(*node)[k];
183 }
184 return node;
185}
186
187bool SettingsStore::get_bool(const std::string& dotted_key, bool fallback_default) {
188 std::lock_guard<std::mutex> lk(mtx_);
190
191 const auto* n = get_node_locked(cache_, dotted_key);
192 if (!n) return fallback_default;
193 if (n->is_boolean()) return n->get<bool>();
194 return fallback_default;
195}
196
197void SettingsStore::set_bool(const std::string& dotted_key, bool value) {
198 std::lock_guard<std::mutex> lk(mtx_);
200
201 auto parts = split_dotted(dotted_key);
202 if (parts.empty()) return;
203
204 nlohmann::json* parent = get_or_create_node_locked(cache_, dotted_key);
205 if (!parent) return;
206
207 (*parent)[parts.back()] = value;
209}
210
211int SettingsStore::get_int(const std::string& dotted_key, int fallback_default) {
212 std::lock_guard<std::mutex> lk(mtx_);
214
215 const auto* n = get_node_locked(cache_, dotted_key);
216 if (!n) return fallback_default;
217 if (n->is_number_integer()) return n->get<int>();
218 if (n->is_number()) return static_cast<int>(n->get<double>());
219 return fallback_default;
220}
221
222void SettingsStore::set_int(const std::string& dotted_key, int value) {
223 std::lock_guard<std::mutex> lk(mtx_);
225
226 auto parts = split_dotted(dotted_key);
227 if (parts.empty()) return;
228
229 nlohmann::json* parent = get_or_create_node_locked(cache_, dotted_key);
230 if (!parent) return;
231
232 (*parent)[parts.back()] = value;
234}
235
236std::string SettingsStore::get_string(const std::string& dotted_key, const std::string& fallback_default) {
237 std::lock_guard<std::mutex> lk(mtx_);
239
240 const auto* n = get_node_locked(cache_, dotted_key);
241 if (!n) return fallback_default;
242 if (n->is_string()) return n->get<std::string>();
243 return fallback_default;
244}
245
246void SettingsStore::set_string(const std::string& dotted_key, const std::string& value) {
247 std::lock_guard<std::mutex> lk(mtx_);
249
250 auto parts = split_dotted(dotted_key);
251 if (parts.empty()) return;
252
253 nlohmann::json* parent = get_or_create_node_locked(cache_, dotted_key);
254 if (!parent) return;
255
256 (*parent)[parts.back()] = value;
258}
259
260
262 // usa default ufficiale (false)
263 return get_bool("security.secure_payments_enabled", false);
264}
265
267 set_bool("security.secure_payments_enabled", enabled);
268}
269
271 return get_string("ui.language", "");
272}
273
274void SettingsStore::set_language(const std::string& lang) {
275 set_string("ui.language", lang);
276
277 // Mantieni lang.conf sincronizzato — usato dai binari che girano
278 // prima che SettingsStore sia inizializzato (es. script di sistema).
279 const std::string lang_conf =
280 Glib::get_home_dir() + "/.config/BastionGuard/lang.conf";
281 try {
282 std::filesystem::create_directories(
283 std::filesystem::path(lang_conf).parent_path());
284 std::ofstream f(lang_conf, std::ios::trunc);
285 if (f.is_open()) {
286 f << "LANG=" << lang << ".UTF-8\n"
287 << "LC_ALL=" << lang << ".UTF-8\n"
288 << "LANGUAGE=" << lang << ":" << lang.substr(0, 2) << "\n";
289 }
290 } catch (...) {}
291}
292
294 // ── Fonte 1: settings.json ui.language ───────────────────────────────────
295 const std::string from_settings = get_language();
296
297 // ── Fonte 2: lang.conf ────────────────────────────────────────────────────
298 const std::string lang_conf =
299 Glib::get_home_dir() + "/.config/BastionGuard/lang.conf";
300
301 // Leggi la lingua da lang.conf (es. "it_IT" da LANG=it_IT.UTF-8)
302 std::string from_lang_conf;
303 {
304 std::ifstream f(lang_conf);
305 std::string line;
306 while (std::getline(f, line)) {
307 // Cerca LANG= o LANGUAGE= per estrarre il codice lingua
308 if (line.rfind("LANGUAGE=", 0) == 0) {
309 from_lang_conf = line.substr(9);
310 // Prendi solo la prima voce (es. "it_IT:it" → "it_IT")
311 const auto colon = from_lang_conf.find(':');
312 if (colon != std::string::npos)
313 from_lang_conf = from_lang_conf.substr(0, colon);
314 break;
315 }
316 }
317 }
318
319 // Determina la lingua effettiva da usare:
320 // Se settings.json è vuoto o è ancora il default "en_US" E lang.conf
321 // ha qualcosa di diverso → lang.conf vince (migrazione automatica).
322 std::string lang;
323 const bool settings_is_default =
324 from_settings.empty() || from_settings == "en_US";
325
326 if (settings_is_default && !from_lang_conf.empty()
327 && from_lang_conf != "en_US") {
328 lang = from_lang_conf;
329 // Scrivi in settings.json così i prossimi avvii usano direttamente quello
330 set_language(lang);
331 std::cerr << "[i18n] migrated language from lang.conf to settings.json: "
332 << lang << "\n";
333 } else if (!from_settings.empty()) {
334 lang = from_settings;
335 std::cerr << "[i18n] language from settings.json: " << lang << "\n";
336 } else if (!from_lang_conf.empty()) {
337 lang = from_lang_conf;
338 std::cerr << "[i18n] language from lang.conf: " << lang << "\n";
339 } else {
340 std::cerr << "[i18n] no language configured, using system locale\n";
341 setlocale(LC_ALL, "");
342 return;
343 }
344
345 setenv("LANGUAGE", lang.c_str(), 1);
346 setenv("LANG", (lang + ".UTF-8").c_str(), 1);
347 setenv("LC_ALL", (lang + ".UTF-8").c_str(), 1);
348 if (!setlocale(LC_ALL, (lang + ".UTF-8").c_str()))
349 if (!setlocale(LC_ALL, lang.c_str()))
350 setlocale(LC_ALL, "");
351}
352
354 return Glib::get_home_dir() + "/.config/BastionGuard/payments.json";
355}
356
358 return "/usr/share/BastionGuard/data/payments/payments.json";
359}
360
361static std::string normalize_domain(std::string s) {
362 s.erase(0, s.find_first_not_of(" \t\r\n"));
363 s.erase(s.find_last_not_of(" \t\r\n") + 1);
364 std::transform(s.begin(), s.end(), s.begin(),
365 [](unsigned char c){ return (char)std::tolower(c); });
366 if (!s.empty() && s[0] == '.') s.erase(0, 1);
367 if (s.rfind("www.", 0) == 0) s.erase(0, 4);
368 auto p = s.find(':');
369 if (p != std::string::npos) s.erase(p);
370
371 return s;
372}
373
375 nlohmann::json j;
376 j["version"] = 1;
377 j["updated_at"] = "2026-01-13";
378 j["list"] = nlohmann::json::array({
379 "paypal.com",
380 "stripe.com",
381 "adyen.com",
382 "klarna.com",
383 "checkout.com",
384 "worldpay.com",
385 "braintreepayments.com",
386 "braintreegateway.com",
387 "amazonpay.com",
388 "pay.google.com",
389 "pay.apple.com",
390 "sisalpay.it",
391 "nexi.it",
392 "payments.amazon.com"
393 });
394 return j;
395}
396
397bool SettingsStore::read_json_file(const std::string& path, nlohmann::json& out) {
398 try {
399 if (!fs::exists(path)) return false;
400 std::ifstream in(path, std::ios::binary);
401 if (!in.good()) return false;
402 in >> out;
403 return true;
404 } catch (...) {
405 return false;
406 }
407}
408
409bool SettingsStore::write_json_atomic(const std::string& path, const nlohmann::json& j) {
410 try {
411 fs::create_directories(fs::path(path).parent_path());
412 const auto tmp = path + ".tmp";
413
414 {
415 std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
416 if (!out.good()) return false;
417 out << j.dump(2);
418 out.flush();
419 if (!out.good()) return false;
420 }
421
422 std::error_code ec;
423 fs::rename(tmp, path, ec);
424 if (ec) {
425 fs::remove(path, ec);
426 ec.clear();
427 fs::rename(tmp, path, ec);
428 if (ec) {
429 fs::remove(tmp, ec);
430 return false;
431 }
432 }
433 return true;
434 } catch (...) {
435 return false;
436 }
437}
438
440 std::lock_guard<std::mutex> lk(mtx_);
441
442 const auto up = user_payments_path();
443 const auto sp = system_payments_path();
444
445 if (fs::exists(up)) return;
446 if (fs::exists(sp)) {
447 try {
448 fs::create_directories(fs::path(up).parent_path());
449 std::ifstream in(sp, std::ios::binary);
450 std::ofstream out(up, std::ios::binary | std::ios::trunc);
451 if (in.good() && out.good()) {
452 out << in.rdbuf();
453 return;
454 }
455 } catch (...) {
456 }
457 }
458
460}
461
462static void extract_domains_from_payments_json(const nlohmann::json& j, std::vector<std::string>& out) {
463 if (j.is_object()) {
464 if (j.contains("list") && j["list"].is_array()) {
465 for (const auto& v : j["list"]) {
466 if (v.is_string()) out.push_back(normalize_domain(v.get<std::string>()));
467 }
468 } else if (j.contains("domains") && j["domains"].is_array()) {
469 for (const auto& v : j["domains"]) {
470 if (v.is_string()) out.push_back(normalize_domain(v.get<std::string>()));
471 }
472 }
473 } else if (j.is_array()) {
474 for (const auto& v : j) {
475 if (v.is_string()) out.push_back(normalize_domain(v.get<std::string>()));
476 }
477 }
478}
479
480std::vector<std::string> SettingsStore::get_payment_domains() {
482
483 std::lock_guard<std::mutex> lk(mtx_);
484
485 std::vector<std::string> tmp;
486 nlohmann::json jsys, jusr;
489
490 // dedup + sort
491 std::unordered_set<std::string> uniq;
492 std::vector<std::string> out;
493 out.reserve(tmp.size());
494
495 for (auto& d : tmp) {
496 d = normalize_domain(d);
497 if (d.empty()) continue;
498 if (uniq.insert(d).second) out.push_back(d);
499 }
500
501 std::sort(out.begin(), out.end());
502 return out;
503}
504
505bool SettingsStore::save_payment_domains_user(const std::vector<std::string>& domains) {
506 std::lock_guard<std::mutex> lk(mtx_);
507
508 nlohmann::json j;
509 j["version"] = 1;
510 j["updated_at"] = "2026-01-13";
511 j["list"] = nlohmann::json::array();
512
513 std::unordered_set<std::string> uniq;
514 for (auto d : domains) {
515 d = normalize_domain(d);
516 if (d.empty()) continue;
517 if (uniq.insert(d).second) j["list"].push_back(d);
518 }
519
521}
522
524 std::lock_guard<std::mutex> lk(mtx_);
525
526 const auto sp = system_payments_path();
527 const auto up = user_payments_path();
528
529 if (!fs::exists(sp)) return false;
530
531 try {
532 fs::create_directories(fs::path(up).parent_path());
533 std::ifstream in(sp, std::ios::binary);
534 std::ofstream out(up, std::ios::binary | std::ios::trunc);
535 if (!in.good() || !out.good()) return false;
536 out << in.rdbuf();
537 return true;
538 } catch (...) {
539 return false;
540 }
541}
static std::string normalize_domain(std::string s)
static std::string config_dir_path()
static std::vector< std::string > split_dotted(const std::string &dotted_key)
static void extract_domains_from_payments_json(const nlohmann::json &j, std::vector< std::string > &out)
static std::string user_payments_path()
static void ensure_loaded_locked()
static void init()
static std::string get_string(const std::string &dotted_key, const std::string &fallback_default)
static nlohmann::json cache_
static bool write_to_disk_locked(const nlohmann::json &j)
static void set_language(const std::string &lang)
static void set_secure_payments_enabled(bool enabled)
static nlohmann::json default_payments_seed()
static std::string settings_file_path()
static bool get_secure_payments_enabled()
static bool save_payment_domains_user(const std::vector< std::string > &domains)
static std::mutex mtx_
static nlohmann::json defaults()
static bool get_bool(const std::string &dotted_key, bool fallback_default)
static void ensure_payments_json_exists()
static bool flush()
static nlohmann::json * get_or_create_node_locked(nlohmann::json &root, const std::string &dotted_key)
static bool loaded_
static bool reset_to_defaults()
static const nlohmann::json * get_node_locked(const nlohmann::json &root, const std::string &dotted_key)
static std::vector< std::string > get_payment_domains()
static void apply_language()
static void reload()
static void set_int(const std::string &dotted_key, int value)
static std::string get_language()
static bool read_json_file(const std::string &path, nlohmann::json &out)
static void set_bool(const std::string &dotted_key, bool value)
static void set_string(const std::string &dotted_key, const std::string &value)
static std::string system_payments_path()
static int get_int(const std::string &dotted_key, int fallback_default)
static bool reset_payment_domains_from_system()
static bool write_json_atomic(const std::string &path, const nlohmann::json &j)
static bool load_from_disk_locked()