keepassxc/src/gui/csvImport/CsvImportWidget.cpp

356 lines
13 KiB
C++
Raw Normal View History

/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
2017-06-09 21:40:36 +00:00
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CsvImportWidget.h"
#include "ui_CsvImportWidget.h"
2021-07-12 02:10:29 +00:00
#include <QStringListModel>
#include "core/Clock.h"
#include "format/KeePass2Writer.h"
#include "gui/MessageBox.h"
#include "totp/totp.h"
// I wanted to make the CSV import GUI future-proof, so if one day you need a new field,
// all you have to do is add a field to m_columnHeader, and the GUI will follow:
2018-03-31 20:01:30 +00:00
// dynamic generation of comboBoxes, labels, placement and so on. Try it for immense fun!
2018-03-31 20:01:30 +00:00
CsvImportWidget::CsvImportWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::CsvImportWidget())
, m_parserModel(new CsvParserModel(this))
, m_comboModel(new QStringListModel(this))
, m_columnHeader(QStringList() << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username")
<< QObject::tr("Password") << QObject::tr("URL") << QObject::tr("Notes")
<< QObject::tr("TOTP") << QObject::tr("Icon") << QObject::tr("Last Modified")
<< QObject::tr("Created"))
2020-06-21 13:16:33 +00:00
, m_fieldSeparatorList(QStringList() << ","
<< ";"
<< "-"
<< ":"
<< "."
2020-06-21 13:16:33 +00:00
<< "\t")
{
m_ui->setupUi(this);
m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus);
m_ui->messageWidget->setHidden(true);
2020-06-21 13:16:33 +00:00
m_combos << m_ui->groupCombo << m_ui->titleCombo << m_ui->usernameCombo << m_ui->passwordCombo << m_ui->urlCombo
<< m_ui->notesCombo << m_ui->totpCombo << m_ui->iconCombo << m_ui->lastModifiedCombo << m_ui->createdCombo;
2020-06-21 13:16:33 +00:00
for (auto combo : m_combos) {
combo->setModel(m_comboModel);
2020-06-21 13:16:33 +00:00
connect(combo, SIGNAL(currentIndexChanged(int)), SLOT(comboChanged(int)));
}
m_parserModel->setHeaderLabels(m_columnHeader);
m_ui->tableViewFields->setModel(m_parserModel);
connect(m_ui->spinBoxSkip, SIGNAL(valueChanged(int)), SLOT(skippedChanged(int)));
connect(m_ui->comboBoxCodec, SIGNAL(currentIndexChanged(int)), SLOT(parse()));
connect(m_ui->comboBoxTextQualifier, SIGNAL(currentIndexChanged(int)), SLOT(parse()));
connect(m_ui->comboBoxComment, SIGNAL(currentIndexChanged(int)), SLOT(parse()));
connect(m_ui->comboBoxFieldSeparator, SIGNAL(currentIndexChanged(int)), SLOT(parse()));
connect(m_ui->checkBoxBackslash, SIGNAL(toggled(bool)), SLOT(parse()));
connect(m_ui->checkBoxFieldNames, SIGNAL(toggled(bool)), SLOT(updatePreview()));
connect(m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(writeDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
}
2020-06-21 13:16:33 +00:00
void CsvImportWidget::comboChanged(int index)
2018-03-31 20:01:30 +00:00
{
2020-06-21 13:16:33 +00:00
// this line is the one that actually updates GUI table
m_parserModel->mapColumns(index, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
updateTableview();
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::skippedChanged(int rows)
{
m_parserModel->setSkippedRows(rows);
updateTableview();
}
2018-03-31 20:01:30 +00:00
CsvImportWidget::~CsvImportWidget()
{
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::configParser()
{
m_parserModel->setBackslashSyntax(m_ui->checkBoxBackslash->isChecked());
m_parserModel->setComment(m_ui->comboBoxComment->currentText().at(0));
m_parserModel->setTextQualifier(m_ui->comboBoxTextQualifier->currentText().at(0));
m_parserModel->setCodec(m_ui->comboBoxCodec->currentText());
m_parserModel->setFieldSeparator(m_fieldSeparatorList.at(m_ui->comboBoxFieldSeparator->currentIndex()).at(0));
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::updateTableview()
{
m_ui->tableViewFields->resizeRowsToContents();
m_ui->tableViewFields->resizeColumnsToContents();
for (int c = 0; c < m_ui->tableViewFields->horizontalHeader()->count(); ++c) {
m_ui->tableViewFields->horizontalHeader()->setSectionResizeMode(c, QHeaderView::Stretch);
}
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::updatePreview()
{
int minSkip = 0;
if (m_ui->checkBoxFieldNames->isChecked()) {
minSkip = 1;
}
m_ui->labelSizeRowsCols->setText(m_parserModel->getFileInfo());
m_ui->spinBoxSkip->setRange(minSkip, qMax(minSkip, m_parserModel->rowCount() - 1));
m_ui->spinBoxSkip->setValue(minSkip);
2020-06-21 13:16:33 +00:00
QStringList list(tr("Not Present"));
2017-04-27 20:11:26 +00:00
for (int i = 1; i < m_parserModel->getCsvCols(); ++i) {
if (m_ui->checkBoxFieldNames->isChecked()) {
2020-06-21 13:16:33 +00:00
auto columnName = m_parserModel->getCsvTable().at(0).at(i);
if (columnName.isEmpty()) {
2020-06-21 13:16:33 +00:00
list << QString(tr("Column %1").arg(i));
} else {
list << columnName;
}
} else {
2020-06-21 13:16:33 +00:00
list << QString(tr("Column %1").arg(i));
}
}
m_comboModel->setStringList(list);
2018-03-31 20:01:30 +00:00
int j = 1;
for (QComboBox* b : m_combos) {
if (j < m_parserModel->getCsvCols()) {
2017-04-27 20:11:26 +00:00
b->setCurrentIndex(j);
} else {
b->setCurrentIndex(0);
}
2017-04-27 20:11:26 +00:00
++j;
}
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::load(const QString& filename, Database* const db)
{
// QApplication::processEvents();
m_db = db;
m_parserModel->setFilename(filename);
m_ui->labelFilename->setText(filename);
Group* group = m_db->rootGroup();
2018-03-22 21:56:05 +00:00
group->setUuid(QUuid::createUuid());
group->setNotes(tr("Imported from CSV file").append("\n").append(tr("Original data: ")) + filename);
parse();
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::parse()
{
configParser();
QApplication::setOverrideCursor(Qt::WaitCursor);
2018-03-31 20:01:30 +00:00
// QApplication::processEvents();
bool good = m_parserModel->parse();
updatePreview();
QApplication::restoreOverrideCursor();
if (!good) {
2018-03-31 20:01:30 +00:00
m_ui->messageWidget->showMessage(tr("Error(s) detected in CSV file!").append("\n").append(formatStatusText()),
MessageWidget::Warning);
} else {
m_ui->messageWidget->setHidden(true);
}
}
2018-03-31 20:01:30 +00:00
QString CsvImportWidget::formatStatusText() const
{
QString text = m_parserModel->getStatus();
int items = text.count('\n');
if (items > 2) {
2018-03-31 20:01:30 +00:00
return text.section('\n', 0, 1).append("\n").append(tr("[%n more message(s) skipped]", "", items - 2));
}
if (items == 1) {
text.append(QString("\n"));
}
return text;
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::writeDatabase()
{
setRootGroup();
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
2018-03-31 20:01:30 +00:00
// use validity of second column as a GO/NOGO for all others fields
if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid()) {
continue;
}
Entry* entry = new Entry();
2018-03-22 21:56:05 +00:00
entry->setUuid(QUuid::createUuid());
entry->setGroup(splitGroups(m_parserModel->data(m_parserModel->index(r, 0)).toString()));
entry->setTitle(m_parserModel->data(m_parserModel->index(r, 1)).toString());
entry->setUsername(m_parserModel->data(m_parserModel->index(r, 2)).toString());
entry->setPassword(m_parserModel->data(m_parserModel->index(r, 3)).toString());
entry->setUrl(m_parserModel->data(m_parserModel->index(r, 4)).toString());
entry->setNotes(m_parserModel->data(m_parserModel->index(r, 5)).toString());
auto otpString = m_parserModel->data(m_parserModel->index(r, 6));
if (otpString.isValid() && !otpString.toString().isEmpty()) {
auto totp = Totp::parseSettings(otpString.toString());
if (totp->key.isEmpty()) {
// Bare secret, use default TOTP settings
totp = Totp::parseSettings({}, otpString.toString());
}
entry->setTotp(totp);
}
bool ok;
int icon = m_parserModel->data(m_parserModel->index(r, 7)).toInt(&ok);
if (ok) {
entry->setIcon(icon);
}
TimeInfo timeInfo;
if (m_parserModel->data(m_parserModel->index(r, 8)).isValid()) {
auto datetime = m_parserModel->data(m_parserModel->index(r, 8)).toString();
if (datetime.contains(QRegularExpression("^\\d+$"))) {
auto t = datetime.toLongLong();
if (t <= INT32_MAX) {
t *= 1000;
}
auto lastModified = Clock::datetimeUtc(t);
timeInfo.setLastModificationTime(lastModified);
timeInfo.setLastAccessTime(lastModified);
} else {
auto lastModified = QDateTime::fromString(datetime, Qt::ISODate);
if (lastModified.isValid()) {
timeInfo.setLastModificationTime(lastModified);
timeInfo.setLastAccessTime(lastModified);
}
}
}
if (m_parserModel->data(m_parserModel->index(r, 9)).isValid()) {
auto datetime = m_parserModel->data(m_parserModel->index(r, 9)).toString();
if (datetime.contains(QRegularExpression("^\\d+$"))) {
auto t = datetime.toLongLong();
if (t <= INT32_MAX) {
t *= 1000;
}
timeInfo.setCreationTime(Clock::datetimeUtc(t));
} else {
auto created = QDateTime::fromString(datetime, Qt::ISODate);
if (created.isValid()) {
timeInfo.setCreationTime(created);
}
}
}
entry->setTimeInfo(timeInfo);
}
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
KeePass2Writer writer;
writer.writeDatabase(&buffer, m_db);
if (writer.hasError()) {
2018-03-31 20:01:30 +00:00
MessageBox::warning(this,
tr("Error"),
tr("CSV import: writer has errors:\n%1").arg(writer.errorString()),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-20 04:14:11 +00:00
MessageBox::Ok,
MessageBox::Ok);
}
emit editFinished(true);
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::setRootGroup()
{
QString groupLabel;
QStringList groupList;
2018-03-31 20:01:30 +00:00
bool is_root = false;
bool is_empty = false;
bool is_label = false;
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
2018-03-31 20:01:30 +00:00
// use validity of second column as a GO/NOGO for all others fields
if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid()) {
continue;
}
groupLabel = m_parserModel->data(m_parserModel->index(r, 0)).toString();
2018-03-31 20:01:30 +00:00
// check if group name is either "root", "" (empty) or some other label
groupList = groupLabel.split("/", QString::SkipEmptyParts);
if (groupList.isEmpty()) {
is_empty = true;
} else if (not groupList.first().compare("Root", Qt::CaseSensitive)) {
2018-03-31 20:01:30 +00:00
is_root = true;
} else if (not groupLabel.compare("")) {
2018-03-31 20:01:30 +00:00
is_empty = true;
} else {
2018-03-31 20:01:30 +00:00
is_label = true;
}
groupList.clear();
}
if ((is_empty and is_root) or (is_label and not is_empty and is_root)) {
m_db->rootGroup()->setName("CSV IMPORTED");
} else {
m_db->rootGroup()->setName("Root");
}
}
Group* CsvImportWidget::splitGroups(const QString& label)
2018-03-31 20:01:30 +00:00
{
// extract group names from nested path provided in "label"
Group* current = m_db->rootGroup();
if (label.isEmpty()) {
return current;
}
QStringList groupList = label.split("/", QString::SkipEmptyParts);
2018-03-31 20:01:30 +00:00
// avoid the creation of a subgroup with the same name as Root
if (m_db->rootGroup()->name() == "Root" && groupList.first() == "Root") {
groupList.removeFirst();
}
for (const QString& groupName : groupList) {
2018-03-31 20:01:30 +00:00
Group* children = hasChildren(current, groupName);
if (children == nullptr) {
2018-03-31 20:01:30 +00:00
Group* brandNew = new Group();
brandNew->setParent(current);
brandNew->setName(groupName);
2018-03-22 21:56:05 +00:00
brandNew->setUuid(QUuid::createUuid());
current = brandNew;
} else {
Q_ASSERT(children != nullptr);
current = children;
}
}
return current;
}
Group* CsvImportWidget::hasChildren(Group* current, const QString& groupName)
2018-03-31 20:01:30 +00:00
{
// returns the group whose name is "groupName" and is child of "current" group
for (Group* group : current->children()) {
if (group->name() == groupName) {
return group;
}
}
return nullptr;
}
2018-03-31 20:01:30 +00:00
void CsvImportWidget::reject()
{
emit editFinished(false);
}