keepassxc/src/gui/csvImport/CsvImportWidget.cpp
Gianluca Recchia da9afd3f6f
Reduce number of unneeded copies
This patch aims at reducing the number of copies for obejcts that could
be referenced rather than copied, because they're not modified during
the computation.
2018-10-31 15:16:04 +01:00

362 lines
14 KiB
C++

/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
* 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"
#include <QFile>
#include <QFileInfo>
#include <QSpacerItem>
#include "core/Clock.h"
#include "format/KeePass2Writer.h"
#include "gui/MessageBox.h"
#include "gui/MessageWidget.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:
// dynamic generation of comboBoxes, labels, placement and so on. Try it for immense fun!
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("Last Modified") << QObject::tr("Created")
/* << QObject::tr("Future field1") */ )
{
m_ui->setupUi(this);
m_ui->comboBoxCodec->addItems(QStringList() << "UTF-8"
<< "Windows-1252"
<< "UTF-16"
<< "UTF-16LE");
m_ui->comboBoxFieldSeparator->addItems(QStringList() << ","
<< ";"
<< "-"
<< ":"
<< "."
<< "TAB (\\t)");
m_fieldSeparatorList = QStringList() << ","
<< ";"
<< "-"
<< ":"
<< "."
<< "\t";
m_ui->comboBoxTextQualifier->addItems(QStringList() << "\""
<< "'"
<< ":"
<< "."
<< "|");
m_ui->comboBoxComment->addItems(QStringList() << "#"
<< ";"
<< ":"
<< "@");
m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus);
m_ui->messageWidget->setHidden(true);
for (int i = 0; i < m_columnHeader.count(); ++i) {
QLabel* label = new QLabel(m_columnHeader.at(i), this);
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QFont font = label->font();
font.setBold(false);
label->setFont(font);
QComboBox* combo = new QComboBox(this);
font = combo->font();
font.setBold(false);
combo->setFont(font);
m_combos.append(combo);
combo->setModel(m_comboModel);
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), [=]{ comboChanged(combo, i); });
#else
connect(combo, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=]{ comboChanged(combo, i); });
#endif
// layout labels and combo fields in column-first order
int combo_rows = 1 + (m_columnHeader.count() - 1) / 2;
int x = i % combo_rows;
int y = 2 * (i / combo_rows);
m_ui->gridLayout_combos->addWidget(label, x, y);
m_ui->gridLayout_combos->addWidget(combo, x, y + 1);
QSpacerItem* item = new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed);
m_ui->gridLayout_combos->addItem(item, x, y + 2);
}
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()));
}
void CsvImportWidget::comboChanged(QComboBox* currentSender, int comboId)
{
if (currentSender->currentIndex() != -1) {
// this line is the one that actually updates GUI table
m_parserModel->mapColumns(currentSender->currentIndex(), comboId);
}
updateTableview();
}
void CsvImportWidget::skippedChanged(int rows)
{
m_parserModel->setSkippedRows(rows);
updateTableview();
}
CsvImportWidget::~CsvImportWidget()
{
}
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));
}
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);
}
}
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);
int emptyId = 0;
QString columnName;
QStringList list(tr("Not present in CSV file"));
for (int i = 1; i < m_parserModel->getCsvCols(); ++i) {
if (m_ui->checkBoxFieldNames->isChecked()) {
columnName = m_parserModel->getCsvTable().at(0).at(i);
if (columnName.isEmpty()) {
columnName = "<" + tr("Empty fieldname %1").arg(++emptyId) + ">";
}
list << columnName;
} else {
list << QString(tr("column %1").arg(i));
}
}
m_comboModel->setStringList(list);
int j = 1;
for (QComboBox* b : m_combos) {
if (j < m_parserModel->getCsvCols()) {
b->setCurrentIndex(j);
} else {
b->setCurrentIndex(0);
}
++j;
}
}
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();
group->setUuid(QUuid::createUuid());
group->setNotes(tr("Imported from CSV file").append("\n").append(tr("Original data: ")) + filename);
parse();
}
void CsvImportWidget::parse()
{
configParser();
QApplication::setOverrideCursor(Qt::WaitCursor);
// QApplication::processEvents();
bool good = m_parserModel->parse();
updatePreview();
QApplication::restoreOverrideCursor();
if (!good) {
m_ui->messageWidget->showMessage(tr("Error(s) detected in CSV file!").append("\n").append(formatStatusText()),
MessageWidget::Warning);
} else {
m_ui->messageWidget->setHidden(true);
}
QWidget::adjustSize();
}
QString CsvImportWidget::formatStatusText() const
{
QString text = m_parserModel->getStatus();
int items = text.count('\n');
if (items > 2) {
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;
}
void CsvImportWidget::writeDatabase()
{
setRootGroup();
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
// 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();
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());
TimeInfo timeInfo;
if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) {
qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong();
if (lastModified) {
timeInfo.setLastModificationTime(Clock::datetimeUtc(lastModified * 1000));
}
}
if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) {
qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong();
if (created) {
timeInfo.setCreationTime(Clock::datetimeUtc(created * 1000));
}
}
entry->setTimeInfo(timeInfo);
}
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
KeePass2Writer writer;
writer.writeDatabase(&buffer, m_db);
if (writer.hasError()) {
MessageBox::warning(this,
tr("Error"),
tr("CSV import: writer has errors:\n%1").arg(writer.errorString()),
QMessageBox::Ok,
QMessageBox::Ok);
}
emit editFinished(true);
}
void CsvImportWidget::setRootGroup()
{
QString groupLabel;
QStringList groupList;
bool is_root = false;
bool is_empty = false;
bool is_label = false;
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
// 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();
// 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))
is_root = true;
else if (not groupLabel.compare(""))
is_empty = true;
else
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)
{
// 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);
// 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) {
Group* children = hasChildren(current, groupName);
if (children == nullptr) {
Group* brandNew = new Group();
brandNew->setParent(current);
brandNew->setName(groupName);
brandNew->setUuid(QUuid::createUuid());
current = brandNew;
} else {
Q_ASSERT(children != nullptr);
current = children;
}
}
return current;
}
Group* CsvImportWidget::hasChildren(Group* current, const QString& groupName)
{
// 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;
}
void CsvImportWidget::reject()
{
emit editFinished(false);
}