Add 1Password 1PUX and Bitwarden JSON Importers

* Closes #7545 - Support 1Password 1PUX import format based on https://support.1password.com/1pux-format/

* Closes #8367 - Support Bitwarden JSON import format (both unencrypted and encrypted) based on https://bitwarden.com/help/encrypted-export/

* Fixes #9577 - OPVault import when fields have the same name or type

* Introduce the import wizard to handle all import tasks (CSV, KDBX1, OPVault, 1PUX, JSON)

* Clean up CSV parser code to make it much more efficient and easier to read

* Combine all importer tests (except CSV) into one test file
This commit is contained in:
Jonathan White 2023-08-27 21:54:53 -04:00
parent a02bceabd2
commit e700195f0a
70 changed files with 3562 additions and 1876 deletions

View file

@ -71,6 +71,7 @@ set(keepassx_SOURCES
crypto/kdf/Kdf.cpp
crypto/kdf/AesKdf.cpp
crypto/kdf/Argon2Kdf.cpp
format/BitwardenReader.cpp
format/CsvExporter.cpp
format/CsvParser.cpp
format/KeePass1Reader.cpp
@ -87,6 +88,7 @@ set(keepassx_SOURCES
format/Kdbx4Writer.cpp
format/KdbxXmlWriter.cpp
format/OpData01.cpp
format/OPUXReader.cpp
format/OpVaultReader.cpp
format/OpVaultReaderAttachments.cpp
format/OpVaultReaderBandEntry.cpp
@ -118,12 +120,10 @@ set(keepassx_SOURCES
gui/GuiTools.cpp
gui/HtmlExporter.cpp
gui/IconModels.cpp
gui/KeePass1OpenWidget.cpp
gui/KMessageWidget.cpp
gui/MainWindow.cpp
gui/MessageBox.cpp
gui/MessageWidget.cpp
gui/OpVaultOpenWidget.cpp
gui/PasswordWidget.cpp
gui/PasswordGeneratorWidget.cpp
gui/ApplicationSettingsWidget.cpp
@ -139,7 +139,6 @@ set(keepassx_SOURCES
gui/URLEdit.cpp
gui/WelcomeWidget.cpp
gui/csvImport/CsvImportWidget.cpp
gui/csvImport/CsvImportWizard.cpp
gui/csvImport/CsvParserModel.cpp
gui/entry/AutoTypeAssociationsModel.cpp
gui/entry/EditEntryWidget.cpp
@ -183,6 +182,9 @@ set(keepassx_SOURCES
gui/widgets/KPToolBar.cpp
gui/widgets/PopupHelpWidget.cpp
gui/widgets/ShortcutWidget.cpp
gui/wizard/ImportWizard.cpp
gui/wizard/ImportWizardPageReview.cpp
gui/wizard/ImportWizardPageSelect.cpp
gui/wizard/NewDatabaseWizard.cpp
gui/wizard/NewDatabaseWizardPage.cpp
gui/wizard/NewDatabaseWizardPageMetaData.cpp
@ -390,6 +392,7 @@ target_link_libraries(keepassx_core
${PCSC_LIBRARIES}
${ZXCVBN_LIBRARIES}
${ZLIB_LIBRARIES}
${MINIZIP_LIBRARIES}
${ARGON2_LIBRARIES}
${KEYUTILS_LIBRARIES}
${thirdparty_LIBRARIES}

View file

@ -21,6 +21,7 @@
#include <QApplication>
#include <QDebug>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QUrl>
#include "config-keepassx.h"

View file

@ -0,0 +1,313 @@
/*
* Copyright (C) 2023 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 "BitwardenReader.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Tools.h"
#include "core/Totp.h"
#include "crypto/CryptoHash.h"
#include "crypto/SymmetricCipher.h"
#include <botan/kdf.h>
#include <botan/pwdhash.h>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QMap>
#include <QScopedPointer>
namespace
{
Entry* readItem(const QJsonObject& item, QString& folderId)
{
// Create the item map and extract the folder id
const auto itemMap = item.toVariantMap();
folderId = itemMap.value("folderId").toString();
// Create entry and assign basic values
QScopedPointer<Entry> entry(new Entry());
entry->setUuid(QUuid::createUuid());
entry->setTitle(itemMap.value("name").toString());
entry->setNotes(itemMap.value("notes").toString());
if (itemMap.value("favorite").toBool()) {
entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
}
// Parse login details if present
if (itemMap.contains("login")) {
const auto loginMap = itemMap.value("login").toMap();
entry->setUsername(loginMap.value("username").toString());
entry->setPassword(loginMap.value("password").toString());
if (loginMap.contains("totp")) {
// Bitwarden stores TOTP as otpauth string
entry->setTotp(Totp::parseSettings(loginMap.value("totp").toString()));
}
// Set the entry url(s)
int i = 1;
for (const auto& urlObj : loginMap.value("uris").toList()) {
const auto url = urlObj.toMap().value("uri").toString();
if (entry->url().isEmpty()) {
// First url encountered is set as the primary url
entry->setUrl(url);
} else {
// Subsequent urls
entry->attributes()->set(
QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url);
++i;
}
}
}
// Parse identity details if present
if (itemMap.contains("identity")) {
const auto idMap = itemMap.value("identity").toMap();
// Combine name attributes
auto attrs = QStringList({idMap.value("title").toString(),
idMap.value("firstName").toString(),
idMap.value("middleName").toString(),
idMap.value("lastName").toString()});
attrs.removeAll("");
entry->attributes()->set("identity_name", attrs.join(" "));
// Combine all the address attributes
attrs = QStringList({idMap.value("address1").toString(),
idMap.value("address2").toString(),
idMap.value("address3").toString()});
attrs.removeAll("");
auto address = attrs.join("\n") + "\n" + idMap.value("city").toString() + ", "
+ idMap.value("state").toString() + " " + idMap.value("postalCode").toString() + "\n"
+ idMap.value("country").toString();
entry->attributes()->set("identity_address", address);
// Add the remaining attributes
attrs = QStringList({"company", "email", "phone", "ssn", "passportNumber", "licenseNumber"});
const QStringList sensitive({"ssn", "passportNumber", "licenseNumber"});
for (const auto& attr : attrs) {
const auto value = idMap.value(attr).toString();
if (!value.isEmpty()) {
entry->attributes()->set("identity_" + attr, value, sensitive.contains(attr));
}
}
// Set the username or push it into attributes if already set
const auto username = idMap.value("username").toString();
if (!username.isEmpty()) {
if (entry->username().isEmpty()) {
entry->setUsername(username);
} else {
entry->attributes()->set("identity_username", username);
}
}
}
// Parse card details if present
if (itemMap.contains("card")) {
const auto cardMap = itemMap.value("card").toMap();
const QStringList attrs({"cardholderName", "brand", "number", "expMonth", "expYear", "code"});
const QStringList sensitive({"code"});
for (const auto& attr : attrs) {
auto value = cardMap.value(attr).toString();
if (!value.isEmpty()) {
entry->attributes()->set("card_" + attr, value, sensitive.contains(attr));
}
}
}
// Parse remaining fields
for (const auto& field : itemMap.value("fields").toList()) {
// Derive a prefix for attribute names using the title or uuid if missing
const auto fieldMap = field.toMap();
auto name = fieldMap.value("name").toString();
if (entry->attributes()->hasKey(name)) {
name = QString("%1_%2").arg(name, QUuid::createUuid().toString().mid(1, 5));
}
const auto value = fieldMap.value("value").toString();
const auto type = fieldMap.value("type").toInt();
entry->attributes()->set(name, value, type == 1);
}
// Collapse any accumulated history
entry->removeHistoryItems(entry->historyItems());
return entry.take();
}
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
{
if (!vault.contains("folders") || !vault.contains("items")) {
// Early out if the vault is missing critical items
return;
}
// Create groups from folders and store a temporary map of id -> uuid
QMap<QString, Group*> folderMap;
for (const auto& folder : vault.value("folders").toArray()) {
auto group = new Group();
group->setUuid(QUuid::createUuid());
group->setName(folder.toObject().value("name").toString());
group->setParent(db->rootGroup());
folderMap.insert(folder.toObject().value("id").toString(), group);
}
QString folderId;
const auto items = vault.value("items").toArray();
for (const auto& item : items) {
auto entry = readItem(item.toObject(), folderId);
if (entry) {
entry->setGroup(folderMap.value(folderId, db->rootGroup()), false);
}
}
}
} // namespace
bool BitwardenReader::hasError()
{
return !m_error.isEmpty();
}
QString BitwardenReader::errorString()
{
return m_error;
}
QSharedPointer<Database> BitwardenReader::convert(const QString& path, const QString& password)
{
m_error.clear();
QFileInfo fileinfo(path);
if (!fileinfo.exists()) {
m_error = QObject::tr("File does not exist.").arg(path);
return {};
}
// Bitwarden uses a json file format
QFile file(fileinfo.absoluteFilePath());
if (!file.open(QFile::ReadOnly)) {
m_error = QObject::tr("Cannot open file: %1").arg(file.errorString());
return {};
}
QJsonParseError error;
auto json = QJsonDocument::fromJson(file.readAll(), &error).object();
if (error.error != QJsonParseError::NoError) {
m_error =
QObject::tr("Cannot parse file: %1 at position %2").arg(error.errorString(), QString::number(error.offset));
return {};
}
file.close();
// Check if this is an encrypted json
if (json.contains("encrypted") && json.value("encrypted").toBool()) {
auto buildError = [](const QString& errorString) {
return QObject::tr("Failed to decrypt json file: %1").arg(errorString);
};
QByteArray key(32, '\0');
auto salt = json.value("salt").toString().toUtf8();
auto pwd_fam = Botan::PasswordHashFamily::create_or_throw("PBKDF2(SHA-256)");
auto kdf = Botan::KDF::create_or_throw("HKDF-Expand(SHA-256)");
// Derive the Master Key
auto pwd_hash = pwd_fam->from_params(json.value("kdfIterations").toInt());
pwd_hash->derive_key(reinterpret_cast<uint8_t*>(key.data()),
key.size(),
password.toUtf8().data(),
password.toUtf8().size(),
reinterpret_cast<uint8_t*>(salt.data()),
salt.size());
// Derive the MAC Key
auto stretched_mac = kdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "mac");
auto mac = QByteArray(reinterpret_cast<const char*>(stretched_mac.data()), stretched_mac.size());
// Stretch the Master Key
auto stretched_key = kdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "enc");
key = QByteArray(reinterpret_cast<const char*>(stretched_key.data()), stretched_key.size());
// Validate the encryption key
auto keyList = json.value("encKeyValidation_DO_NOT_EDIT").toString().split(".");
if (keyList.size() < 2) {
m_error = buildError(QObject::tr("Invalid encKeyValidation field"));
return {};
}
auto cipherList = keyList[1].split("|");
if (cipherList.size() < 3) {
m_error = buildError(QObject::tr("Invalid cipher list within encKeyValidation field"));
return {};
}
CryptoHash hash(CryptoHash::Sha256, true);
hash.setKey(mac);
hash.addData(QByteArray::fromBase64(cipherList[0].toUtf8())); // iv
hash.addData(QByteArray::fromBase64(cipherList[1].toUtf8())); // ciphertext
if (hash.result().toBase64() != cipherList[2].toUtf8()) {
// Calculated MAC doesn't equal the Validation
m_error = buildError(QObject::tr("Wrong password"));
return {};
}
// Decrypt data field using AES-256-CBC
keyList = json.value("data").toString().split(".");
if (keyList.size() < 2) {
m_error = buildError(QObject::tr("Invalid encrypted data field"));
return {};
}
cipherList = keyList[1].split("|");
if (cipherList.size() < 2) {
m_error = buildError(QObject::tr("Invalid cipher list within encrypted data field"));
return {};
}
auto iv = QByteArray::fromBase64(cipherList[0].toUtf8());
auto data = QByteArray::fromBase64(cipherList[1].toUtf8());
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_CBC, SymmetricCipher::Decrypt, key, iv)) {
m_error = buildError(QObject::tr("Cannot initialize cipher"));
return {};
}
if (!cipher.finish(data)) {
m_error = buildError(QObject::tr("Cannot decrypt data"));
return {};
}
json = QJsonDocument::fromJson(data, &error).object();
if (error.error != QJsonParseError::NoError) {
m_error = buildError(error.errorString());
return {};
}
}
auto db = QSharedPointer<Database>::create();
db->rootGroup()->setName(QObject::tr("Bitwarden Import"));
writeVaultToDatabase(json, db);
return db;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 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
@ -15,20 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_OPVAULTOPENWIDGET_H
#define KEEPASSXC_OPVAULTOPENWIDGET_H
#ifndef BITWARDEN_READER_H
#define BITWARDEN_READER_H
#include "gui/DatabaseOpenWidget.h"
#include <QSharedPointer>
class OpVaultOpenWidget : public DatabaseOpenWidget
class Database;
/*!
* Imports a Bitwarden vault in JSON format: https://bitwarden.com/help/encrypted-export/
*/
class BitwardenReader
{
Q_OBJECT
public:
explicit OpVaultOpenWidget(QWidget* parent = nullptr);
explicit BitwardenReader() = default;
~BitwardenReader() = default;
protected:
void openDatabase() override;
QSharedPointer<Database> convert(const QString& path, const QString& password = {});
bool hasError();
QString errorString();
private:
QString m_error;
};
#endif // KEEPASSXC_OPVAULTOPENWIDGET_H
#endif // BITWARDEN_READER_H

View file

@ -24,20 +24,13 @@
#include "core/Tools.h"
CsvParser::CsvParser()
: m_ch(0)
, m_comment('#')
, m_currCol(1)
, m_currRow(1)
: m_comment('#')
, m_isBackslashSyntax(false)
, m_isEof(false)
, m_isFileLoaded(false)
, m_isGood(true)
, m_lastPos(-1)
, m_maxCols(0)
, m_qualifier('"')
, m_separator(',')
, m_statusMsg("")
{
reset();
m_csv.setBuffer(&m_array);
m_ts.setDevice(&m_csv);
m_csv.open(QIODevice::ReadOnly);
@ -105,10 +98,10 @@ void CsvParser::reset()
m_isGood = true;
m_lastPos = -1;
m_maxCols = 0;
m_statusMsg = "";
m_statusMsg.clear();
m_ts.seek(0);
m_table.clear();
// the following are users' concern :)
// the following can be overridden by the user
// m_comment = '#';
// m_backslashSyntax = false;
// m_comment = '#';
@ -148,7 +141,7 @@ void CsvParser::parseRecord()
do {
parseField(row);
getChar(m_ch);
} while (isSeparator(m_ch) && !m_isEof);
} while (m_ch == m_separator && !m_isEof);
if (!m_isEof) {
ungetChar();
@ -168,7 +161,7 @@ void CsvParser::parseField(CsvRow& row)
{
QString field;
peek(m_ch);
if (!isTerminator(m_ch)) {
if (m_ch != m_separator && m_ch != '\n' && m_ch != '\r') {
if (isQualifier(m_ch)) {
parseQuoted(field);
} else {
@ -182,7 +175,7 @@ void CsvParser::parseSimple(QString& s)
{
QChar c;
getChar(c);
while ((isText(c)) && (!m_isEof)) {
while (c != '\n' && c != m_separator && !m_isEof) {
s.append(c);
getChar(c);
}
@ -215,7 +208,7 @@ void CsvParser::parseEscaped(QString& s)
void CsvParser::parseEscapedText(QString& s)
{
getChar(m_ch);
while ((!isQualifier(m_ch)) && !m_isEof) {
while (!isQualifier(m_ch) && !m_isEof) {
s.append(m_ch);
getChar(m_ch);
}
@ -223,10 +216,9 @@ void CsvParser::parseEscapedText(QString& s)
bool CsvParser::processEscapeMark(QString& s, QChar c)
{
QChar buf;
peek(buf);
QChar c2;
if (true == m_isBackslashSyntax) {
peek(c2);
if (m_isBackslashSyntax) {
// escape-character syntax, e.g. \"
if (c != '\\') {
return false;
@ -237,25 +229,24 @@ bool CsvParser::processEscapeMark(QString& s, QChar c)
c2 = '\\';
s.append('\\');
return false;
} else {
s.append(c2);
return true;
}
} else {
// double quote syntax, e.g. ""
if (!isQualifier(c)) {
return false;
}
peek(c2);
if (!m_isEof) { // not EOF, can read one char
if (isQualifier(c2)) {
s.append(c2);
getChar(c2);
return true;
}
}
s.append(c2);
return true;
}
// double quote syntax, e.g. ""
if (!isQualifier(c)) {
return false;
}
peek(c2);
if (!m_isEof) { // not EOF, can read one char
if (isQualifier(c2)) {
s.append(c2);
getChar(c2);
return true;
}
}
return false;
}
void CsvParser::fillColumns()
@ -282,7 +273,7 @@ void CsvParser::skipLine()
bool CsvParser::skipEndline()
{
getChar(m_ch);
return (m_ch == '\n');
return m_ch == '\n';
}
void CsvParser::getChar(QChar& c)
@ -312,11 +303,10 @@ void CsvParser::peek(QChar& c)
bool CsvParser::isQualifier(const QChar& c) const
{
if (true == m_isBackslashSyntax && (c != m_qualifier)) {
return (c == '\\');
} else {
return (c == m_qualifier);
if (m_isBackslashSyntax && c != m_qualifier) {
return c == '\\';
}
return c == m_qualifier;
}
bool CsvParser::isComment()
@ -327,7 +317,7 @@ bool CsvParser::isComment()
do {
getChar(c2);
} while ((isSpace(c2) || isTab(c2)) && (!m_isEof));
} while ((c2 == ' ' || c2 == '\t') && !m_isEof);
if (c2 == m_comment) {
result = true;
@ -336,47 +326,16 @@ bool CsvParser::isComment()
return result;
}
bool CsvParser::isText(QChar c) const
{
return !((isCRLF(c)) || (isSeparator(c)));
}
bool CsvParser::isEmptyRow(const CsvRow& row) const
{
CsvRow::const_iterator it = row.constBegin();
for (; it != row.constEnd(); ++it) {
if (((*it) != "\n") && ((*it) != "")) {
for (auto it = row.constBegin(); it != row.constEnd(); ++it) {
if (*it != "\n" && *it != "") {
return false;
}
}
return true;
}
bool CsvParser::isCRLF(const QChar& c) const
{
return (c == '\n');
}
bool CsvParser::isSpace(const QChar& c) const
{
return (c == ' ');
}
bool CsvParser::isTab(const QChar& c) const
{
return (c == '\t');
}
bool CsvParser::isSeparator(const QChar& c) const
{
return (c == m_separator);
}
bool CsvParser::isTerminator(const QChar& c) const
{
return (isSeparator(c) || (c == '\n') || (c == '\r'));
}
void CsvParser::setBackslashSyntax(bool set)
{
m_isBackslashSyntax = set;
@ -407,7 +366,7 @@ int CsvParser::getFileSize() const
return m_csv.size();
}
const CsvTable CsvParser::getCsvTable() const
CsvTable CsvParser::getCsvTable() const
{
return m_table;
}
@ -421,9 +380,8 @@ int CsvParser::getCsvCols() const
{
if (!m_table.isEmpty() && !m_table.at(0).isEmpty()) {
return m_table.at(0).size();
} else {
return 0;
}
return 0;
}
int CsvParser::getCsvRows() const

View file

@ -47,7 +47,7 @@ public:
int getCsvRows() const;
int getCsvCols() const;
QString getStatus() const;
const CsvTable getCsvTable() const;
CsvTable getCsvTable() const;
protected:
CsvTable m_table;
@ -74,15 +74,9 @@ private:
void ungetChar();
void peek(QChar& c);
void fillColumns();
bool isTerminator(const QChar& c) const;
bool isSeparator(const QChar& c) const;
bool isQualifier(const QChar& c) const;
bool processEscapeMark(QString& s, QChar c);
bool isText(QChar c) const;
bool isComment();
bool isCRLF(const QChar& c) const;
bool isSpace(const QChar& c) const;
bool isTab(const QChar& c) const;
bool isEmptyRow(const CsvRow& row) const;
bool parseFile();
void parseRecord();

View file

@ -18,6 +18,7 @@
#include "KeePass1Reader.h"
#include <QFile>
#include <QFileInfo>
#include <QTextCodec>
#include "core/Endian.h"
@ -275,6 +276,10 @@ KeePass1Reader::readDatabase(const QString& filename, const QString& password, c
return {};
}
if (db) {
db->metadata()->setName(QFileInfo(filename).completeBaseName());
}
return db;
}

288
src/format/OPUXReader.cpp Normal file
View file

@ -0,0 +1,288 @@
/*
* Copyright (C) 2023 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 "OPUXReader.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Totp.h"
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScopedPointer>
#include <QUrl>
#include <minizip/unzip.h>
namespace
{
QByteArray extractFile(unzFile uf, QString filename)
{
if (unzLocateFile(uf, filename.toLatin1(), 2) != UNZ_OK) {
qWarning("Failed to extract 1PUX document: %s", qPrintable(filename));
return {};
}
// Read export.data into memory
int bytes, bytesRead = 0;
QByteArray data;
unzOpenCurrentFile(uf);
do {
data.resize(data.size() + 8192);
bytes = unzReadCurrentFile(uf, data.data() + bytesRead, 8192);
if (bytes > 0) {
bytesRead += bytes;
}
} while (bytes > 0);
unzCloseCurrentFile(uf);
data.truncate(bytesRead);
return data;
}
Entry* readItem(const QJsonObject& item, unzFile uf = nullptr)
{
const auto itemMap = item.toVariantMap();
const auto overviewMap = itemMap.value("overview").toMap();
const auto detailsMap = itemMap.value("details").toMap();
// Create entry and assign basic values
QScopedPointer<Entry> entry(new Entry());
entry->setUuid(QUuid::createUuid());
entry->setTitle(overviewMap.value("title").toString());
entry->setUrl(overviewMap.value("url").toString());
if (overviewMap.contains("urls")) {
int i = 1;
for (const auto& urlRaw : overviewMap.value("urls").toList()) {
const auto urlMap = urlRaw.toMap();
const auto url = urlMap.value("url").toString();
if (entry->url() != url) {
entry->attributes()->set(
QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url);
++i;
}
}
}
if (overviewMap.contains("tags")) {
entry->setTags(overviewMap.value("tags").toStringList().join(","));
}
if (itemMap.value("favIndex").toString() == "1") {
entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
}
if (itemMap.value("state").toString() == "archived") {
entry->addTag(QObject::tr("Archived", "Tag for archived entries"));
}
// Parse the details map by setting the username, password, and notes first
const auto loginFields = detailsMap.value("loginFields").toList();
for (const auto& field : loginFields) {
const auto fieldMap = field.toMap();
const auto designation = fieldMap.value("designation").toString();
if (designation.compare("username", Qt::CaseInsensitive) == 0) {
entry->setUsername(fieldMap.value("value").toString());
} else if (designation.compare("password", Qt::CaseInsensitive) == 0) {
entry->setPassword(fieldMap.value("value").toString());
}
}
entry->setNotes(detailsMap.value("notesPlain").toString());
// Dive into the item sections to pull out advanced attributes
const auto sections = detailsMap.value("sections").toList();
for (const auto& section : sections) {
// Derive a prefix for attribute names using the title or uuid if missing
const auto sectionMap = section.toMap();
auto prefix = sectionMap.value("title").toString();
if (prefix.isEmpty()) {
prefix = QUuid::createUuid().toString().mid(1, 5);
}
for (const auto& field : sectionMap.value("fields").toList()) {
// Form the name of the attribute using the prefix and title or id
const auto fieldMap = field.toMap();
auto name = fieldMap.value("title").toString();
if (name.isEmpty()) {
name = fieldMap.value("id").toString();
}
name = QString("%1_%2").arg(prefix, name);
const auto valueMap = fieldMap.value("value").toMap();
const auto key = valueMap.firstKey();
if (key == "totp") {
// Build otpauth url
QUrl otpurl(QString("otpauth://totp/%1:%2?secret=%3")
.arg(entry->title(), entry->username(), valueMap.value(key).toString()));
if (entry->hasTotp()) {
// Store multiple TOTP definitions as additional otp attributes
int i = 0;
name = "otp";
const auto attributes = entry->attributes()->keys();
while (attributes.contains(name)) {
name = QString("otp_%1").arg(++i);
}
entry->attributes()->set(name, otpurl.toEncoded(), true);
} else {
// First otp value encountered gets formal storage
entry->setTotp(Totp::parseSettings(otpurl.toEncoded()));
}
} else if (key == "file") {
// Add a file to the entry attachments
const auto fileMap = valueMap.value(key).toMap();
const auto fileName = fileMap.value("fileName").toString();
const auto docId = fileMap.value("documentId").toString();
const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName));
if (!data.isNull()) {
entry->attachments()->set(fileName, data);
}
} else {
auto value = valueMap.value(key).toString();
if (key == "date") {
// Convert date fields from Unix time
value = QDateTime::fromSecsSinceEpoch(valueMap.value(key).toULongLong(), Qt::UTC).toString();
} else if (key == "email") {
// Email address is buried in a sub-value
value = valueMap.value(key).toMap().value("email_address").toString();
} else if (key == "address") {
// Combine all the address attributes into a fully formed structure
const auto address = valueMap.value(key).toMap();
value = address.value("street").toString() + "\n" + address.value("city").toString() + ", "
+ address.value("state").toString() + " " + address.value("zip").toString() + "\n"
+ address.value("country").toString();
}
if (!value.isEmpty()) {
entry->attributes()->set(name, value, key == "concealed");
}
}
}
}
// Add a document attachment if defined
if (detailsMap.contains("documentAttributes")) {
const auto document = detailsMap.value("documentAttributes").toMap();
const auto fileName = document.value("fileName").toString();
const auto docId = document.value("documentId").toString();
const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName));
if (!data.isNull()) {
entry->attachments()->set(fileName, data);
}
}
// Collapse any accumulated history
entry->removeHistoryItems(entry->historyItems());
// Adjust the created and modified times
auto timeInfo = entry->timeInfo();
const auto createdTime = QDateTime::fromSecsSinceEpoch(itemMap.value("createdAt").toULongLong(), Qt::UTC);
const auto modifiedTime = QDateTime::fromSecsSinceEpoch(itemMap.value("updatedAt").toULongLong(), Qt::UTC);
timeInfo.setCreationTime(createdTime);
timeInfo.setLastModificationTime(modifiedTime);
timeInfo.setLastAccessTime(modifiedTime);
entry->setTimeInfo(timeInfo);
return entry.take();
}
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db, unzFile uf = nullptr)
{
if (!vault.contains("attrs") || !vault.contains("items")) {
// Early out if the vault is missing critical items
return;
}
const auto attr = vault.value("attrs").toObject().toVariantMap();
// Create group and assign basic values
auto group = new Group();
group->setUuid(QUuid::createUuid());
group->setName(attr.value("name").toString());
group->setParent(db->rootGroup());
const auto items = vault.value("items").toArray();
for (const auto& item : items) {
auto entry = readItem(item.toObject(), uf);
if (entry) {
entry->setGroup(group, false);
}
}
// Add the group icon if present
const auto icon = attr.value("avatar").toString();
if (!icon.isEmpty()) {
auto data = extractFile(uf, QString("files/%1").arg(icon));
if (!data.isNull()) {
const auto uuid = QUuid::createUuid();
db->metadata()->addCustomIcon(uuid, data);
group->setIcon(uuid);
}
}
}
} // namespace
bool OPUXReader::hasError()
{
return !m_error.isEmpty();
}
QString OPUXReader::errorString()
{
return m_error;
}
QSharedPointer<Database> OPUXReader::convert(const QString& path)
{
m_error.clear();
QFileInfo fileinfo(path);
if (!fileinfo.exists()) {
m_error = QObject::tr("File does not exist.").arg(path);
return {};
}
// 1PUX is a zip file format, open it and process the contents in memory
auto uf = unzOpen64(fileinfo.absoluteFilePath().toLatin1().constData());
if (!uf) {
m_error = QObject::tr("Invalid 1PUX file format: Not a valid ZIP file.");
return {};
}
// Find the export.data file, if not found this isn't a 1PUX file
auto data = extractFile(uf, "export.data");
if (data.isNull()) {
m_error = QObject::tr("Invalid 1PUX file format: Missing export.data");
unzClose(uf);
return {};
}
auto db = QSharedPointer<Database>::create();
db->rootGroup()->setName(QObject::tr("1Password Import"));
const auto json = QJsonDocument::fromJson(data);
const auto account = json.object().value("accounts").toArray().first().toObject();
const auto vaults = account.value("vaults").toArray();
for (const auto& vault : vaults) {
writeVaultToDatabase(vault.toObject(), db, uf);
}
unzClose(uf);
return db;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2023 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
@ -15,20 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_KEEPASS1OPENWIDGET_H
#define KEEPASSX_KEEPASS1OPENWIDGET_H
#ifndef OPUX_READER_H
#define OPUX_READER_H
#include "gui/DatabaseOpenWidget.h"
#include <QSharedPointer>
class KeePass1OpenWidget : public DatabaseOpenWidget
class Database;
/*!
* Imports a 1Password vault in 1PUX format: https://support.1password.com/1pux-format/
*/
class OPUXReader
{
Q_OBJECT
public:
explicit KeePass1OpenWidget(QWidget* parent = nullptr);
explicit OPUXReader() = default;
~OPUXReader() = default;
protected:
void openDatabase() override;
QSharedPointer<Database> convert(const QString& path);
bool hasError();
QString errorString();
private:
QString m_error;
};
#endif // KEEPASSX_KEEPASS1OPENWIDGET_H
#endif // OPUX_READER_H

View file

@ -31,68 +31,49 @@
OpVaultReader::OpVaultReader(QObject* parent)
: QObject(parent)
, m_error(false)
{
}
OpVaultReader::~OpVaultReader() = default;
Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password)
QSharedPointer<Database> OpVaultReader::convert(QDir& opdataDir, const QString& password)
{
if (!opdataDir.exists()) {
m_error = true;
m_errorStr = tr("Directory .opvault must exist");
return nullptr;
m_error = tr("Directory .opvault must exist");
return {};
}
if (!opdataDir.isReadable()) {
m_error = true;
m_errorStr = tr("Directory .opvault must be readable");
return nullptr;
m_error = tr("Directory .opvault must be readable");
return {};
}
// https://support.1password.com/opvault-design/#directory-layout
QDir defaultDir = QDir(opdataDir);
if (!defaultDir.cd("default")) {
m_error = true;
m_errorStr = tr("Directory .opvault/default must exist");
return nullptr;
m_error = tr("Directory .opvault/default must exist");
return {};
}
if (!defaultDir.isReadable()) {
m_error = true;
m_errorStr = tr("Directory .opvault/default must be readable");
return nullptr;
m_error = tr("Directory .opvault/default must be readable");
return {};
}
auto vaultName = opdataDir.dirName();
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(password));
QScopedPointer<Database> db(new Database());
db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D));
db->setCipher(KeePass2::CIPHER_AES256);
db->setKey(key, true, false);
db->metadata()->setName(vaultName);
auto db = QSharedPointer<Database>::create();
auto rootGroup = db->rootGroup();
rootGroup->setTimeInfo({});
rootGroup->setUpdateTimeinfo(false);
rootGroup->setName(vaultName.remove(".opvault"));
rootGroup->setUuid(QUuid::createUuid());
populateCategoryGroups(rootGroup);
QFile profileJsFile(defaultDir.absoluteFilePath("profile.js"));
QJsonObject profileJson = readAndAssertJsonFile(profileJsFile, "var profile=", ";");
if (profileJson.isEmpty()) {
return nullptr;
return {};
}
if (!processProfileJson(profileJson, password, rootGroup)) {
zeroKeys();
return nullptr;
}
if (profileJson.contains("uuid") and profileJson["uuid"].isString()) {
rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString()));
return {};
}
QFile foldersJsFile(defaultDir.filePath("folders.js"));
@ -100,7 +81,7 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password)
QJsonObject foldersJs = readAndAssertJsonFile(foldersJsFile, "loadFolders(", ");");
if (!processFolderJson(foldersJs, rootGroup)) {
zeroKeys();
return nullptr;
return {};
}
}
@ -150,17 +131,17 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password)
}
zeroKeys();
return db.take();
return db;
}
bool OpVaultReader::hasError()
{
return m_error;
return !m_error.isEmpty();
}
QString OpVaultReader::errorString()
{
return m_errorStr;
return m_error;
}
bool OpVaultReader::processProfileJson(QJsonObject& profileJson, const QString& password, Group* rootGroup)
@ -182,38 +163,29 @@ bool OpVaultReader::processProfileJson(QJsonObject& profileJson, const QString&
rootGroupTime.setLastModificationTime(QDateTime::fromTime_t(updatedAt, Qt::UTC));
rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString()));
const auto derivedKeys = deriveKeysFromPassPhrase(salt, password, iterations);
if (derivedKeys->error) {
m_error = true;
m_errorStr = derivedKeys->errorStr;
delete derivedKeys;
QScopedPointer derivedKeys(deriveKeysFromPassPhrase(salt, password, iterations));
if (!derivedKeys->error.isEmpty()) {
m_error = derivedKeys->error;
return false;
}
QByteArray encKey = derivedKeys->encrypt;
QByteArray hmacKey = derivedKeys->hmac;
delete derivedKeys;
auto masterKeys = decodeB64CompositeKeys(masterKeyB64, encKey, hmacKey);
if (masterKeys->error) {
m_error = true;
m_errorStr = masterKeys->errorStr;
delete masterKeys;
QScopedPointer masterKeys(decodeB64CompositeKeys(masterKeyB64, encKey, hmacKey));
if (!masterKeys->error.isEmpty()) {
m_error = masterKeys->error;
return false;
}
m_masterKey = masterKeys->encrypt;
m_masterHmacKey = masterKeys->hmac;
delete masterKeys;
auto overviewKeys = decodeB64CompositeKeys(overviewKeyB64, encKey, hmacKey);
if (overviewKeys->error) {
m_error = true;
m_errorStr = overviewKeys->errorStr;
delete overviewKeys;
QScopedPointer overviewKeys(decodeB64CompositeKeys(overviewKeyB64, encKey, hmacKey));
if (!overviewKeys->error.isEmpty()) {
m_error = overviewKeys->error;
return false;
}
m_overviewKey = overviewKeys->encrypt;
m_overviewHmacKey = overviewKeys->hmac;
delete overviewKeys;
return true;
}
@ -338,15 +310,13 @@ QJsonObject OpVaultReader::readAndAssertJsonFile(QFile& file, const QString& str
OpVaultReader::DerivedKeyHMAC*
OpVaultReader::decodeB64CompositeKeys(const QString& b64, const QByteArray& encKey, const QByteArray& hmacKey)
{
auto result = new DerivedKeyHMAC();
OpData01 keyKey01;
if (!keyKey01.decodeBase64(b64, encKey, hmacKey)) {
result->error = true;
result->errorStr = tr("Unable to decode masterKey: %1").arg(keyKey01.errorString());
auto result = new DerivedKeyHMAC();
result->error = tr("Unable to decode masterKey: %1").arg(keyKey01.errorString());
return result;
}
delete result;
const QByteArray keyKey = keyKey01.getClearText();
@ -364,7 +334,6 @@ OpVaultReader::decodeB64CompositeKeys(const QString& b64, const QByteArray& encK
OpVaultReader::DerivedKeyHMAC* OpVaultReader::decodeCompositeKeys(const QByteArray& keyKey)
{
auto result = new DerivedKeyHMAC;
result->error = false;
auto digest = CryptoHash::hash(keyKey, CryptoHash::Sha512);
result->encrypt = digest.left(32);
@ -383,7 +352,6 @@ OpVaultReader::DerivedKeyHMAC*
OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& password, unsigned long iterations)
{
auto result = new DerivedKeyHMAC;
result->error = false;
QByteArray out(64, '\0');
try {
@ -395,8 +363,7 @@ OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& passwor
reinterpret_cast<const uint8_t*>(salt.constData()),
salt.size());
} catch (std::exception& e) {
result->error = true;
result->errorStr = tr("Unable to derive master key: %1").arg(e.what());
result->error = tr("Unable to derive master key: %1").arg(e.what());
return result;
}

View file

@ -39,7 +39,7 @@ public:
explicit OpVaultReader(QObject* parent = nullptr);
~OpVaultReader() override;
Database* readDatabase(QDir& opdataDir, const QString& password);
QSharedPointer<Database> convert(QDir& opdataDir, const QString& password);
bool hasError();
QString errorString();
@ -49,8 +49,7 @@ private:
{
QByteArray encrypt;
QByteArray hmac;
bool error;
QString errorStr;
QString error;
};
QJsonObject readAndAssertJsonFile(QFile& file, const QString& stripLeading, const QString& stripTrailing);
@ -106,15 +105,14 @@ private:
/*! Used to blank the memory after the keys have been used. */
void zeroKeys();
bool m_error;
QString m_errorStr;
QString m_error;
QByteArray m_masterKey;
QByteArray m_masterHmacKey;
/*! Used to decrypt overview text, such as folder names. */
QByteArray m_overviewKey;
QByteArray m_overviewHmacKey;
friend class TestOpVaultReader;
friend class TestImports;
};
#endif /* OPVAULT_READER_H_ */

View file

@ -229,6 +229,10 @@ void OpVaultReader::fillAttachment(Entry* entry,
qWarning() << QString("Unexpected type of attachment \"filename\": %1").arg(attFilename.type());
}
}
if (entry->attachments()->hasKey(attachKey)) {
// Prepend a random string to the attachment name to avoid collisions
attachKey.prepend(QString("%1_").arg(QUuid::createUuid().toString().mid(1, 5)));
}
entry->attachments()->set(attachKey, attachPayload);
}

View file

@ -92,7 +92,7 @@ void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionNam
while (attributes.contains(name)) {
name = QString("otp_%1").arg(++i);
}
entry->attributes()->set(name, attrValue);
entry->attributes()->set(name, attrValue, true);
} else if (attrValue.startsWith("otpauth://")) {
QUrlQuery query(attrValue);
// at least as of 1Password 7, they don't append the digits= and period= which totp.cpp requires
@ -128,10 +128,14 @@ void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionNam
} else if (kind == "address") {
// Expand address into multiple attributes
auto addrFields = field.value("v").toObject().toVariantMap();
for (auto part : addrFields.keys()) {
for (auto& part : addrFields.keys()) {
entry->attributes()->set(attrName + QString("_%1").arg(part), addrFields.value(part).toString());
}
} else {
if (entry->attributes()->hasKey(attrName)) {
// Append a random string to the attribute name to avoid collisions
attrName += QString("_%1").arg(QUuid::createUuid().toString().mid(1, 5));
}
entry->attributes()->set(attrName, attrValue, (kind == "password" || kind == "concealed"));
}
}

View file

@ -18,6 +18,8 @@
#ifndef KEEPASSX_CLONEDIALOG_H
#define KEEPASSX_CLONEDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View file

@ -21,6 +21,7 @@
#include <QTabBar>
#include "autotype/AutoType.h"
#include "core/Merger.h"
#include "core/Tools.h"
#include "format/CsvExporter.h"
#include "gui/Clipboard.h"
@ -28,13 +29,13 @@
#include "gui/DatabaseWidget.h"
#include "gui/DatabaseWidgetStateSync.h"
#include "gui/FileDialog.h"
#include "gui/HtmlExporter.h"
#include "gui/MessageBox.h"
#include "gui/export/ExportDialog.h"
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
#include "gui/wizard/NewDatabaseWizard.h"
#include "wizard/ImportWizard.h"
DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
@ -250,24 +251,52 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou
connect(dbWidget, SIGNAL(databaseLocked()), SLOT(emitDatabaseLockChanged()));
}
void DatabaseTabWidget::importCsv()
DatabaseWidget* DatabaseTabWidget::importFile()
{
auto filter = QString("%1 (*.csv);;%2 (*)").arg(tr("CSV file"), tr("All files"));
auto fileName = fileDialog()->getOpenFileName(this, tr("Select CSV file"), FileDialog::getLastDir("csv"), filter);
if (fileName.isEmpty()) {
return;
// Show the import wizard
QScopedPointer wizard(new ImportWizard(this));
if (!wizard->exec()) {
return nullptr;
}
FileDialog::saveLastDir("csv", fileName, true);
auto db = execNewDatabaseWizard();
auto db = wizard->database();
if (!db) {
return;
// Import wizard was cancelled
return nullptr;
}
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToCsvImport(fileName);
auto importInto = wizard->importInto();
if (importInto.first.isNull()) {
// Start the new database wizard with the imported database
auto newDb = execNewDatabaseWizard();
if (newDb) {
// Merge the imported db into the new one
Merger merger(db.data(), newDb.data());
merger.merge();
// Show the new database
auto dbWidget = new DatabaseWidget(newDb, this);
addDatabaseTab(dbWidget);
newDb->markAsModified();
return dbWidget;
}
} else {
for (int i = 0, c = count(); i < c; ++i) {
// Find the database and group to import into based on import wizard choice
auto dbWidget = databaseWidgetFromIndex(i);
if (!dbWidget->isLocked() && dbWidget->database()->uuid() == importInto.first) {
auto group = dbWidget->database()->rootGroup()->findGroupByUuid(importInto.second);
if (group) {
// Extract the root group from the import database
auto importGroup = db->setRootGroup(new Group());
importGroup->setParent(group);
setCurrentIndex(i);
return dbWidget;
}
}
}
}
return nullptr;
}
void DatabaseTabWidget::mergeDatabase()
@ -289,44 +318,6 @@ void DatabaseTabWidget::mergeDatabase(const QString& filePath)
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::Merge, filePath);
}
void DatabaseTabWidget::importKeePass1Database()
{
auto filter = QString("%1 (*.kdb);;%2 (*)").arg(tr("KeePass 1 database"), tr("All files"));
auto fileName =
fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), FileDialog::getLastDir("kp1"), filter);
if (fileName.isEmpty()) {
return;
}
FileDialog::saveLastDir("kp1", fileName, true);
auto db = QSharedPointer<Database>::create();
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToImportKeepass1(fileName);
}
void DatabaseTabWidget::importOpVaultDatabase()
{
auto defaultDir = FileDialog::getLastDir("opvault");
#ifdef Q_OS_MACOS
QString fileName = fileDialog()->getOpenFileName(this, tr("Open OPVault"), defaultDir, "OPVault (*.opvault)");
#else
QString fileName = fileDialog()->getExistingDirectory(this, tr("Open OPVault"), defaultDir);
#endif
if (fileName.isEmpty()) {
return;
}
FileDialog::saveLastDir("opvault", fileName);
auto db = QSharedPointer<Database>::create();
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToImportOpVault(fileName);
}
/**
* Attempt to close the current database and remove its tab afterwards.
*
@ -611,43 +602,18 @@ bool DatabaseTabWidget::hasLockableDatabases() const
*/
QString DatabaseTabWidget::tabName(int index)
{
if (index == -1 || index > count()) {
return "";
auto dbWidget = databaseWidgetFromIndex(index);
if (!dbWidget) {
return {};
}
auto* dbWidget = databaseWidgetFromIndex(index);
auto db = dbWidget->database();
Q_ASSERT(db);
if (!db) {
return "";
}
QString tabName;
if (!db->filePath().isEmpty()) {
QFileInfo fileInfo(db->filePath());
if (db->metadata()->name().isEmpty()) {
tabName = fileInfo.fileName();
} else {
tabName = db->metadata()->name();
}
setTabToolTip(index, fileInfo.absoluteFilePath());
} else {
if (db->metadata()->name().isEmpty()) {
tabName = tr("New Database");
} else {
tabName = tr("%1 [New Database]", "Database tab name modifier").arg(db->metadata()->name());
}
}
auto tabName = dbWidget->displayName();
if (dbWidget->isLocked()) {
tabName = tr("%1 [Locked]", "Database tab name modifier").arg(tabName);
}
if (db->isModified()) {
if (dbWidget->database()->isModified()) {
tabName.append("*");
}
@ -670,6 +636,7 @@ void DatabaseTabWidget::updateTabName(int index)
}
index = indexOf(dbWidget);
setTabText(index, tabName(index));
setTabToolTip(index, dbWidget->displayFilePath());
emit tabNameChanged();
}

View file

@ -64,9 +64,7 @@ public slots:
DatabaseWidget* newDatabase();
void openDatabase();
void mergeDatabase();
void importCsv();
void importKeePass1Database();
void importOpVaultDatabase();
DatabaseWidget* importFile();
bool saveDatabase(int index = -1);
bool saveDatabaseAs(int index = -1);
bool saveDatabaseBackup(int index = -1);

View file

@ -30,20 +30,20 @@
#include <QSplitter>
#include <QTextDocumentFragment>
#include <QTextEdit>
#include <core/Tools.h>
#include "autotype/AutoType.h"
#include "core/EntrySearcher.h"
#include "core/Merger.h"
#include "core/Tools.h"
#include "gui/Clipboard.h"
#include "gui/CloneDialog.h"
#include "gui/DatabaseOpenDialog.h"
#include "gui/DatabaseOpenWidget.h"
#include "gui/EntryPreviewWidget.h"
#include "gui/FileDialog.h"
#include "gui/GuiTools.h"
#include "gui/KeePass1OpenWidget.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/OpVaultOpenWidget.h"
#include "gui/TotpDialog.h"
#include "gui/TotpExportSettingsDialog.h"
#include "gui/TotpSetupDialog.h"
@ -79,15 +79,12 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
, m_previewSplitter(new QSplitter(m_mainWidget))
, m_searchingLabel(new QLabel(this))
, m_shareLabel(new ElidedLabel(this))
, m_csvImportWizard(new CsvImportWizard(this))
, m_editEntryWidget(new EditEntryWidget(this))
, m_editGroupWidget(new EditGroupWidget(this))
, m_historyEditEntryWidget(new EditEntryWidget(this))
, m_reportsDialog(new ReportsDialog(this))
, m_databaseSettingDialog(new DatabaseSettingsDialog(this))
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
, m_groupView(new GroupView(m_db.data(), this))
, m_tagView(new TagView(this))
, m_saveAttempts(0)
@ -179,12 +176,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_editEntryWidget->setObjectName("editEntryWidget");
m_editGroupWidget->setObjectName("editGroupWidget");
m_csvImportWizard->setObjectName("csvImportWizard");
m_reportsDialog->setObjectName("reportsDialog");
m_databaseSettingDialog->setObjectName("databaseSettingsDialog");
m_databaseOpenWidget->setObjectName("databaseOpenWidget");
m_keepass1OpenWidget->setObjectName("keepass1OpenWidget");
m_opVaultOpenWidget->setObjectName("opVaultOpenWidget");
addChildWidget(m_mainWidget);
addChildWidget(m_editEntryWidget);
@ -193,9 +187,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
addChildWidget(m_databaseSettingDialog);
addChildWidget(m_historyEditEntryWidget);
addChildWidget(m_databaseOpenWidget);
addChildWidget(m_csvImportWizard);
addChildWidget(m_keepass1OpenWidget);
addChildWidget(m_opVaultOpenWidget);
// clang-format off
connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
@ -216,9 +207,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
connect(this, SIGNAL(requestGlobalAutoType(const QString&)), parent, SLOT(performGlobalAutoType(const QString&)));
// clang-format on
@ -273,10 +261,8 @@ DatabaseWidget::Mode DatabaseWidget::currentMode() const
return Mode::None;
} else if (currentWidget() == m_mainWidget) {
return Mode::ViewMode;
} else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) {
} else if (currentWidget() == m_databaseOpenWidget) {
return Mode::LockedMode;
} else if (currentWidget() == m_csvImportWizard) {
return Mode::ImportMode;
} else {
return Mode::EditMode;
}
@ -327,6 +313,45 @@ bool DatabaseWidget::isEditWidgetModified() const
return false;
}
QString DatabaseWidget::displayName() const
{
if (!m_db) {
return {};
}
auto displayName = m_db->metadata()->name();
if (!m_db->filePath().isEmpty()) {
if (displayName.isEmpty()) {
displayName = displayFileName();
}
} else {
if (displayName.isEmpty()) {
displayName = tr("New Database");
} else {
displayName = tr("%1 [New Database]", "Database tab name modifier").arg(displayName);
}
}
return displayName;
}
QString DatabaseWidget::displayFileName() const
{
if (m_db) {
QFileInfo fileinfo(m_db->filePath());
return fileinfo.fileName();
}
return {};
}
QString DatabaseWidget::displayFilePath() const
{
if (m_db) {
return m_db->canonicalFilePath();
}
return {};
}
QHash<Config::ConfigKey, QList<int>> DatabaseWidget::splitterSizes() const
{
return {{Config::GUI_SplitterState, m_mainSplitter->sizes()},
@ -1341,33 +1366,6 @@ void DatabaseWidget::switchToOpenDatabase(const QString& filePath, const QString
m_databaseOpenWidget->enterKey(password, keyFile);
}
void DatabaseWidget::switchToCsvImport(const QString& filePath)
{
setCurrentWidget(m_csvImportWizard);
m_csvImportWizard->load(filePath, m_db.data());
}
void DatabaseWidget::csvImportFinished(bool accepted)
{
if (!accepted) {
emit closeRequest();
} else {
switchToMainView();
}
}
void DatabaseWidget::switchToImportKeepass1(const QString& filePath)
{
m_keepass1OpenWidget->load(filePath);
setCurrentWidget(m_keepass1OpenWidget);
}
void DatabaseWidget::switchToImportOpVault(const QString& fileName)
{
m_opVaultOpenWidget->load(fileName);
setCurrentWidget(m_opVaultOpenWidget);
}
void DatabaseWidget::switchToEntryEdit()
{
auto entry = m_entryView->currentEntry();

View file

@ -19,35 +19,29 @@
#ifndef KEEPASSX_DATABASEWIDGET_H
#define KEEPASSX_DATABASEWIDGET_H
#include <QFileSystemWatcher>
#include <QListView>
#include <QBuffer>
#include <QStackedWidget>
#include "DatabaseOpenDialog.h"
#include "config-keepassx.h"
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "gui/MessageWidget.h"
#include "gui/csvImport/CsvImportWizard.h"
#include "gui/entry/EntryModel.h"
class DatabaseOpenDialog;
class DatabaseOpenWidget;
class KeePass1OpenWidget;
class OpVaultOpenWidget;
class DatabaseSettingsDialog;
class ReportsDialog;
class Database;
class FileWatcher;
class EditEntryWidget;
class EditGroupWidget;
class Entry;
class EntryView;
class EntrySearcher;
class Group;
class GroupView;
class QFile;
class QMenu;
class QSplitter;
class QLabel;
class MessageWidget;
class EntryPreviewWidget;
class TagView;
class ElidedLabel;
@ -67,7 +61,6 @@ public:
enum class Mode
{
None,
ImportMode,
ViewMode,
EditMode,
LockedMode
@ -104,6 +97,10 @@ public:
int numberOfSelectedEntries() const;
int currentEntryIndex() const;
QString displayName() const;
QString displayFileName() const;
QString displayFilePath() const;
QStringList customEntryAttributes() const;
bool isEditWidgetModified() const;
void clearAllWidgets();
@ -219,11 +216,7 @@ public slots:
void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath);
void switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile);
void switchToCsvImport(const QString& filePath);
void performUnlockDatabase(const QString& password, const QString& keyfile = {});
void csvImportFinished(bool accepted);
void switchToImportKeepass1(const QString& filePath);
void switchToImportOpVault(const QString& fileName);
void emptyRecycleBin();
// Search related slots
@ -288,15 +281,12 @@ private:
QPointer<QSplitter> m_previewSplitter;
QPointer<QLabel> m_searchingLabel;
QPointer<ElidedLabel> m_shareLabel;
QPointer<CsvImportWizard> m_csvImportWizard;
QPointer<EditEntryWidget> m_editEntryWidget;
QPointer<EditGroupWidget> m_editGroupWidget;
QPointer<EditEntryWidget> m_historyEditEntryWidget;
QPointer<ReportsDialog> m_reportsDialog;
QPointer<DatabaseSettingsDialog> m_databaseSettingDialog;
QPointer<DatabaseOpenWidget> m_databaseOpenWidget;
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
QPointer<GroupView> m_groupView;
QPointer<TagView> m_tagView;
QPointer<EntryView> m_entryView;

View file

@ -31,7 +31,7 @@
#endif
#include <QScrollBar>
#include <QTabWidget>
namespace
{
constexpr int GeneralTabIndex = 0;

View file

@ -26,6 +26,7 @@ namespace Ui
class EntryPreviewWidget;
}
class QTabWidget;
class QTextEdit;
class EntryPreviewWidget : public QWidget

View file

@ -18,6 +18,7 @@
#include "Icons.h"
#include <QBuffer>
#include <QIconEngine>
#include <QImageReader>
#include <QPaintDevice>
@ -25,6 +26,7 @@
#include "config-keepassx.h"
#include "core/Config.h"
#include "core/Database.h"
#include "gui/DatabaseIcons.h"
#include "gui/MainWindow.h"
#include "gui/osutils/OSUtils.h"

View file

@ -1,63 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* 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 "KeePass1OpenWidget.h"
#include "ui_DatabaseOpenWidget.h"
#include <QFileInfo>
#include "core/Database.h"
#include "core/Metadata.h"
#include "format/KeePass1Reader.h"
KeePass1OpenWidget::KeePass1OpenWidget(QWidget* parent)
: DatabaseOpenWidget(parent)
{
m_ui->labelHeadline->setText(tr("Import KeePass1 Database"));
}
void KeePass1OpenWidget::openDatabase()
{
KeePass1Reader reader;
QString password;
QString keyFileName = m_ui->keyFileLineEdit->text();
if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) {
password = m_ui->editPassword->text();
}
QFile file(m_filename);
if (!file.open(QIODevice::ReadOnly)) {
m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(file.errorString()),
MessageWidget::Error);
return;
}
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_db = reader.readDatabase(&file, password, keyFileName);
QApplication::restoreOverrideCursor();
if (m_db) {
m_db->metadata()->setName(QFileInfo(m_filename).completeBaseName());
emit dialogFinished(true);
clearForms();
} else {
m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(reader.errorString()),
MessageWidget::Error);
}
}

View file

@ -354,7 +354,7 @@ MainWindow::MainWindow()
m_ui->actionLockAllDatabases->setIcon(icons()->icon("database-lock-all"));
m_ui->actionQuit->setIcon(icons()->icon("application-exit"));
m_ui->actionDatabaseMerge->setIcon(icons()->icon("database-merge"));
m_ui->menuImport->setIcon(icons()->icon("document-import"));
m_ui->actionImport->setIcon(icons()->icon("document-import"));
m_ui->menuExport->setIcon(icons()->icon("document-export"));
m_ui->actionEntryNew->setIcon(icons()->icon("entry-new"));
@ -464,9 +464,7 @@ MainWindow::MainWindow()
connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey()));
connect(m_ui->actionEntryImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskeyToEntry()));
#endif
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv()));
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database()));
connect(m_ui->actionImportOpVault, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importOpVaultDatabase()));
connect(m_ui->actionImport, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importFile()));
connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv()));
connect(m_ui->actionExportHtml, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToHtml()));
connect(m_ui->actionExportXML, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToXML()));
@ -532,9 +530,7 @@ MainWindow::MainWindow()
connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase()));
connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase()));
connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString)));
connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database()));
connect(m_ui->welcomeWidget, SIGNAL(importOpVaultDatabase()), SLOT(switchToOpVaultDatabase()));
connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport()));
connect(m_ui->welcomeWidget, SIGNAL(importFile()), m_ui->tabWidget, SLOT(importFile()));
connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog()));
connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl()));
@ -863,7 +859,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionLockDatabase->setEnabled(m_ui->tabWidget->hasLockableDatabases());
m_ui->actionLockDatabaseToolbar->setEnabled(m_ui->tabWidget->hasLockableDatabases());
m_ui->actionLockAllDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases());
@ -977,7 +973,6 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
break;
}
case DatabaseWidget::Mode::EditMode:
case DatabaseWidget::Mode::ImportMode:
case DatabaseWidget::Mode::LockedMode: {
// Enable select actions when editing an entry
bool editEntryActive = dbWidget->isEntryEditActive();
@ -1291,24 +1286,6 @@ void MainWindow::switchToDatabaseFile(const QString& file)
switchToDatabases();
}
void MainWindow::switchToKeePass1Database()
{
m_ui->tabWidget->importKeePass1Database();
switchToDatabases();
}
void MainWindow::switchToOpVaultDatabase()
{
m_ui->tabWidget->importOpVaultDatabase();
switchToDatabases();
}
void MainWindow::switchToCsvImport()
{
m_ui->tabWidget->importCsv();
switchToDatabases();
}
void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget)
{
Q_UNUSED(dbWidget);

View file

@ -24,6 +24,7 @@
#include <QMainWindow>
#include <QProgressBar>
#include <QSystemTrayIcon>
#include <QTimer>
#include "core/SignalMultiplexer.h"
#include "gui/DatabaseWidget.h"
@ -124,9 +125,6 @@ private slots:
void switchToNewDatabase();
void switchToOpenDatabase();
void switchToDatabaseFile(const QString& file);
void switchToKeePass1Database();
void switchToOpVaultDatabase();
void switchToCsvImport();
void databaseStatusChanged(DatabaseWidget* dbWidget);
void databaseTabChanged(int tabIndex);
void openRecentDatabase(QAction* action);

View file

@ -231,14 +231,6 @@
<string>&amp;Recent Databases</string>
</property>
</widget>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>&amp;Import</string>
</property>
<addaction name="actionImportCsv"/>
<addaction name="actionImportOpVault"/>
<addaction name="actionImportKeePass1"/>
</widget>
<widget class="QMenu" name="menuExport">
<property name="title">
<string>&amp;Export</string>
@ -266,7 +258,7 @@
<addaction name="actionImportPasskey"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="menuImport"/>
<addaction name="actionImport"/>
<addaction name="menuExport"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
@ -1271,6 +1263,19 @@
<string>Toggle Allow Screen Capture</string>
</property>
</action>
<action name="actionImport1PUX">
<property name="text">
<string>1Password 1PUX...</string>
</property>
<property name="toolTip">
<string>Import a 1Password 1PUX file</string>
</property>
</action>
<action name="actionImport">
<property name="text">
<string>Import…</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View file

@ -1,50 +0,0 @@
/*
* Copyright (C) 2019 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 "OpVaultOpenWidget.h"
#include "core/Database.h"
#include "format/OpVaultReader.h"
#include "ui_DatabaseOpenWidget.h"
OpVaultOpenWidget::OpVaultOpenWidget(QWidget* parent)
: DatabaseOpenWidget(parent)
{
m_ui->labelHeadline->setText("Import 1Password Database");
}
void OpVaultOpenWidget::openDatabase()
{
OpVaultReader reader;
QString password;
password = m_ui->editPassword->text();
QDir opVaultDir(m_filename);
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_db.reset(reader.readDatabase(opVaultDir, password));
QApplication::restoreOverrideCursor();
if (m_db) {
emit dialogFinished(true);
} else {
m_ui->messageWidget->showMessage(tr("Read Database did not produce an instance\n%1").arg(reader.errorString()),
MessageWidget::Error);
m_ui->editPassword->clear();
}
}

View file

@ -33,6 +33,7 @@ class PasswordWidget : public QWidget
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged USER true)
public:
explicit PasswordWidget(QWidget* parent = nullptr);
~PasswordWidget() override;

View file

@ -19,6 +19,8 @@
#ifndef KEEPASSX_TOTPDIALOG_H
#define KEEPASSX_TOTPDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View file

@ -25,6 +25,7 @@
#include <QBoxLayout>
#include <QBuffer>
#include <QDialogButtonBox>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>

View file

@ -18,6 +18,8 @@
#ifndef KEEPASSX_TotpExportSettingsDialog_H
#define KEEPASSX_TotpExportSettingsDialog_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View file

@ -19,6 +19,8 @@
#ifndef KEEPASSX_SETUPTOTPDIALOG_H
#define KEEPASSX_SETUPTOTPDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View file

@ -37,14 +37,15 @@ WelcomeWidget::WelcomeWidget(QWidget* parent)
m_ui->welcomeLabel->setFont(welcomeLabelFont);
m_ui->iconLabel->setPixmap(icons()->applicationIcon().pixmap(64));
m_ui->buttonNewDatabase->setIcon(icons()->icon("document-new"));
m_ui->buttonOpenDatabase->setIcon(icons()->icon("document-open"));
m_ui->buttonImport->setIcon(icons()->icon("document-import"));
refreshLastDatabases();
connect(m_ui->buttonNewDatabase, SIGNAL(clicked()), SIGNAL(newDatabase()));
connect(m_ui->buttonOpenDatabase, SIGNAL(clicked()), SIGNAL(openDatabase()));
connect(m_ui->buttonImportKeePass1, SIGNAL(clicked()), SIGNAL(importKeePass1Database()));
connect(m_ui->buttonImportOpVault, SIGNAL(clicked()), SIGNAL(importOpVaultDatabase()));
connect(m_ui->buttonImportCSV, SIGNAL(clicked()), SIGNAL(importCsv()));
connect(m_ui->buttonImport, SIGNAL(clicked()), SIGNAL(importFile()));
connect(m_ui->recentListWidget,
SIGNAL(itemActivated(QListWidgetItem*)),
this,

View file

@ -39,9 +39,7 @@ signals:
void newDatabase();
void openDatabase();
void openDatabaseFile(QString);
void importKeePass1Database();
void importOpVaultDatabase();
void importCsv();
void importFile();
protected:
void keyPressEvent(QKeyEvent* event) override;

View file

@ -70,6 +70,22 @@
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="welcomeLabel">
<property name="alignment">
@ -103,40 +119,26 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonNewDatabase">
<property name="text">
<string>Create new database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonOpenDatabase">
<property name="text">
<string>Open existing database</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="buttonImportKeePass1">
<widget class="QPushButton" name="buttonNewDatabase">
<property name="text">
<string>Import from KeePass 1</string>
<string>Create Database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImportOpVault">
<widget class="QPushButton" name="buttonOpenDatabase">
<property name="text">
<string>Import from 1Password</string>
<string>Open Database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImportCSV">
<widget class="QPushButton" name="buttonImport">
<property name="text">
<string>Import from CSV</string>
<string>Import File</string>
</property>
</widget>
</item>
@ -148,12 +150,12 @@
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>5</height>
<height>20</height>
</size>
</property>
</spacer>
@ -193,11 +195,7 @@
</layout>
</widget>
<tabstops>
<tabstop>buttonNewDatabase</tabstop>
<tabstop>buttonOpenDatabase</tabstop>
<tabstop>buttonImportKeePass1</tabstop>
<tabstop>buttonImportOpVault</tabstop>
<tabstop>buttonImportCSV</tabstop>
<tabstop>buttonImport</tabstop>
<tabstop>recentListWidget</tabstop>
</tabstops>
<resources/>

View file

@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
@ -19,38 +19,69 @@
#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/csvImport/CsvParserModel.h"
#include <QStringListModel>
#include "core/Clock.h"
#include "core/Totp.h"
#include "format/KeePass2Writer.h"
#include "gui/MessageBox.h"
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;
}
// 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!
auto nameList = groupPath.split("/", QString::SkipEmptyParts);
// Skip over first group name if root
if (nameList.first().compare("root", Qt::CaseInsensitive)) {
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_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"))
, m_fieldSeparatorList(QStringList() << ","
<< ";"
<< "-"
<< ":"
<< "."
<< "\t")
{
m_ui->setupUi(this);
m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus);
m_ui->messageWidget->setHidden(true);
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;
@ -70,15 +101,12 @@ CsvImportWidget::CsvImportWidget(QWidget* parent)
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(int index)
{
// this line is the one that actually updates GUI table
m_parserModel->mapColumns(index, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
m_parserModel->mapColumns(index - 1, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
updateTableview();
}
@ -92,68 +120,81 @@ CsvImportWidget::~CsvImportWidget() = default;
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));
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()
{
m_ui->tableViewFields->resizeRowsToContents();
m_ui->tableViewFields->resizeColumnsToContents();
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);
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_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 list(tr("Not Present"));
for (int i = 1; i < m_parserModel->getCsvCols(); ++i) {
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 = m_parserModel->getCsvTable().at(0).at(i);
auto columnName = parser->getCsvTable().at(0).at(i);
if (columnName.isEmpty()) {
list << QString(tr("Column %1").arg(i));
csvColumns << QString(tr("Column %1").arg(i));
} else {
list << columnName;
csvColumns << columnName;
}
} else {
list << QString(tr("Column %1").arg(i));
csvColumns << QString(tr("Column %1").arg(i));
}
}
m_comboModel->setStringList(list);
m_comboModel->setStringList(csvColumns);
int j = 1;
for (QComboBox* b : m_combos) {
if (j < m_parserModel->getCsvCols()) {
b->setCurrentIndex(j);
} else {
b->setCurrentIndex(0);
// 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);
}
++j;
}
m_buildingPreview = false;
updateTableview();
}
void CsvImportWidget::load(const QString& filename, Database* const db)
void CsvImportWidget::load(const QString& filename)
{
// QApplication::processEvents();
m_db = db;
m_filename = filename;
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();
}
@ -161,42 +202,33 @@ void CsvImportWidget::parse()
{
configParser();
QApplication::setOverrideCursor(Qt::WaitCursor);
// QApplication::processEvents();
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);
emit message(tr("Failed to parse CSV file: %1").arg(formatStatusText()));
}
}
QString CsvImportWidget::formatStatusText() const
QSharedPointer<Database> CsvImportWidget::buildDatabase()
{
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;
}
auto db = QSharedPointer<Database>::create();
db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename));
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()) {
if (!m_parserModel->data(m_parserModel->index(r, 1)).isValid()) {
continue;
}
auto group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString());
if (!group) {
continue;
}
auto entry = new Entry();
entry->setUuid(QUuid::createUuid());
entry->setGroup(splitGroups(m_parserModel->data(m_parserModel->index(r, 0)).toString()));
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());
@ -255,99 +287,19 @@ void CsvImportWidget::writeDatabase()
}
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()),
MessageBox::Ok,
MessageBox::Ok);
}
emit editFinished(true);
return db;
}
void CsvImportWidget::setRootGroup()
QString CsvImportWidget::formatStatusText() const
{
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();
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 ((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");
if (items == 1) {
text.append(QString("\n"));
}
}
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) {
auto 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);
return text;
}

View file

@ -19,12 +19,13 @@
#ifndef KEEPASSX_CSVIMPORTWIDGET_H
#define KEEPASSX_CSVIMPORTWIDGET_H
#include <QComboBox>
#include <QStringListModel>
#include <QWidget>
#include "core/Metadata.h"
#include "gui/csvImport/CsvParserModel.h"
class QStringListModel;
class CsvParserModel;
class Database;
class Group;
class QComboBox;
namespace Ui
{
@ -38,35 +39,35 @@ class CsvImportWidget : public QWidget
public:
explicit CsvImportWidget(QWidget* parent = nullptr);
~CsvImportWidget() override;
void load(const QString& filename, Database* const db);
void load(const QString& filename);
QSharedPointer<Database> buildDatabase();
signals:
void editFinished(bool accepted);
void message(QString msg);
private slots:
void parse();
void comboChanged(int index);
void skippedChanged(int rows);
void writeDatabase();
void updatePreview();
void setRootGroup();
void reject();
private:
Q_DISABLE_COPY(CsvImportWidget)
const QScopedPointer<Ui::CsvImportWidget> m_ui;
CsvParserModel* const m_parserModel;
QStringListModel* const m_comboModel;
QList<QComboBox*> m_combos;
Database* m_db;
const QStringList m_columnHeader;
QStringList m_fieldSeparatorList;
void configParser();
void updateTableview();
Group* splitGroups(const QString& label);
Group* hasChildren(Group* current, const QString& groupName);
QString formatStatusText() const;
QScopedPointer<Ui::CsvImportWidget> m_ui;
CsvParserModel* m_parserModel;
QStringListModel* m_comboModel;
QList<QComboBox*> m_combos;
QStringList m_columnHeader;
QStringList m_fieldSeparatorList;
QString m_filename;
bool m_buildingPreview = false;
Q_DISABLE_COPY(CsvImportWidget)
};
#endif // KEEPASSX_CSVIMPORTWIDGET_H

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
/*
* 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 "CsvImportWizard.h"
#include <QGridLayout>
CsvImportWizard::CsvImportWizard(QWidget* parent)
: DialogyWidget(parent)
{
m_layout = new QGridLayout(this);
m_layout->addWidget(m_parse = new CsvImportWidget(this), 0, 0);
connect(m_parse, SIGNAL(editFinished(bool)), this, SLOT(parseFinished(bool)));
}
CsvImportWizard::~CsvImportWizard() = default;
void CsvImportWizard::load(const QString& filename, Database* database)
{
m_db = database;
m_parse->load(filename, database);
}
void CsvImportWizard::parseFinished(bool accepted)
{
emit importFinished(accepted);
}

View file

@ -1,51 +0,0 @@
/*
* 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/>.
*/
#ifndef KEEPASSX_CSVIMPORTWIZARD_H
#define KEEPASSX_CSVIMPORTWIZARD_H
#include "CsvImportWidget.h"
#include "gui/DialogyWidget.h"
class QGridLayout;
class CsvImportWidget;
class CsvImportWizard : public DialogyWidget
{
Q_OBJECT
public:
explicit CsvImportWizard(QWidget* parent = nullptr);
~CsvImportWizard() override;
void load(const QString& filename, Database* database);
signals:
void importFinished(bool accepted);
private slots:
void parseFinished(bool accepted);
private:
QPointer<Database> m_db;
CsvImportWidget* m_parse;
QGridLayout* m_layout;
};
#endif // KEEPASSX_CSVIMPORTWIZARD_H

View file

@ -18,16 +18,25 @@
#include "CsvParserModel.h"
#include "core/Tools.h"
#include "format/CsvParser.h"
#include <QFile>
CsvParserModel::CsvParserModel(QObject* parent)
: QAbstractTableModel(parent)
, m_parser(new CsvParser())
, m_skipped(0)
{
}
CsvParserModel::~CsvParserModel() = default;
CsvParser* CsvParserModel::parser()
{
return m_parser;
}
void CsvParserModel::setFilename(const QString& filename)
{
m_filename = filename;
@ -35,11 +44,10 @@ void CsvParserModel::setFilename(const QString& filename)
QString CsvParserModel::getFileInfo()
{
QString a(tr("%1, %2, %3", "file info: bytes, rows, columns")
.arg(tr("%n byte(s)", nullptr, getFileSize()),
tr("%n row(s)", nullptr, getCsvRows()),
tr("%n column(s)", nullptr, qMax(0, getCsvCols() - 1))));
return a;
return QString("%1, %2, %3")
.arg(Tools::humanReadableFileSize(m_parser->getFileSize()),
tr("%n row(s)", "CSV row count", m_parser->getCsvRows()),
tr("%n column(s)", "CSV column count", qMax(0, m_parser->getCsvCols() - 1)));
}
bool CsvParserModel::parse()
@ -47,37 +55,28 @@ bool CsvParserModel::parse()
bool r;
beginResetModel();
m_columnMap.clear();
if (CsvParser::isFileLoaded()) {
r = CsvParser::reparse();
if (m_parser->isFileLoaded()) {
r = m_parser->reparse();
} else {
QFile csv(m_filename);
r = CsvParser::parse(&csv);
r = m_parser->parse(&csv);
}
for (int i = 0; i < columnCount(); ++i) {
m_columnMap.insert(i, 0);
}
addEmptyColumn();
endResetModel();
return r;
}
void CsvParserModel::addEmptyColumn()
{
for (int i = 0; i < m_table.size(); ++i) {
CsvRow r = m_table.at(i);
r.prepend(QString(""));
m_table.replace(i, r);
}
}
void CsvParserModel::mapColumns(int csvColumn, int dbColumn)
{
if ((csvColumn < 0) || (dbColumn < 0)) {
if (dbColumn < 0 || dbColumn >= m_columnMap.size()) {
return;
}
beginResetModel();
if (csvColumn >= getCsvCols()) {
m_columnMap[dbColumn] = 0; // map to the empty column
if (csvColumn < 0 || csvColumn >= m_parser->getCsvCols()) {
// This indicates a blank cell
m_columnMap[dbColumn] = -1;
} else {
m_columnMap[dbColumn] = csvColumn;
}
@ -103,7 +102,7 @@ int CsvParserModel::rowCount(const QModelIndex& parent) const
if (parent.isValid()) {
return 0;
}
return getCsvRows();
return m_parser->getCsvRows();
}
int CsvParserModel::columnCount(const QModelIndex& parent) const
@ -116,11 +115,14 @@ int CsvParserModel::columnCount(const QModelIndex& parent) const
QVariant CsvParserModel::data(const QModelIndex& index, int role) const
{
if ((index.column() >= m_columnHeader.size()) || (index.row() + m_skipped >= rowCount()) || !index.isValid()) {
if (index.column() >= m_columnHeader.size() || index.row() + m_skipped >= rowCount() || !index.isValid()) {
return {};
}
if (role == Qt::DisplayRole) {
return m_table.at(index.row() + m_skipped).at(m_columnMap[index.column()]);
auto column = m_columnMap[index.column()];
if (column >= 0) {
return m_parser->getCsvTable().at(index.row() + m_skipped).at(column);
}
}
return {};
}
@ -129,15 +131,13 @@ QVariant CsvParserModel::headerData(int section, Qt::Orientation orientation, in
{
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
if ((section < 0) || (section >= m_columnHeader.size())) {
return {};
if (section >= 0 && section < m_columnHeader.size()) {
return m_columnHeader.at(section);
}
return m_columnHeader.at(section);
} else if (orientation == Qt::Vertical) {
if (section + m_skipped >= rowCount()) {
return {};
if (section + m_skipped < rowCount()) {
return QString::number(section + 1);
}
return QString::number(section + 1);
}
}
return {};

View file

@ -21,20 +21,22 @@
#include <QAbstractTableModel>
#include "core/Group.h"
#include "format/CsvParser.h"
class CsvParser;
class CsvParserModel : public QAbstractTableModel, public CsvParser
class CsvParserModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit CsvParserModel(QObject* parent = nullptr);
~CsvParserModel() override;
void setFilename(const QString& filename);
QString getFileInfo();
bool parse();
CsvParser* parser();
void setHeaderLabels(const QStringList& labels);
void mapColumns(int csvColumn, int dbColumn);
@ -47,12 +49,12 @@ public slots:
void setSkippedRows(int skipped);
private:
CsvParser* m_parser;
int m_skipped;
QString m_filename;
QStringList m_columnHeader;
// first column of model must be empty (aka combobox row "Not present in CSV file")
void addEmptyColumn();
// mapping CSV columns to keepassx columns
QMap<int, int> m_columnMap;
};

View file

@ -24,6 +24,7 @@
#include "TagsEdit.h"
#include "gui/MainWindow.h"
#include <QAbstractItemView>
#include <QApplication>
#include <QClipboard>
#include <QCompleter>

View file

@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 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 "ImportWizard.h"
#include "ImportWizardPageReview.h"
#include "ImportWizardPageSelect.h"
#include "core/Global.h"
#include "core/Group.h"
#include <QFrame>
#include <QPalette>
ImportWizard::ImportWizard(QWidget* parent)
: QWizard(parent)
, m_pageSelect(new ImportWizardPageSelect)
, m_pageReview(new ImportWizardPageReview)
{
setWizardStyle(MacStyle);
setOption(HaveHelpButton, false);
setOption(NoDefaultButton, false); // Needed for macOS
addPage(m_pageSelect.data());
addPage(m_pageReview.data());
setWindowTitle(tr("Import Wizard"));
Q_INIT_RESOURCE(wizard);
setPixmap(BackgroundPixmap, QPixmap(":/wizard/background-pixmap.png"));
// Fix MacStyle QWizard page frame too bright in dark mode (QTBUG-70346, QTBUG-71696)
QPalette defaultPalette;
auto windowColor = defaultPalette.color(QPalette::Window);
windowColor.setAlpha(153);
auto baseColor = defaultPalette.color(QPalette::Base);
baseColor.setAlpha(153);
auto* pageFrame = findChildren<QFrame*>()[0];
auto framePalette = pageFrame->palette();
framePalette.setBrush(QPalette::Window, windowColor.lighter(120));
framePalette.setBrush(QPalette::Base, baseColor.lighter(120));
pageFrame->setPalette(framePalette);
}
ImportWizard::~ImportWizard()
{
}
bool ImportWizard::validateCurrentPage()
{
bool ret = QWizard::validateCurrentPage();
if (ret && currentPage() == m_pageReview) {
m_db = m_pageReview->database();
}
return ret;
}
QPair<QUuid, QUuid> ImportWizard::importInto()
{
auto list = field("ImportInto").toList();
if (list.size() >= 2) {
return qMakePair(QUuid(list[0].toString()), QUuid(list[1].toString()));
}
return {};
}
QSharedPointer<Database> ImportWizard::database()
{
return m_db;
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2018 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/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARD_H
#define KEEPASSXC_IMPORTWIZARD_H
#include <QPointer>
#include <QWizard>
class Database;
class ImportWizardPageSelect;
class ImportWizardPageReview;
/**
* Setup wizard for importing a file into a database.
*/
class ImportWizard : public QWizard
{
Q_OBJECT
public:
explicit ImportWizard(QWidget* parent = nullptr);
~ImportWizard() override;
bool validateCurrentPage() override;
QSharedPointer<Database> database();
QPair<QUuid, QUuid> importInto();
enum ImportType
{
IMPORT_NONE = 0,
IMPORT_CSV,
IMPORT_OPVAULT,
IMPORT_OPUX,
IMPORT_BITWARDEN,
IMPORT_KEEPASS1
};
private:
QSharedPointer<Database> m_db;
QPointer<ImportWizardPageSelect> m_pageSelect;
QPointer<ImportWizardPageReview> m_pageReview;
};
#endif // KEEPASSXC_IMPORTWIZARD_H

View file

@ -0,0 +1,202 @@
/*
* Copyright (C) 2023 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 "ImportWizardPageReview.h"
#include "ui_ImportWizardPageReview.h"
#include "core/Database.h"
#include "core/Group.h"
#include "format/BitwardenReader.h"
#include "format/KeePass1Reader.h"
#include "format/OPUXReader.h"
#include "format/OpVaultReader.h"
#include "gui/csvImport/CsvImportWidget.h"
#include "gui/wizard/ImportWizard.h"
#include <QBoxLayout>
#include <QDir>
#include <QHeaderView>
#include <QTableWidget>
ImportWizardPageReview::ImportWizardPageReview(QWidget* parent)
: QWizardPage(parent)
, m_ui(new Ui::ImportWizardPageReview)
{
}
ImportWizardPageReview::~ImportWizardPageReview()
{
}
void ImportWizardPageReview::initializePage()
{
m_db.reset();
// Reset the widget in case we changed the import type
for (auto child : children()) {
delete child;
}
m_ui->setupUi(this);
auto filename = field("ImportFile").toString();
m_ui->filenameLabel->setText(filename);
m_ui->messageWidget->hideMessage();
m_ui->messageWidget->setAnimate(false);
m_ui->messageWidget->setCloseButtonVisible(false);
auto importType = field("ImportType").toInt();
switch (importType) {
case ImportWizard::IMPORT_CSV:
setupCsvImport(filename);
break;
case ImportWizard::IMPORT_OPVAULT:
m_db = importOPVault(filename, field("ImportPassword").toString());
setupDatabasePreview();
break;
case ImportWizard::IMPORT_OPUX:
m_db = importOPUX(filename);
setupDatabasePreview();
break;
case ImportWizard::IMPORT_KEEPASS1:
m_db = importKeePass1(filename, field("ImportPassword").toString(), field("ImportKeyFile").toString());
setupDatabasePreview();
break;
case ImportWizard::IMPORT_BITWARDEN:
m_db = importBitwarden(filename, field("ImportPassword").toString());
setupDatabasePreview();
break;
default:
break;
}
}
bool ImportWizardPageReview::validatePage()
{
if (m_csvWidget && field("ImportType").toInt() == ImportWizard::IMPORT_CSV) {
m_db = m_csvWidget->buildDatabase();
}
return !m_db.isNull();
}
QSharedPointer<Database> ImportWizardPageReview::database()
{
return m_db;
}
void ImportWizardPageReview::setupCsvImport(const QString& filename)
{
// No need for this label with CSV
m_ui->previewLabel->hide();
m_csvWidget = new CsvImportWidget();
connect(m_csvWidget, &CsvImportWidget::message, m_ui->messageWidget, [this](QString message) {
m_ui->messageWidget->showMessage(message, KMessageWidget::Error, -1);
});
m_csvWidget->load(filename);
// Qt does not automatically resize a QScrollWidget in a QWizard...
m_ui->scrollAreaContents->layout()->addWidget(m_csvWidget);
m_ui->scrollArea->setMinimumSize(m_csvWidget->width() + 50, m_csvWidget->height() + 100);
}
void ImportWizardPageReview::setupDatabasePreview()
{
if (!m_db) {
m_ui->scrollArea->setVisible(false);
return;
}
auto entryList = m_db->rootGroup()->entriesRecursive();
m_ui->previewLabel->setText(tr("Entry count: %1").arg(entryList.count()));
QStringList headerLabels({tr("Group"), tr("Title"), tr("Username"), tr("Password"), tr("Url")});
auto tableWidget = new QTableWidget(entryList.count(), headerLabels.count());
tableWidget->setHorizontalHeaderLabels(headerLabels);
int row = 0;
for (auto entry : entryList) {
QList items({new QTableWidgetItem(entry->group()->name()),
new QTableWidgetItem(entry->title()),
new QTableWidgetItem(entry->username()),
new QTableWidgetItem(entry->password()),
new QTableWidgetItem(entry->url())});
int column = 0;
for (auto item : items) {
tableWidget->setItem(row, column++, item);
}
++row;
}
tableWidget->setSortingEnabled(true);
tableWidget->setSelectionMode(QTableWidget::NoSelection);
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
tableWidget->setWordWrap(true);
tableWidget->horizontalHeader()->setMaximumSectionSize(200);
tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
tableWidget->horizontalHeader()->setStretchLastSection(true);
m_ui->scrollAreaContents->layout()->addWidget(tableWidget);
}
QSharedPointer<Database> ImportWizardPageReview::importOPUX(const QString& filename)
{
OPUXReader reader;
auto db = reader.convert(filename);
if (reader.hasError()) {
m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1);
}
return db;
}
QSharedPointer<Database> ImportWizardPageReview::importBitwarden(const QString& filename, const QString& password)
{
BitwardenReader reader;
auto db = reader.convert(filename, password);
if (reader.hasError()) {
m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1);
}
return db;
}
QSharedPointer<Database> ImportWizardPageReview::importOPVault(const QString& filename, const QString& password)
{
OpVaultReader reader;
QDir opVault(filename);
auto db = reader.convert(opVault, password);
if (reader.hasError()) {
m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1);
}
return db;
}
QSharedPointer<Database>
ImportWizardPageReview::importKeePass1(const QString& filename, const QString& password, const QString& keyfile)
{
KeePass1Reader reader;
// TODO: Handle case of empty password?
auto db = reader.readDatabase(filename, password, keyfile);
if (reader.hasError()) {
m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1);
}
return db;
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 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/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARDPAGEREVIEW_H
#define KEEPASSXC_IMPORTWIZARDPAGEREVIEW_H
#include <QPointer>
#include <QWizardPage>
class CsvImportWidget;
class Database;
namespace Ui
{
class ImportWizardPageReview;
};
class ImportWizardPageReview : public QWizardPage
{
Q_OBJECT
public:
explicit ImportWizardPageReview(QWidget* parent = nullptr);
Q_DISABLE_COPY(ImportWizardPageReview)
~ImportWizardPageReview() override;
void initializePage() override;
bool validatePage() override;
QSharedPointer<Database> database();
private:
void setupCsvImport(const QString& filename);
QSharedPointer<Database> importOPUX(const QString& filename);
QSharedPointer<Database> importBitwarden(const QString& filename, const QString& password);
QSharedPointer<Database> importOPVault(const QString& filename, const QString& password);
QSharedPointer<Database> importKeePass1(const QString& filename, const QString& password, const QString& keyfile);
void setupDatabasePreview();
QScopedPointer<Ui::ImportWizardPageReview> m_ui;
QSharedPointer<Database> m_db;
QPointer<CsvImportWidget> m_csvWidget;
};
#endif

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImportWizardPageReview</class>
<widget class="QWizardPage" name="ImportWizardPageReview">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>518</width>
<height>334</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="minimumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>498</width>
<height>298</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="filenameLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">filename</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="previewLabel">
<property name="text">
<string notr="true">Entry count: %1</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,236 @@
/*
* Copyright (C) 2023 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 "ImportWizardPageSelect.h"
#include "ui_ImportWizardPageSelect.h"
#include "ImportWizard.h"
#include "gui/DatabaseWidget.h"
#include "gui/FileDialog.h"
#include "gui/Icons.h"
#include "gui/MainWindow.h"
ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent)
: QWizardPage(parent)
, m_ui(new Ui::ImportWizardPageSelect())
{
m_ui->setupUi(this);
new QListWidgetItem(icons()->icon("csv"), tr("Comma Separated Values (.csv)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Export (.1pux)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Vault (.opvault)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("bitwarden"), tr("Bitwarden (.json)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("object-locked"), tr("KeePass 1 Database (.kdb)"), m_ui->importTypeList);
m_ui->importTypeList->item(0)->setData(Qt::UserRole, ImportWizard::IMPORT_CSV);
m_ui->importTypeList->item(1)->setData(Qt::UserRole, ImportWizard::IMPORT_OPUX);
m_ui->importTypeList->item(2)->setData(Qt::UserRole, ImportWizard::IMPORT_OPVAULT);
m_ui->importTypeList->item(3)->setData(Qt::UserRole, ImportWizard::IMPORT_BITWARDEN);
m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1);
connect(m_ui->importTypeList, &QListWidget::currentItemChanged, this, &ImportWizardPageSelect::itemSelected);
m_ui->importTypeList->setCurrentRow(0);
connect(m_ui->importFileButton, &QAbstractButton::clicked, this, &ImportWizardPageSelect::chooseImportFile);
connect(m_ui->keyFileButton, &QAbstractButton::clicked, this, &ImportWizardPageSelect::chooseKeyFile);
connect(m_ui->existingDatabaseRadio, &QRadioButton::toggled, this, [this](bool state) {
m_ui->existingDatabaseChoice->setEnabled(state);
});
updateDatabaseChoices();
registerField("ImportType", this);
registerField("ImportFile*", m_ui->importFileEdit);
registerField("ImportInto", m_ui->importIntoLabel);
registerField("ImportPassword", m_ui->passwordEdit, "text", "textChanged");
registerField("ImportKeyFile", m_ui->keyFileEdit);
}
ImportWizardPageSelect::~ImportWizardPageSelect()
{
}
void ImportWizardPageSelect::initializePage()
{
setField("ImportType", m_ui->importTypeList->currentItem()->data(Qt::UserRole).toInt());
adjustSize();
}
bool ImportWizardPageSelect::validatePage()
{
if (m_ui->existingDatabaseRadio->isChecked()) {
if (m_ui->existingDatabaseChoice->currentIndex() == -1) {
return false;
}
setField("ImportInto", m_ui->existingDatabaseChoice->currentData());
} else {
setField("ImportInto", {});
}
return true;
}
void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetItem* previous)
{
Q_UNUSED(previous)
if (!current) {
setCredentialState(false);
return;
}
m_ui->importFileEdit->clear();
m_ui->passwordEdit->clear();
m_ui->keyFileEdit->clear();
auto type = current->data(Qt::UserRole).toInt();
setField("ImportType", type);
switch (type) {
// Unencrypted types
case ImportWizard::IMPORT_CSV:
case ImportWizard::IMPORT_OPUX:
setCredentialState(false);
break;
// Password may be required
case ImportWizard::IMPORT_BITWARDEN:
case ImportWizard::IMPORT_OPVAULT:
setCredentialState(true);
break;
// Password and/or Key File may be required
case ImportWizard::IMPORT_KEEPASS1:
setCredentialState(true, true);
break;
default:
Q_ASSERT(false);
}
}
void ImportWizardPageSelect::updateDatabaseChoices() const
{
m_ui->existingDatabaseChoice->clear();
auto mainWindow = getMainWindow();
if (mainWindow) {
for (auto dbWidget : mainWindow->getOpenDatabases()) {
// Skip over locked databases
if (dbWidget->isLocked()) {
continue;
}
// Enable the selection of an existing database
m_ui->existingDatabaseRadio->setEnabled(true);
m_ui->existingDatabaseRadio->setToolTip("");
// Add a separator between databases
if (m_ui->existingDatabaseChoice->count() > 0) {
m_ui->existingDatabaseChoice->insertSeparator(m_ui->existingDatabaseChoice->count());
}
// Add the root group as a special line item
auto db = dbWidget->database();
m_ui->existingDatabaseChoice->addItem(
QString("%1 (%2)").arg(dbWidget->displayName(), db->rootGroup()->name()),
QList<QVariant>() << db->uuid() << db->rootGroup()->uuid());
if (dbWidget->isVisible()) {
m_ui->existingDatabaseChoice->setCurrentIndex(m_ui->existingDatabaseChoice->count() - 1);
}
// Add remaining groups
for (const auto& group : db->rootGroup()->groupsRecursive(false)) {
if (!group->isRecycled()) {
auto path = group->hierarchy();
path.removeFirst();
m_ui->existingDatabaseChoice->addItem(QString(" / %1").arg(path.join(" / ")),
QList<QVariant>() << db->uuid() << group->uuid());
}
}
}
}
}
void ImportWizardPageSelect::chooseImportFile()
{
QString file;
#ifndef Q_OS_MACOS
// OPVault is a folder except on macOS
if (field("ImportType").toInt() == ImportWizard::IMPORT_OPVAULT) {
file = fileDialog()->getExistingDirectory(this, tr("Open OPVault"), QDir::homePath());
} else {
#endif
file = fileDialog()->getOpenFileName(this, tr("Select import file"), QDir::homePath(), importFileFilter());
#ifndef Q_OS_MACOS
}
#endif
if (!file.isEmpty()) {
m_ui->importFileEdit->setText(file);
}
}
void ImportWizardPageSelect::chooseKeyFile()
{
auto filter = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files"));
auto file = fileDialog()->getOpenFileName(this, tr("Select key file"), QDir::homePath(), filter);
if (!file.isEmpty()) {
m_ui->keyFileEdit->setText(file);
}
}
void ImportWizardPageSelect::setCredentialState(bool passwordEnabled, bool keyFileEnable)
{
bool passwordStateChanged = m_ui->passwordLabel->isVisible() != passwordEnabled;
m_ui->passwordLabel->setVisible(passwordEnabled);
m_ui->passwordEdit->setVisible(passwordEnabled);
bool keyFileStateChanged = m_ui->keyFileLabel->isVisible() != keyFileEnable;
m_ui->keyFileLabel->setVisible(keyFileEnable);
m_ui->keyFileEdit->setVisible(keyFileEnable);
m_ui->keyFileButton->setVisible(keyFileEnable);
// Workaround Qt bug where the wizard window is not updated when the internal layout changes
if (window()) {
int height = window()->height();
if (passwordStateChanged) {
auto diff = m_ui->passwordEdit->height() + m_ui->inputFields->layout()->spacing();
height += passwordEnabled ? diff : -diff;
}
if (keyFileStateChanged) {
auto diff = m_ui->keyFileEdit->height() + m_ui->inputFields->layout()->spacing();
height += keyFileEnable ? diff : -diff;
}
window()->resize(window()->width(), height);
}
}
QString ImportWizardPageSelect::importFileFilter()
{
switch (field("ImportType").toInt()) {
case ImportWizard::IMPORT_CSV:
return QString("%1 (*.csv);;%2 (*)").arg(tr("Comma Separated Values"), tr("All files"));
case ImportWizard::IMPORT_OPUX:
return QString("%1 (*.1pux)").arg(tr("1Password Export"));
case ImportWizard::IMPORT_BITWARDEN:
return QString("%1 (*.json)").arg(tr("Bitwarden JSON Export"));
case ImportWizard::IMPORT_OPVAULT:
return QString("%1 (*.opvault)").arg(tr("1Password Vault"));
case ImportWizard::IMPORT_KEEPASS1:
return QString("%1 (*.kdb)").arg(tr("KeePass1 Database"));
default:
return {};
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 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/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARDPAGESELECT_H
#define KEEPASSXC_IMPORTWIZARDPAGESELECT_H
#include <QPointer>
#include <QWizardPage>
class QListWidgetItem;
namespace Ui
{
class ImportWizardPageSelect;
}
class ImportWizardPageSelect : public QWizardPage
{
Q_OBJECT
public:
explicit ImportWizardPageSelect(QWidget* parent = nullptr);
Q_DISABLE_COPY(ImportWizardPageSelect)
~ImportWizardPageSelect() override;
void initializePage() override;
bool validatePage() override;
private slots:
void itemSelected(QListWidgetItem* current, QListWidgetItem* previous);
void chooseImportFile();
void chooseKeyFile();
void updateDatabaseChoices() const;
private:
QString importFileFilter();
void setCredentialState(bool passwordEnabled, bool keyFileEnable = false);
QScopedPointer<Ui::ImportWizardPageSelect> m_ui;
};
#endif

View file

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImportWizardPageSelect</class>
<widget class="QWizardPage" name="ImportWizardPageSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>388</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="title">
<string>Import File Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="importTypeList">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>125</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="inputFields" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="importFileLabel">
<property name="text">
<string>Import File:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="importFileLayout">
<item>
<widget class="QLineEdit" name="importFileEdit"/>
</item>
<item>
<widget class="QPushButton" name="importFileButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="PasswordWidget" name="passwordEdit" native="true"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="keyFileLabel">
<property name="text">
<string>Key File:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="keyFileLayout">
<item>
<widget class="QLineEdit" name="keyFileEdit"/>
</item>
<item>
<widget class="QPushButton" name="keyFileButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="importIntoLabel">
<property name="text">
<string>Import Into:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QGroupBox" name="importIntoGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item>
<widget class="QRadioButton" name="newDatabaseRadio">
<property name="text">
<string>New Database</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="existingDatabaseLayout" stretch="0,1">
<item>
<widget class="QRadioButton" name="existingDatabaseRadio">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>No unlocked databases available</string>
</property>
<property name="text">
<string>Existing Database:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="existingDatabaseChoice">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PasswordWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -13,8 +13,6 @@ if(WITH_XC_KEESHARE)
ShareObserver.cpp
)
find_package(Minizip REQUIRED)
add_library(keeshare STATIC ${keeshare_SOURCES})
target_link_libraries(keeshare PUBLIC Qt5::Core Qt5::Widgets ${BOTAN_LIBRARIES} ${ZLIB_LIBRARIES} PRIVATE ${MINIZIP_LIBRARIES})
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})