Skip to content
API Documentation
BastionGuard™
PasswordImportDialog.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
22#include "PasswordImporter.hpp"
23#include <glibmm/i18n.h>
24#include <fstream>
25#include <sstream>
26
27
29: parent_(parent),
30 mainBox_(Gtk::Orientation::VERTICAL, 10),
31 hintLabel_(_("Seleziona un file CSV esportato da Chrome, Edge, Firefox o Safari.")),
32 chooseFileBtn_(_("📂 Scegli file CSV…")),
33 progressBox_(Gtk::Orientation::VERTICAL, 4),
34 importBtn_(_("⬆ Importa credenziali"))
35{
36 dialog_ = new Gtk::Dialog(_("Importa credenziali da browser"), parent, true);
37 dialog_->set_default_size(720, 560);
38
39 dispProgress_.connect(sigc::mem_fun(*this, &PasswordImportDialog::onProgressTick));
40 dispDone_.connect(sigc::mem_fun(*this, &PasswordImportDialog::onImportDone));
41
42 buildUi();
43}
44
49
52
54 auto* content = dialog_->get_content_area();
55 content->set_spacing(0);
56 content->set_margin(0);
57
58 mainBox_.set_margin(16);
59 mainBox_.set_spacing(10);
60
61 hintLabel_.set_xalign(0.0f);
62 hintLabel_.set_wrap(true);
63 mainBox_.append(hintLabel_);
64
65 auto* fileRow = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
66 fileLabel_.set_xalign(0.0f);
67 fileLabel_.set_hexpand(true);
68 fileLabel_.set_ellipsize(Pango::EllipsizeMode::MIDDLE);
69 fileRow->append(chooseFileBtn_);
70 fileRow->append(fileLabel_);
71 mainBox_.append(*fileRow);
72
73 auto* infoRow = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 16);
74 formatLabel_.set_xalign(0.0f);
75 countLabel_.set_xalign(0.0f);
76 infoRow->append(formatLabel_);
77 infoRow->append(countLabel_);
78 mainBox_.append(*infoRow);
79
80 mainBox_.append(*Gtk::make_managed<Gtk::Separator>(Gtk::Orientation::HORIZONTAL));
81
82 auto* previewLabel = Gtk::make_managed<Gtk::Label>(
83 _("Anteprima (prime 50 righe — tutte verranno importate):"));
84 previewLabel->set_xalign(0.0f);
85 previewLabel->get_style_context()->add_class("heading");
86 mainBox_.append(*previewLabel);
87
88 previewList_.set_selection_mode(Gtk::SelectionMode::NONE);
90 previewScrolled_.set_vexpand(true);
91 previewScrolled_.set_min_content_height(240);
93 progressLabel_.set_xalign(0.0f);
94 progressBar_.set_show_text(false);
95 progressBar_.set_fraction(0.0);
98 progressBox_.set_visible(false);
99 mainBox_.append(progressBox_);
100
101 content->append(mainBox_);
102
103 dialog_->add_button(_("Chiudi"), Gtk::ResponseType::CANCEL);
104
105 importBtn_.set_sensitive(false);
107 content->append(importBtn_);
108
109 chooseFileBtn_.signal_clicked().connect(
110 sigc::mem_fun(*this, &PasswordImportDialog::onChooseFile));
111 importBtn_.signal_clicked().connect(
112 sigc::mem_fun(*this, &PasswordImportDialog::onImportClicked));
113
114 dialog_->signal_response().connect([this](int r) {
115 if ((r == Gtk::ResponseType::CANCEL || r == Gtk::ResponseType::DELETE_EVENT)
116 && !importing_)
117 dialog_->hide();
118 });
119}
120
122 importBtn_.get_style_context()->add_class("suggested-action");
123 auto provider = Gtk::CssProvider::create();
124 provider->load_from_data(
125 "button.suggested-action {"
126 " background: linear-gradient(to bottom, #33d17a, #26a269);"
127 " color: white; font-weight: bold;"
128 " padding: 8px 24px; border-radius: 6px;"
129 " border: 1px solid #1e8449;"
130 "}");
131 importBtn_.get_style_context()->add_provider(
132 provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
133}
134
135
137 auto* fc = new Gtk::FileChooserDialog(
138 _("Seleziona file CSV"), Gtk::FileChooser::Action::OPEN);
139 fc->set_transient_for(*dialog_);
140 fc->set_modal(true);
141
142 auto filter = Gtk::FileFilter::create();
143 filter->set_name(_("File CSV (*.csv)"));
144 filter->add_pattern("*.csv");
145 filter->add_pattern("*.CSV");
146 fc->add_filter(filter);
147
148 auto filterAll = Gtk::FileFilter::create();
149 filterAll->set_name(_("Tutti i file"));
150 filterAll->add_pattern("*");
151 fc->add_filter(filterAll);
152
153 fc->add_button(_("Annulla"), Gtk::ResponseType::CANCEL);
154 fc->add_button(_("Apri"), Gtk::ResponseType::OK);
155
156 fc->signal_response().connect([this, fc](int r) {
157 if (r == Gtk::ResponseType::OK) {
158 const auto file = fc->get_file();
159 if (file) loadFile(file->get_path());
160 }
161 fc->hide();
162 delete fc;
163 });
164 fc->present();
165}
166
167
168void PasswordImportDialog::loadFile(const std::string& path) {
169 std::string errorMsg;
171
172 const auto slash = path.rfind('/');
173 fileLabel_.set_text(slash != std::string::npos ? path.substr(slash + 1) : path);
174
175 if (!errorMsg.empty() || parsed_.empty()) {
176 const std::string msg = errorMsg.empty() ? "Nessuna credenziale trovata." : errorMsg;
177 formatLabel_.set_markup(
178 std::string("<span foreground='red'>⚠ ") +
179 Glib::Markup::escape_text(msg) + "</span>");
180 countLabel_.set_text("");
181 importBtn_.set_sensitive(false);
182 while (auto* child = previewList_.get_first_child())
183 previewList_.remove(*child);
184 return;
185 }
186
187 std::ifstream probe(path);
188 std::string firstLine;
189 std::getline(probe, firstLine);
190 if (!firstLine.empty() && firstLine.back() == '\r') firstLine.pop_back();
191
192 const auto fmt = PasswordImporter::detectFormat(firstLine);
193 std::string fmtName;
194 switch (fmt) {
195 case BrowserFormat::Chrome: fmtName = "Chrome"; break;
196 case BrowserFormat::Edge: fmtName = "Edge"; break;
197 case BrowserFormat::Firefox: fmtName = "Firefox"; break;
198 case BrowserFormat::Safari: fmtName = "Safari"; break;
199 default: fmtName = "?"; break;
200 }
201
202 formatLabel_.set_markup(
203 std::string("<b>") + std::string(_("Browser: ")) + "</b>" + fmtName);
204 countLabel_.set_markup(
205 std::string("<b>") + std::string(_("Credenziali trovate: ")) + "</b>" +
206 std::to_string(parsed_.size()));
207
208 importBtn_.set_sensitive(true);
210}
211
212
214 while (auto* child = previewList_.get_first_child())
215 previewList_.remove(*child);
216
217 // Header
218 {
219 auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
220 auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0);
221 box->set_margin(6);
222 auto makeHdr = [](const std::string& t, int chars) {
223 auto* l = Gtk::make_managed<Gtk::Label>();
224 l->set_markup("<b>" + Glib::Markup::escape_text(t) + "</b>");
225 l->set_xalign(0.0f);
226 l->set_width_chars(chars);
227 l->set_max_width_chars(chars);
228 l->set_ellipsize(Pango::EllipsizeMode::END);
229 return l;
230 };
231 box->append(*makeHdr(_("Nome / Sito"), 26));
232 box->append(*makeHdr(_("Username"), 22));
233 box->append(*makeHdr(_("URL"), 28));
234 row->set_child(*box);
235 row->set_activatable(false);
236 previewList_.append(*row);
237 }
238
239 const size_t limit = std::min(parsed_.size(), size_t(50));
240 for (size_t i = 0; i < limit; ++i) {
241 const auto& c = parsed_[i];
242 auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
243 auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0);
244 box->set_margin_top(3); box->set_margin_bottom(3);
245 box->set_margin_start(6); box->set_margin_end(6);
246 auto makeCell = [](const std::string& t, int chars) {
247 auto* l = Gtk::make_managed<Gtk::Label>(t);
248 l->set_xalign(0.0f);
249 l->set_width_chars(chars);
250 l->set_max_width_chars(chars);
251 l->set_ellipsize(Pango::EllipsizeMode::END);
252 return l;
253 };
254 const std::string name = c.name.empty()
255 ? PasswordImporter::domainFromUrl(c.url) : c.name;
256 box->append(*makeCell(name, 26));
257 box->append(*makeCell(c.username, 22));
258 box->append(*makeCell(c.url, 28));
259 row->set_child(*box);
260 row->set_activatable(false);
261 previewList_.append(*row);
262 }
263
264 if (parsed_.size() > limit) {
265 auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
266 auto* lbl = Gtk::make_managed<Gtk::Label>(
267 std::string("… ") + std::to_string(parsed_.size() - limit) +
268 std::string(_(" altre (tutte verranno importate)")));
269 lbl->set_margin(6);
270 lbl->get_style_context()->add_class("dim-label");
271 row->set_child(*lbl);
272 row->set_activatable(false);
273 previewList_.append(*row);
274 }
275}
276
277
279 if (parsed_.empty() || importing_) return;
280
281 importing_ = true;
282 progressDone_ = 0;
283 progressTotal_ = static_cast<int>(parsed_.size());
284
285 progressBar_.set_fraction(0.0);
286 progressLabel_.set_text(
287 std::string(_("Preparazione: ")) +
288 std::to_string(progressTotal_) +
289 std::string(_(" credenziali…")));
290 progressBox_.set_visible(true);
291 importBtn_.set_sensitive(false);
292 chooseFileBtn_.set_sensitive(false);
293 dialog_->set_response_sensitive(Gtk::ResponseType::CANCEL, false);
294 auto creds = parsed_;
295
296 importThread_ = std::thread([this, creds]() mutable {
297 pendingItems_.clear();
298 pendingItems_.reserve(creds.size());
299
300 for (auto& c : creds) {
302 pendingItems_.emplace_back(std::move(e), std::move(c.password));
304 dispProgress_.emit();
305 }
306
307 dispDone_.emit();
308 });
309}
310
312 const int done = progressDone_.load();
313 const int total = progressTotal_.load();
314 if (total > 0) {
315 progressBar_.set_fraction(static_cast<double>(done) / total);
316 progressLabel_.set_text(
317 std::string(_("Preparazione: ")) + std::to_string(done) +
318 " / " + std::to_string(total));
319 }
320}
321
323 if (importThread_.joinable()) importThread_.join();
324
325 progressBar_.set_fraction(1.0);
326 progressLabel_.set_text(_("Salvataggio nel vault…"));
327 importing_ = false;
328 signalImportConfirmed_.emit(std::move(pendingItems_));
329}
330
331sigc::signal<void(std::vector<PasswordImportDialog::BulkItem>)>
#define _(s)
PasswordImportDialog(Gtk::Window &parent)
std::atomic< bool > importing_
void loadFile(const std::string &path)
std::atomic< int > progressDone_
sigc::signal< void(std::vector< BulkItem >)> signal_import_confirmed()
std::vector< ImportedCredential > parsed_
Gtk::ScrolledWindow previewScrolled_
sigc::signal< void(std::vector< BulkItem >)> signalImportConfirmed_
std::vector< BulkItem > pendingItems_
std::atomic< int > progressTotal_
static std::string domainFromUrl(const std::string &url)
static BrowserFormat detectFormat(const std::string &headerLine)
static std::vector< ImportedCredential > importFromFile(const std::string &path, std::string &errorMsg)
static PasswordEntry toPasswordEntry(const ImportedCredential &cred)