mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-04-03 04:27:39 +03:00
* Deprecated qSort() -> std::sort() * Replace QDateTime::toString(Qt::DefaultLocaleShortDate) with Clock::toString() * Replace QDateTime::toString(Qt::SystemLocaleShortDate) with QLocale::system().toString(..., QLocale::ShortFormat) * Use QDateTime::startOfDay() instead of QDate(QDateTime) Note: QDateTime::startOfDay() is only available in Qt 5.14, we need to guard it * Replace QString::SkipEmptyParts with Qt::SkipEmptyParts Note: Its designated replacement, Qt::SplitBehavior, was only added in Qt 5.14. * Don't call deprecated QFlags(nullptr) constructor * QSet::{toList->values} * Replace QList::toSet, QSet::fromList with Tools::asSet() * QHash::insertMulti -> QMultiHash::insert * QProcess::startDetached: non-deprecated overload * QProcess::{pid->processId} * QPainter::{HighQuality->}Antialiasing * QPalette::{background->window}() * Use Qt::{Background,Foreground}Role * endl -> Qt::endl, flush -> Qt::flush * Make YubiKey::s_interfaceMutex non-recursive * OpenSSHKeyGenDialog: use non-deprecated QComboBox::sizeAdjustPolicy setting
320 lines
11 KiB
C++
320 lines
11 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 "core/Clock.h"
|
|
#include "core/Database.h"
|
|
#include "core/Group.h"
|
|
#include "core/Totp.h"
|
|
#include "format/CsvParser.h"
|
|
#include "format/KeePass2Writer.h"
|
|
#include "gui/MessageBox.h"
|
|
#include "gui/csvImport/CsvParserModel.h"
|
|
|
|
#include <QStringListModel>
|
|
|
|
namespace
|
|
{
|
|
// Extract group names from nested path and return the last group created
|
|
Group* createGroupStructure(Database* db, const QString& groupPath)
|
|
{
|
|
auto group = db->rootGroup();
|
|
if (!group || groupPath.isEmpty()) {
|
|
return group;
|
|
}
|
|
|
|
auto nameList = groupPath.split("/", Qt::SkipEmptyParts);
|
|
// Skip over first group name if root
|
|
if (nameList.first().compare("root", Qt::CaseInsensitive) == 0) {
|
|
nameList.removeFirst();
|
|
}
|
|
|
|
for (const auto& name : qAsConst(nameList)) {
|
|
auto child = group->findChildByName(name);
|
|
if (!child) {
|
|
auto newGroup = new Group();
|
|
newGroup->setUuid(QUuid::createUuid());
|
|
newGroup->setName(name);
|
|
newGroup->setParent(group);
|
|
group = newGroup;
|
|
} else {
|
|
group = child;
|
|
}
|
|
}
|
|
return group;
|
|
}
|
|
} // namespace
|
|
|
|
CsvImportWidget::CsvImportWidget(QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_ui(new Ui::CsvImportWidget())
|
|
, m_parserModel(new CsvParserModel(this))
|
|
, m_comboModel(new QStringListModel(this))
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection);
|
|
m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus);
|
|
|
|
m_columnHeader << 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");
|
|
|
|
m_fieldSeparatorList << ","
|
|
<< ";"
|
|
<< "-"
|
|
<< ":"
|
|
<< "."
|
|
<< "\t";
|
|
|
|
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;
|
|
|
|
for (auto combo : m_combos) {
|
|
combo->setModel(m_comboModel);
|
|
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()));
|
|
}
|
|
|
|
void CsvImportWidget::comboChanged(int index)
|
|
{
|
|
// this line is the one that actually updates GUI table
|
|
m_parserModel->mapColumns(index - 1, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
|
|
updateTableview();
|
|
}
|
|
|
|
void CsvImportWidget::skippedChanged(int rows)
|
|
{
|
|
m_parserModel->setSkippedRows(rows);
|
|
updateTableview();
|
|
}
|
|
|
|
CsvImportWidget::~CsvImportWidget() = default;
|
|
|
|
void CsvImportWidget::configParser()
|
|
{
|
|
auto parser = m_parserModel->parser();
|
|
parser->setBackslashSyntax(m_ui->checkBoxBackslash->isChecked());
|
|
parser->setComment(m_ui->comboBoxComment->currentText().at(0));
|
|
parser->setTextQualifier(m_ui->comboBoxTextQualifier->currentText().at(0));
|
|
parser->setCodec(m_ui->comboBoxCodec->currentText());
|
|
parser->setFieldSeparator(m_fieldSeparatorList.at(m_ui->comboBoxFieldSeparator->currentIndex()).at(0));
|
|
}
|
|
|
|
void CsvImportWidget::updateTableview()
|
|
{
|
|
if (!m_buildingPreview) {
|
|
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()
|
|
{
|
|
m_buildingPreview = true;
|
|
|
|
int minSkip = m_ui->checkBoxFieldNames->isChecked() ? 1 : 0;
|
|
m_ui->labelSizeRowsCols->setText(m_parserModel->getFileInfo());
|
|
m_ui->spinBoxSkip->setRange(minSkip, qMax(minSkip, m_parserModel->rowCount() - 1));
|
|
m_ui->spinBoxSkip->setValue(minSkip);
|
|
|
|
QStringList csvColumns(tr("Not Present"));
|
|
auto parser = m_parserModel->parser();
|
|
for (int i = 0; i < parser->getCsvCols(); ++i) {
|
|
if (m_ui->checkBoxFieldNames->isChecked()) {
|
|
auto columnName = parser->getCsvTable().at(0).at(i);
|
|
if (columnName.isEmpty()) {
|
|
csvColumns << QString(tr("Column %1").arg(i));
|
|
} else {
|
|
csvColumns << columnName;
|
|
}
|
|
} else {
|
|
csvColumns << QString(tr("Column %1").arg(i));
|
|
}
|
|
}
|
|
m_comboModel->setStringList(csvColumns);
|
|
|
|
// Try to match named columns to the combo boxes
|
|
for (int i = 0; i < m_columnHeader.size(); ++i) {
|
|
if (i >= m_combos.size()) {
|
|
// This should not happen, it is a programming error otherwise
|
|
Q_ASSERT(false);
|
|
break;
|
|
}
|
|
|
|
bool found = false;
|
|
for (int j = 0; j < csvColumns.size(); ++j) {
|
|
if (m_columnHeader.at(i).compare(csvColumns.at(j), Qt::CaseInsensitive) == 0) {
|
|
m_combos.at(i)->setCurrentIndex(j);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
// Named column not found, default to "Not Present"
|
|
if (!found) {
|
|
m_combos.at(i)->setCurrentIndex(0);
|
|
}
|
|
}
|
|
|
|
m_buildingPreview = false;
|
|
updateTableview();
|
|
}
|
|
|
|
void CsvImportWidget::load(const QString& filename)
|
|
{
|
|
m_filename = filename;
|
|
m_parserModel->setFilename(filename);
|
|
parse();
|
|
}
|
|
|
|
void CsvImportWidget::parse()
|
|
{
|
|
configParser();
|
|
QApplication::setOverrideCursor(Qt::WaitCursor);
|
|
QApplication::processEvents();
|
|
bool good = m_parserModel->parse();
|
|
updatePreview();
|
|
QApplication::restoreOverrideCursor();
|
|
if (!good) {
|
|
emit message(tr("Failed to parse CSV file: %1").arg(formatStatusText()));
|
|
}
|
|
}
|
|
|
|
QSharedPointer<Database> CsvImportWidget::buildDatabase()
|
|
{
|
|
// Warn if the title column wasn't specified
|
|
if (m_combos[1]->currentIndex() == 0) {
|
|
auto ans = MessageBox::question(
|
|
this,
|
|
tr("No Title Selected"),
|
|
tr("No title column was selected, entries will be hard to tell apart.\nAre you sure you want to import?"),
|
|
MessageBox::Continue | MessageBox::Cancel);
|
|
if (ans == MessageBox::Cancel) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
auto db = QSharedPointer<Database>::create();
|
|
db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename));
|
|
|
|
auto rows = m_parserModel->rowCount() - m_parserModel->skippedRows();
|
|
for (int r = 0; r < rows; ++r) {
|
|
auto group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString());
|
|
if (!group) {
|
|
continue;
|
|
}
|
|
|
|
// Standard entry fields
|
|
auto entry = new Entry();
|
|
entry->setUuid(QUuid::createUuid());
|
|
entry->setGroup(group);
|
|
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());
|
|
|
|
// TOTP
|
|
auto otpString = m_parserModel->data(m_parserModel->index(r, 6));
|
|
if (otpString.isValid() && !otpString.toString().isEmpty()) {
|
|
auto totp = Totp::parseSettings(otpString.toString());
|
|
if (!totp || totp->key.isEmpty()) {
|
|
// Bare secret, use default TOTP settings
|
|
totp = Totp::parseSettings({}, otpString.toString());
|
|
}
|
|
entry->setTotp(totp);
|
|
}
|
|
|
|
// Icon
|
|
bool ok;
|
|
int icon = m_parserModel->data(m_parserModel->index(r, 7)).toInt(&ok);
|
|
if (ok) {
|
|
entry->setIcon(icon);
|
|
}
|
|
|
|
// Modified Time
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// Creation Time
|
|
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);
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
QString CsvImportWidget::formatStatusText() const
|
|
{
|
|
QString text = m_parserModel->parser()->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;
|
|
}
|