mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-09-20 21:04:44 -04:00
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:
parent
a02bceabd2
commit
e700195f0a
70 changed files with 3562 additions and 1876 deletions
|
@ -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}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QPluginLoader>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
|
||||
#include "config-keepassx.h"
|
||||
|
|
313
src/format/BitwardenReader.cpp
Normal file
313
src/format/BitwardenReader.cpp
Normal 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;
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
288
src/format/OPUXReader.cpp
Normal 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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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_ */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#ifndef KEEPASSX_CLONEDIALOG_H
|
||||
#define KEEPASSX_CLONEDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
#endif
|
||||
|
||||
#include <QScrollBar>
|
||||
|
||||
#include <QTabWidget>
|
||||
namespace
|
||||
{
|
||||
constexpr int GeneralTabIndex = 0;
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace Ui
|
|||
class EntryPreviewWidget;
|
||||
}
|
||||
|
||||
class QTabWidget;
|
||||
class QTextEdit;
|
||||
|
||||
class EntryPreviewWidget : public QWidget
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -231,14 +231,6 @@
|
|||
<string>&Recent Databases</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuImport">
|
||||
<property name="title">
|
||||
<string>&Import</string>
|
||||
</property>
|
||||
<addaction name="actionImportCsv"/>
|
||||
<addaction name="actionImportOpVault"/>
|
||||
<addaction name="actionImportKeePass1"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuExport">
|
||||
<property name="title">
|
||||
<string>&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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
#ifndef KEEPASSX_TOTPDIALOG_H
|
||||
#define KEEPASSX_TOTPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include <QBoxLayout>
|
||||
#include <QBuffer>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#ifndef KEEPASSX_TotpExportSettingsDialog_H
|
||||
#define KEEPASSX_TotpExportSettingsDialog_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
#ifndef KEEPASSX_SETUPTOTPDIALOG_H
|
||||
#define KEEPASSX_SETUPTOTPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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 {};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
#include "TagsEdit.h"
|
||||
#include "gui/MainWindow.h"
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCompleter>
|
||||
|
|
84
src/gui/wizard/ImportWizard.cpp
Normal file
84
src/gui/wizard/ImportWizard.cpp
Normal 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;
|
||||
}
|
60
src/gui/wizard/ImportWizard.h
Normal file
60
src/gui/wizard/ImportWizard.h
Normal 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
|
202
src/gui/wizard/ImportWizardPageReview.cpp
Normal file
202
src/gui/wizard/ImportWizardPageReview.cpp
Normal 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;
|
||||
}
|
60
src/gui/wizard/ImportWizardPageReview.h
Normal file
60
src/gui/wizard/ImportWizardPageReview.h
Normal 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
|
95
src/gui/wizard/ImportWizardPageReview.ui
Normal file
95
src/gui/wizard/ImportWizardPageReview.ui
Normal 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>
|
236
src/gui/wizard/ImportWizardPageSelect.cpp
Normal file
236
src/gui/wizard/ImportWizardPageSelect.cpp
Normal 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 {};
|
||||
}
|
||||
}
|
56
src/gui/wizard/ImportWizardPageSelect.h
Normal file
56
src/gui/wizard/ImportWizardPageSelect.h
Normal 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
|
276
src/gui/wizard/ImportWizardPageSelect.ui
Normal file
276
src/gui/wizard/ImportWizardPageSelect.ui
Normal 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>
|
|
@ -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})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue