mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-23 16:10:00 -05:00
Introduce synchronize merge method
* Create history-based merging that keeps older data in history instead of discarding or deleting it * Extract merge logic into the Merger class * Allows special merge behavior * Improve handling of deletion and changes on groups * Enable basic change tracking while merging * Prevent unintended timestamp changes while merging * Handle differences in timestamp precision * Introduce comparison operators to allow for more sophisticated comparisons (ignore special properties, ...) * Introduce Clock class to handle datetime across the app Merge Strategies: * Default (use inherited/fallback method) * Duplicate (duplicate conflicting nodes, apply all deletions) * KeepLocal (use local values, but apply all deletions) * KeepRemote (use remote values, but apply all deletions) * KeepNewer (merge history only) * Synchronize (merge history, newest value stays on top, apply all deletions)
This commit is contained in:
parent
b40e5686dc
commit
c1e9f45df9
@ -40,6 +40,7 @@ set(keepassx_SOURCES
|
|||||||
core/AutoTypeAssociations.cpp
|
core/AutoTypeAssociations.cpp
|
||||||
core/AsyncTask.h
|
core/AsyncTask.h
|
||||||
core/AutoTypeMatch.cpp
|
core/AutoTypeMatch.cpp
|
||||||
|
core/Compare.cpp
|
||||||
core/Config.cpp
|
core/Config.cpp
|
||||||
core/CsvParser.cpp
|
core/CsvParser.cpp
|
||||||
core/CustomData.cpp
|
core/CustomData.cpp
|
||||||
@ -54,6 +55,7 @@ set(keepassx_SOURCES
|
|||||||
core/Group.cpp
|
core/Group.cpp
|
||||||
core/InactivityTimer.cpp
|
core/InactivityTimer.cpp
|
||||||
core/ListDeleter.h
|
core/ListDeleter.h
|
||||||
|
core/Merger.cpp
|
||||||
core/Metadata.cpp
|
core/Metadata.cpp
|
||||||
core/PasswordGenerator.cpp
|
core/PasswordGenerator.cpp
|
||||||
core/PassphraseGenerator.cpp
|
core/PassphraseGenerator.cpp
|
||||||
@ -64,6 +66,7 @@ set(keepassx_SOURCES
|
|||||||
core/ScreenLockListenerPrivate.cpp
|
core/ScreenLockListenerPrivate.cpp
|
||||||
core/TimeDelta.cpp
|
core/TimeDelta.cpp
|
||||||
core/TimeInfo.cpp
|
core/TimeInfo.cpp
|
||||||
|
core/Clock.cpp
|
||||||
core/Tools.cpp
|
core/Tools.cpp
|
||||||
core/Translator.cpp
|
core/Translator.cpp
|
||||||
core/Base32.h
|
core/Base32.h
|
||||||
|
@ -756,9 +756,8 @@ bool AutoType::verifyAutoTypeSyntax(const QString& sequence)
|
|||||||
}
|
}
|
||||||
} else if (AutoType::checkHighRepetition(sequence)) {
|
} else if (AutoType::checkHighRepetition(sequence)) {
|
||||||
QMessageBox::StandardButton reply;
|
QMessageBox::StandardButton reply;
|
||||||
reply = QMessageBox::question(nullptr,
|
reply =
|
||||||
tr("Auto-Type"),
|
QMessageBox::question(nullptr, tr("Auto-Type"), tr("This Auto-Type command contains arguments which are "
|
||||||
tr("This Auto-Type command contains arguments which are "
|
|
||||||
"repeated very often. Do you really want to proceed?"));
|
"repeated very often. Do you really want to proceed?"));
|
||||||
|
|
||||||
if (reply == QMessageBox::No) {
|
if (reply == QMessageBox::No) {
|
||||||
|
@ -114,7 +114,7 @@ QString BrowserService::getDatabaseRootUuid()
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return QString::fromLatin1(rootGroup->uuid().toRfc4122().toHex());
|
return rootGroup->uuidToHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString BrowserService::getDatabaseRecycleBinUuid()
|
QString BrowserService::getDatabaseRecycleBinUuid()
|
||||||
@ -128,7 +128,7 @@ QString BrowserService::getDatabaseRecycleBinUuid()
|
|||||||
if (!recycleBin) {
|
if (!recycleBin) {
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
return QString::fromLatin1(recycleBin->uuid().toRfc4122().toHex());
|
return recycleBin->uuidToHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry* BrowserService::getConfigEntry(bool create)
|
Entry* BrowserService::getConfigEntry(bool create)
|
||||||
@ -636,7 +636,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
|
|||||||
res["login"] = entry->resolveMultiplePlaceholders(entry->username());
|
res["login"] = entry->resolveMultiplePlaceholders(entry->username());
|
||||||
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
|
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
|
||||||
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
|
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
|
||||||
res["uuid"] = entry->resolveMultiplePlaceholders(QString::fromLatin1(entry->uuid().toRfc4122().toHex()));
|
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
|
||||||
|
|
||||||
if (entry->hasTotp()) {
|
if (entry->hasTotp()) {
|
||||||
res["totp"] = entry->totp();
|
res["totp"] = entry->totp();
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
|
#include "core/Merger.h"
|
||||||
|
|
||||||
Merge::Merge()
|
Merge::Merge()
|
||||||
{
|
{
|
||||||
@ -82,7 +83,8 @@ int Merge::execute(const QStringList& arguments)
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
db1->merge(db2);
|
Merger merger(db2, db1);
|
||||||
|
merger.merge();
|
||||||
|
|
||||||
QString errorMessage = db1->saveToFile(args.at(0));
|
QString errorMessage = db1->saveToFile(args.at(0));
|
||||||
if (!errorMessage.isEmpty()) {
|
if (!errorMessage.isEmpty()) {
|
||||||
|
@ -115,3 +115,21 @@ void AutoTypeAssociations::clear()
|
|||||||
{
|
{
|
||||||
m_associations.clear();
|
m_associations.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AutoTypeAssociations::operator==(const AutoTypeAssociations& other) const
|
||||||
|
{
|
||||||
|
if (m_associations.count() != other.m_associations.count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < m_associations.count(); ++i) {
|
||||||
|
if (m_associations[i] != other.m_associations[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoTypeAssociations::operator!=(const AutoTypeAssociations& other) const
|
||||||
|
{
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
@ -46,6 +46,9 @@ public:
|
|||||||
int associationsSize() const;
|
int associationsSize() const;
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
bool operator==(const AutoTypeAssociations& other) const;
|
||||||
|
bool operator!=(const AutoTypeAssociations& other) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<AutoTypeAssociations::Association> m_associations;
|
QList<AutoTypeAssociations::Association> m_associations;
|
||||||
|
|
||||||
|
109
src/core/Clock.cpp
Normal file
109
src/core/Clock.cpp
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 "Clock.h"
|
||||||
|
|
||||||
|
QSharedPointer<Clock> Clock::m_instance = QSharedPointer<Clock>();
|
||||||
|
|
||||||
|
QDateTime Clock::currentDateTimeUtc()
|
||||||
|
{
|
||||||
|
return instance().currentDateTimeUtcImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::currentDateTime()
|
||||||
|
{
|
||||||
|
return instance().currentDateTimeImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint Clock::currentSecondsSinceEpoch()
|
||||||
|
{
|
||||||
|
return instance().currentDateTimeImpl().toTime_t();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::serialized(const QDateTime& dateTime)
|
||||||
|
{
|
||||||
|
auto time = dateTime.time();
|
||||||
|
if (time.isValid() && time.msec() != 0) {
|
||||||
|
return dateTime.addMSecs(-time.msec());
|
||||||
|
}
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::datetimeUtc(int year, int month, int day, int hour, int min, int second)
|
||||||
|
{
|
||||||
|
return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::datetime(int year, int month, int day, int hour, int min, int second)
|
||||||
|
{
|
||||||
|
return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::LocalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::datetimeUtc(qint64 msecSinceEpoch)
|
||||||
|
{
|
||||||
|
return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::datetime(qint64 msecSinceEpoch)
|
||||||
|
{
|
||||||
|
return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::LocalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::parse(const QString& text, Qt::DateFormat format)
|
||||||
|
{
|
||||||
|
return QDateTime::fromString(text, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::parse(const QString& text, const QString& format)
|
||||||
|
{
|
||||||
|
return QDateTime::fromString(text, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
Clock::~Clock()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Clock::Clock()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::currentDateTimeUtcImpl() const
|
||||||
|
{
|
||||||
|
return QDateTime::currentDateTimeUtc();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Clock::currentDateTimeImpl() const
|
||||||
|
{
|
||||||
|
return QDateTime::currentDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clock::resetInstance()
|
||||||
|
{
|
||||||
|
m_instance.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clock::setInstance(Clock* clock)
|
||||||
|
{
|
||||||
|
m_instance = QSharedPointer<Clock>(clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Clock& Clock::instance()
|
||||||
|
{
|
||||||
|
if (!m_instance) {
|
||||||
|
m_instance = QSharedPointer<Clock>(new Clock());
|
||||||
|
}
|
||||||
|
return *m_instance;
|
||||||
|
}
|
58
src/core/Clock.h
Normal file
58
src/core/Clock.h
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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_CLOCK_H
|
||||||
|
#define KEEPASSXC_CLOCK_H
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
class Clock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QDateTime currentDateTimeUtc();
|
||||||
|
static QDateTime currentDateTime();
|
||||||
|
|
||||||
|
static uint currentSecondsSinceEpoch();
|
||||||
|
|
||||||
|
static QDateTime serialized(const QDateTime& dateTime);
|
||||||
|
|
||||||
|
static QDateTime datetimeUtc(int year, int month, int day, int hour, int min, int second);
|
||||||
|
static QDateTime datetime(int year, int month, int day, int hour, int min, int second);
|
||||||
|
|
||||||
|
static QDateTime datetimeUtc(qint64 msecSinceEpoch);
|
||||||
|
static QDateTime datetime(qint64 msecSinceEpoch);
|
||||||
|
|
||||||
|
static QDateTime parse(const QString& text, Qt::DateFormat format = Qt::TextDate);
|
||||||
|
static QDateTime parse(const QString& text, const QString& format);
|
||||||
|
|
||||||
|
virtual ~Clock();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Clock();
|
||||||
|
virtual QDateTime currentDateTimeUtcImpl() const;
|
||||||
|
virtual QDateTime currentDateTimeImpl() const;
|
||||||
|
|
||||||
|
static void resetInstance();
|
||||||
|
static void setInstance(Clock* clock);
|
||||||
|
static const Clock& instance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QSharedPointer<Clock> m_instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_ENTRY_H
|
38
src/core/Compare.cpp
Normal file
38
src/core/Compare.cpp
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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 "Compare.h"
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
bool operator<(const QColor& lhs, const QColor& rhs)
|
||||||
|
{
|
||||||
|
const QColor adaptedLhs = lhs.toCmyk();
|
||||||
|
const QColor adaptedRhs = rhs.toCmyk();
|
||||||
|
const int iCyan = compare(adaptedLhs.cyanF(), adaptedRhs.cyanF());
|
||||||
|
if (iCyan != 0) {
|
||||||
|
return iCyan;
|
||||||
|
}
|
||||||
|
const int iMagenta = compare(adaptedLhs.magentaF(), adaptedRhs.magentaF());
|
||||||
|
if (iMagenta != 0) {
|
||||||
|
return iMagenta;
|
||||||
|
}
|
||||||
|
const int iYellow = compare(adaptedLhs.yellowF(), adaptedRhs.yellowF());
|
||||||
|
if (iYellow != 0) {
|
||||||
|
return iYellow;
|
||||||
|
}
|
||||||
|
return compare(adaptedLhs.blackF(), adaptedRhs.blackF()) < 0;
|
||||||
|
}
|
90
src/core/Compare.h
Normal file
90
src/core/Compare.h
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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_COMPARE_H
|
||||||
|
#define KEEPASSXC_COMPARE_H
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
|
|
||||||
|
enum CompareItemOption
|
||||||
|
{
|
||||||
|
CompareItemDefault = 0,
|
||||||
|
CompareItemIgnoreMilliseconds = 0x4,
|
||||||
|
CompareItemIgnoreStatistics = 0x8,
|
||||||
|
CompareItemIgnoreDisabled = 0x10,
|
||||||
|
CompareItemIgnoreHistory = 0x20,
|
||||||
|
CompareItemIgnoreLocation = 0x40,
|
||||||
|
};
|
||||||
|
Q_DECLARE_FLAGS(CompareItemOptions, CompareItemOption)
|
||||||
|
Q_DECLARE_OPERATORS_FOR_FLAGS(CompareItemOptions)
|
||||||
|
|
||||||
|
class QColor;
|
||||||
|
/*!
|
||||||
|
* \return true when both color match
|
||||||
|
*
|
||||||
|
* Comparison converts both into the cmyk-model
|
||||||
|
*/
|
||||||
|
bool operator<(const QColor& lhs, const QColor& rhs);
|
||||||
|
|
||||||
|
template <typename Type> inline short compareGeneric(const Type& lhs, const Type& rhs, CompareItemOptions)
|
||||||
|
{
|
||||||
|
if (lhs != rhs) {
|
||||||
|
return lhs < rhs ? -1 : +1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Type>
|
||||||
|
inline short compare(const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault)
|
||||||
|
{
|
||||||
|
return compareGeneric(lhs, rhs, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <> inline short compare(const QDateTime& lhs, const QDateTime& rhs, CompareItemOptions options)
|
||||||
|
{
|
||||||
|
if (!options.testFlag(CompareItemIgnoreMilliseconds)) {
|
||||||
|
return compareGeneric(lhs, rhs, options);
|
||||||
|
}
|
||||||
|
return compareGeneric(Clock::serialized(lhs), Clock::serialized(rhs), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Type>
|
||||||
|
inline short compare(bool enabled, const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault)
|
||||||
|
{
|
||||||
|
if (!enabled) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return compare(lhs, rhs, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Type>
|
||||||
|
inline short compare(bool lhsEnabled,
|
||||||
|
const Type& lhs,
|
||||||
|
bool rhsEnabled,
|
||||||
|
const Type& rhs,
|
||||||
|
CompareItemOptions options = CompareItemDefault)
|
||||||
|
{
|
||||||
|
const short enabled = compareGeneric(lhsEnabled, rhsEnabled, options);
|
||||||
|
if (enabled == 0 && (!options.testFlag(CompareItemIgnoreDisabled) || (lhsEnabled && rhsEnabled))) {
|
||||||
|
return compare(lhs, rhs, options);
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // KEEPASSX_COMPARE_H
|
@ -27,7 +27,9 @@
|
|||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
#include "cli/Utils.h"
|
#include "cli/Utils.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
|
#include "core/Merger.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
#include "crypto/kdf/AesKdf.h"
|
#include "crypto/kdf/AesKdf.h"
|
||||||
#include "format/KeePass2.h"
|
#include "format/KeePass2.h"
|
||||||
@ -40,6 +42,7 @@ QHash<QUuid, Database*> Database::m_uuidMap;
|
|||||||
|
|
||||||
Database::Database()
|
Database::Database()
|
||||||
: m_metadata(new Metadata(this))
|
: m_metadata(new Metadata(this))
|
||||||
|
, m_rootGroup(nullptr)
|
||||||
, m_timer(new QTimer(this))
|
, m_timer(new QTimer(this))
|
||||||
, m_emitModified(false)
|
, m_emitModified(false)
|
||||||
, m_uuid(QUuid::createUuid())
|
, m_uuid(QUuid::createUuid())
|
||||||
@ -216,6 +219,39 @@ QList<DeletedObject> Database::deletedObjects()
|
|||||||
return m_deletedObjects;
|
return m_deletedObjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QList<DeletedObject>& Database::deletedObjects() const
|
||||||
|
{
|
||||||
|
return m_deletedObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Database::containsDeletedObject(const QUuid& uuid) const
|
||||||
|
{
|
||||||
|
for (const DeletedObject& currentObject : m_deletedObjects) {
|
||||||
|
if (currentObject.uuid == uuid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Database::containsDeletedObject(const DeletedObject& object) const
|
||||||
|
{
|
||||||
|
for (const DeletedObject& currentObject : m_deletedObjects) {
|
||||||
|
if (currentObject.uuid == object.uuid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
|
||||||
|
{
|
||||||
|
if (m_deletedObjects == delObjs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_deletedObjects = delObjs;
|
||||||
|
}
|
||||||
|
|
||||||
void Database::addDeletedObject(const DeletedObject& delObj)
|
void Database::addDeletedObject(const DeletedObject& delObj)
|
||||||
{
|
{
|
||||||
Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
|
Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
|
||||||
@ -225,7 +261,7 @@ void Database::addDeletedObject(const DeletedObject& delObj)
|
|||||||
void Database::addDeletedObject(const QUuid& uuid)
|
void Database::addDeletedObject(const QUuid& uuid)
|
||||||
{
|
{
|
||||||
DeletedObject delObj;
|
DeletedObject delObj;
|
||||||
delObj.deletionTime = QDateTime::currentDateTimeUtc();
|
delObj.deletionTime = Clock::currentDateTimeUtc();
|
||||||
delObj.uuid = uuid;
|
delObj.uuid = uuid;
|
||||||
|
|
||||||
addDeletedObject(delObj);
|
addDeletedObject(delObj);
|
||||||
@ -303,7 +339,7 @@ bool Database::setKey(QSharedPointer<const CompositeKey> key, bool updateChanged
|
|||||||
m_data.transformedMasterKey = transformedMasterKey;
|
m_data.transformedMasterKey = transformedMasterKey;
|
||||||
m_data.hasKey = true;
|
m_data.hasKey = true;
|
||||||
if (updateChangedTime) {
|
if (updateChangedTime) {
|
||||||
m_metadata->setMasterKeyChanged(QDateTime::currentDateTimeUtc());
|
m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldTransformedMasterKey != m_data.transformedMasterKey) {
|
if (oldTransformedMasterKey != m_data.transformedMasterKey) {
|
||||||
@ -401,21 +437,6 @@ void Database::emptyRecycleBin()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Database::merge(const Database* other)
|
|
||||||
{
|
|
||||||
m_rootGroup->merge(other->rootGroup());
|
|
||||||
|
|
||||||
for (const QUuid& customIconId : other->metadata()->customIcons().keys()) {
|
|
||||||
QImage customIcon = other->metadata()->customIcon(customIconId);
|
|
||||||
if (!this->metadata()->containsCustomIcon(customIconId)) {
|
|
||||||
qDebug() << QString("Adding custom icon %1 to database.").arg(customIconId.toString());
|
|
||||||
this->metadata()->addCustomIcon(customIconId, customIcon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit modified();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Database::setEmitModified(bool value)
|
void Database::setEmitModified(bool value)
|
||||||
{
|
{
|
||||||
if (m_emitModified && !value) {
|
if (m_emitModified && !value) {
|
||||||
@ -425,6 +446,11 @@ void Database::setEmitModified(bool value)
|
|||||||
m_emitModified = value;
|
m_emitModified = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Database::markAsModified()
|
||||||
|
{
|
||||||
|
emit modified();
|
||||||
|
}
|
||||||
|
|
||||||
const QUuid& Database::uuid()
|
const QUuid& Database::uuid()
|
||||||
{
|
{
|
||||||
return m_uuid;
|
return m_uuid;
|
||||||
@ -467,7 +493,6 @@ Database* Database::openDatabaseFile(const QString& fileName, QSharedPointer<con
|
|||||||
|
|
||||||
KeePass2Reader reader;
|
KeePass2Reader reader;
|
||||||
Database* db = reader.readDatabase(&dbFile, key);
|
Database* db = reader.readDatabase(&dbFile, key);
|
||||||
|
|
||||||
if (reader.hasError()) {
|
if (reader.hasError()) {
|
||||||
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@ -600,7 +625,6 @@ QString Database::writeDatabase(QIODevice* device)
|
|||||||
* @param filePath Path to the file to backup
|
* @param filePath Path to the file to backup
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
||||||
bool Database::backupDatabase(QString filePath)
|
bool Database::backupDatabase(QString filePath)
|
||||||
{
|
{
|
||||||
QString backupFilePath = filePath;
|
QString backupFilePath = filePath;
|
||||||
|
@ -37,6 +37,10 @@ struct DeletedObject
|
|||||||
{
|
{
|
||||||
QUuid uuid;
|
QUuid uuid;
|
||||||
QDateTime deletionTime;
|
QDateTime deletionTime;
|
||||||
|
bool operator==(const DeletedObject& other) const
|
||||||
|
{
|
||||||
|
return uuid == other.uuid && deletionTime == other.deletionTime;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_TYPEINFO(DeletedObject, Q_MOVABLE_TYPE);
|
Q_DECLARE_TYPEINFO(DeletedObject, Q_MOVABLE_TYPE);
|
||||||
@ -88,8 +92,12 @@ public:
|
|||||||
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
|
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
|
||||||
Group* resolveGroup(const QUuid& uuid);
|
Group* resolveGroup(const QUuid& uuid);
|
||||||
QList<DeletedObject> deletedObjects();
|
QList<DeletedObject> deletedObjects();
|
||||||
|
const QList<DeletedObject>& deletedObjects() const;
|
||||||
void addDeletedObject(const DeletedObject& delObj);
|
void addDeletedObject(const DeletedObject& delObj);
|
||||||
void addDeletedObject(const QUuid& uuid);
|
void addDeletedObject(const QUuid& uuid);
|
||||||
|
bool containsDeletedObject(const QUuid& uuid) const;
|
||||||
|
bool containsDeletedObject(const DeletedObject& uuid) const;
|
||||||
|
void setDeletedObjects(const QList<DeletedObject>& delObjs);
|
||||||
|
|
||||||
const QUuid& cipher() const;
|
const QUuid& cipher() const;
|
||||||
Database::CompressionAlgorithm compressionAlgo() const;
|
Database::CompressionAlgorithm compressionAlgo() const;
|
||||||
@ -112,7 +120,7 @@ public:
|
|||||||
void recycleGroup(Group* group);
|
void recycleGroup(Group* group);
|
||||||
void emptyRecycleBin();
|
void emptyRecycleBin();
|
||||||
void setEmitModified(bool value);
|
void setEmitModified(bool value);
|
||||||
void merge(const Database* other);
|
void markAsModified();
|
||||||
QString saveToFile(QString filePath, bool atomic = true, bool backup = false);
|
QString saveToFile(QString filePath, bool atomic = true, bool backup = false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "config-keepassx.h"
|
#include "config-keepassx.h"
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/DatabaseIcons.h"
|
#include "core/DatabaseIcons.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
@ -60,6 +61,7 @@ Entry::Entry()
|
|||||||
|
|
||||||
Entry::~Entry()
|
Entry::~Entry()
|
||||||
{
|
{
|
||||||
|
setUpdateTimeinfo(false);
|
||||||
if (m_group) {
|
if (m_group) {
|
||||||
m_group->removeEntry(this);
|
m_group->removeEntry(this);
|
||||||
|
|
||||||
@ -77,19 +79,23 @@ template <class T> inline bool Entry::set(T& property, const T& value)
|
|||||||
property = value;
|
property = value;
|
||||||
emit modified();
|
emit modified();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entry::updateTimeinfo()
|
void Entry::updateTimeinfo()
|
||||||
{
|
{
|
||||||
if (m_updateTimeinfo) {
|
if (m_updateTimeinfo) {
|
||||||
m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc());
|
||||||
m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Entry::canUpdateTimeinfo() const
|
||||||
|
{
|
||||||
|
return m_updateTimeinfo;
|
||||||
|
}
|
||||||
|
|
||||||
void Entry::setUpdateTimeinfo(bool value)
|
void Entry::setUpdateTimeinfo(bool value)
|
||||||
{
|
{
|
||||||
m_updateTimeinfo = value;
|
m_updateTimeinfo = value;
|
||||||
@ -123,6 +129,11 @@ const QUuid& Entry::uuid() const
|
|||||||
return m_uuid;
|
return m_uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString Entry::uuidToHex() const
|
||||||
|
{
|
||||||
|
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
|
||||||
|
}
|
||||||
|
|
||||||
QImage Entry::icon() const
|
QImage Entry::icon() const
|
||||||
{
|
{
|
||||||
if (m_data.customIcon.isNull()) {
|
if (m_data.customIcon.isNull()) {
|
||||||
@ -142,27 +153,23 @@ QPixmap Entry::iconPixmap() const
|
|||||||
{
|
{
|
||||||
if (m_data.customIcon.isNull()) {
|
if (m_data.customIcon.isNull()) {
|
||||||
return databaseIcons()->iconPixmap(m_data.iconNumber);
|
return databaseIcons()->iconPixmap(m_data.iconNumber);
|
||||||
} else {
|
}
|
||||||
Q_ASSERT(database());
|
|
||||||
|
|
||||||
|
Q_ASSERT(database());
|
||||||
if (database()) {
|
if (database()) {
|
||||||
return database()->metadata()->customIconPixmap(m_data.customIcon);
|
return database()->metadata()->customIconPixmap(m_data.customIcon);
|
||||||
} else {
|
}
|
||||||
return QPixmap();
|
return QPixmap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QPixmap Entry::iconScaledPixmap() const
|
QPixmap Entry::iconScaledPixmap() const
|
||||||
{
|
{
|
||||||
if (m_data.customIcon.isNull()) {
|
if (m_data.customIcon.isNull()) {
|
||||||
// built-in icons are 16x16 so don't need to be scaled
|
// built-in icons are 16x16 so don't need to be scaled
|
||||||
return databaseIcons()->iconPixmap(m_data.iconNumber);
|
return databaseIcons()->iconPixmap(m_data.iconNumber);
|
||||||
} else {
|
|
||||||
Q_ASSERT(database());
|
|
||||||
|
|
||||||
return database()->metadata()->customIconScaledPixmap(m_data.customIcon);
|
|
||||||
}
|
}
|
||||||
|
Q_ASSERT(database());
|
||||||
|
return database()->metadata()->customIconScaledPixmap(m_data.customIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Entry::iconNumber() const
|
int Entry::iconNumber() const
|
||||||
@ -195,7 +202,7 @@ QString Entry::tags() const
|
|||||||
return m_data.tags;
|
return m_data.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeInfo Entry::timeInfo() const
|
const TimeInfo& Entry::timeInfo() const
|
||||||
{
|
{
|
||||||
return m_data.timeInfo;
|
return m_data.timeInfo;
|
||||||
}
|
}
|
||||||
@ -300,7 +307,7 @@ QString Entry::notes() const
|
|||||||
|
|
||||||
bool Entry::isExpired() const
|
bool Entry::isExpired() const
|
||||||
{
|
{
|
||||||
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc();
|
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Entry::hasReferences() const
|
bool Entry::hasReferences() const
|
||||||
@ -532,7 +539,7 @@ void Entry::removeHistoryItems(const QList<Entry*>& historyEntries)
|
|||||||
|
|
||||||
for (Entry* entry : historyEntries) {
|
for (Entry* entry : historyEntries) {
|
||||||
Q_ASSERT(!entry->parent());
|
Q_ASSERT(!entry->parent());
|
||||||
Q_ASSERT(entry->uuid() == uuid());
|
Q_ASSERT(entry->uuid().isNull() || entry->uuid() == uuid());
|
||||||
Q_ASSERT(m_history.contains(entry));
|
Q_ASSERT(m_history.contains(entry));
|
||||||
|
|
||||||
m_history.removeOne(entry);
|
m_history.removeOne(entry);
|
||||||
@ -597,6 +604,42 @@ void Entry::truncateHistory()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Entry::equals(const Entry* other, CompareItemOptions options) const
|
||||||
|
{
|
||||||
|
if (!other) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_uuid != other->uuid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!m_data.equals(other->m_data, options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (*m_customData != *other->m_customData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (*m_attributes != *other->m_attributes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (*m_attachments != *other->m_attachments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (*m_autoTypeAssociations != *other->m_autoTypeAssociations) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!options.testFlag(CompareItemIgnoreHistory)) {
|
||||||
|
if (m_history.count() != other->m_history.count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < m_history.count(); ++i) {
|
||||||
|
if (!m_history[i]->equals(other->m_history[i], options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Entry* Entry::clone(CloneFlags flags) const
|
Entry* Entry::clone(CloneFlags flags) const
|
||||||
{
|
{
|
||||||
Entry* entry = new Entry();
|
Entry* entry = new Entry();
|
||||||
@ -613,12 +656,12 @@ Entry* Entry::clone(CloneFlags flags) const
|
|||||||
|
|
||||||
if (flags & CloneUserAsRef) {
|
if (flags & CloneUserAsRef) {
|
||||||
// Build the username reference
|
// Build the username reference
|
||||||
QString username = "{REF:U@I:" + m_uuid.toRfc4122().toHex() + "}";
|
QString username = "{REF:U@I:" + uuidToHex() + "}";
|
||||||
entry->m_attributes->set(EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey));
|
entry->m_attributes->set(EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags & ClonePassAsRef) {
|
if (flags & ClonePassAsRef) {
|
||||||
QString password = "{REF:P@I:" + m_uuid.toRfc4122().toHex() + "}";
|
QString password = "{REF:P@I:" + uuidToHex() + "}";
|
||||||
entry->m_attributes->set(EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey));
|
entry->m_attributes->set(EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -635,7 +678,7 @@ Entry* Entry::clone(CloneFlags flags) const
|
|||||||
entry->setUpdateTimeinfo(true);
|
entry->setUpdateTimeinfo(true);
|
||||||
|
|
||||||
if (flags & CloneResetTimeInfo) {
|
if (flags & CloneResetTimeInfo) {
|
||||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
QDateTime now = Clock::currentDateTimeUtc();
|
||||||
entry->m_data.timeInfo.setCreationTime(now);
|
entry->m_data.timeInfo.setCreationTime(now);
|
||||||
entry->m_data.timeInfo.setLastModificationTime(now);
|
entry->m_data.timeInfo.setLastModificationTime(now);
|
||||||
entry->m_data.timeInfo.setLastAccessTime(now);
|
entry->m_data.timeInfo.setLastAccessTime(now);
|
||||||
@ -835,7 +878,7 @@ QString Entry::referenceFieldValue(EntryReferenceType referenceType) const
|
|||||||
case EntryReferenceType::Notes:
|
case EntryReferenceType::Notes:
|
||||||
return notes();
|
return notes();
|
||||||
case EntryReferenceType::QUuid:
|
case EntryReferenceType::QUuid:
|
||||||
return uuid().toRfc4122().toHex();
|
return uuidToHex();
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -880,7 +923,7 @@ void Entry::setGroup(Group* group)
|
|||||||
QObject::setParent(group);
|
QObject::setParent(group);
|
||||||
|
|
||||||
if (m_updateTimeinfo) {
|
if (m_updateTimeinfo) {
|
||||||
m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -893,9 +936,16 @@ const Database* Entry::database() const
|
|||||||
{
|
{
|
||||||
if (m_group) {
|
if (m_group) {
|
||||||
return m_group->database();
|
return m_group->database();
|
||||||
} else {
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Database* Entry::database()
|
||||||
|
{
|
||||||
|
if (m_group) {
|
||||||
|
return m_group->database();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Entry::maskPasswordPlaceholders(const QString& str) const
|
QString Entry::maskPasswordPlaceholders(const QString& str) const
|
||||||
@ -955,9 +1005,11 @@ Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const
|
|||||||
{
|
{
|
||||||
if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) {
|
if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) {
|
||||||
return PlaceholderType::NotPlaceholder;
|
return PlaceholderType::NotPlaceholder;
|
||||||
} else if (placeholder.startsWith(QLatin1Literal("{S:"))) {
|
}
|
||||||
|
if (placeholder.startsWith(QLatin1Literal("{S:"))) {
|
||||||
return PlaceholderType::CustomAttribute;
|
return PlaceholderType::CustomAttribute;
|
||||||
} else if (placeholder.startsWith(QLatin1Literal("{REF:"))) {
|
}
|
||||||
|
if (placeholder.startsWith(QLatin1Literal("{REF:"))) {
|
||||||
return PlaceholderType::Reference;
|
return PlaceholderType::Reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1020,3 +1072,64 @@ QString Entry::resolveUrl(const QString& url) const
|
|||||||
// No valid http URL's found
|
// No valid http URL's found
|
||||||
return QString("");
|
return QString("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EntryData::operator==(const EntryData& other) const
|
||||||
|
{
|
||||||
|
return equals(other, CompareItemDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EntryData::operator!=(const EntryData& other) const
|
||||||
|
{
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EntryData::equals(const EntryData& other, CompareItemOptions options) const
|
||||||
|
{
|
||||||
|
if (::compare(iconNumber, other.iconNumber, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(customIcon, other.customIcon, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(foregroundColor, other.foregroundColor, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(backgroundColor, other.backgroundColor, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(overrideUrl, other.overrideUrl, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(tags, other.tags, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(autoTypeObfuscation, other.autoTypeObfuscation, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!timeInfo.equals(other.timeInfo, options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!totpSettings.isNull() && !other.totpSettings.isNull()) {
|
||||||
|
// Both have TOTP settings, compare them
|
||||||
|
if (::compare(totpSettings->key, other.totpSettings->key, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(totpSettings->digits, other.totpSettings->digits, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(totpSettings->step, other.totpSettings->step, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (totpSettings.isNull() != other.totpSettings.isNull()) {
|
||||||
|
// The existance of TOTP has changed between these entries
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ -65,6 +65,10 @@ struct EntryData
|
|||||||
QString defaultAutoTypeSequence;
|
QString defaultAutoTypeSequence;
|
||||||
TimeInfo timeInfo;
|
TimeInfo timeInfo;
|
||||||
QSharedPointer<Totp::Settings> totpSettings;
|
QSharedPointer<Totp::Settings> totpSettings;
|
||||||
|
|
||||||
|
bool operator==(const EntryData& other) const;
|
||||||
|
bool operator!=(const EntryData& other) const;
|
||||||
|
bool equals(const EntryData& other, CompareItemOptions options) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Entry : public QObject
|
class Entry : public QObject
|
||||||
@ -75,6 +79,7 @@ public:
|
|||||||
Entry();
|
Entry();
|
||||||
~Entry();
|
~Entry();
|
||||||
const QUuid& uuid() const;
|
const QUuid& uuid() const;
|
||||||
|
const QString uuidToHex() const;
|
||||||
QImage icon() const;
|
QImage icon() const;
|
||||||
QPixmap iconPixmap() const;
|
QPixmap iconPixmap() const;
|
||||||
QPixmap iconScaledPixmap() const;
|
QPixmap iconScaledPixmap() const;
|
||||||
@ -84,7 +89,7 @@ public:
|
|||||||
QColor backgroundColor() const;
|
QColor backgroundColor() const;
|
||||||
QString overrideUrl() const;
|
QString overrideUrl() const;
|
||||||
QString tags() const;
|
QString tags() const;
|
||||||
TimeInfo timeInfo() const;
|
const TimeInfo& timeInfo() const;
|
||||||
bool autoTypeEnabled() const;
|
bool autoTypeEnabled() const;
|
||||||
int autoTypeObfuscation() const;
|
int autoTypeObfuscation() const;
|
||||||
QString defaultAutoTypeSequence() const;
|
QString defaultAutoTypeSequence() const;
|
||||||
@ -143,6 +148,8 @@ public:
|
|||||||
void removeHistoryItems(const QList<Entry*>& historyEntries);
|
void removeHistoryItems(const QList<Entry*>& historyEntries);
|
||||||
void truncateHistory();
|
void truncateHistory();
|
||||||
|
|
||||||
|
bool equals(const Entry* other, CompareItemOptions options = CompareItemDefault) const;
|
||||||
|
|
||||||
enum CloneFlag
|
enum CloneFlag
|
||||||
{
|
{
|
||||||
CloneNoFlags = 0,
|
CloneNoFlags = 0,
|
||||||
@ -204,7 +211,10 @@ public:
|
|||||||
Group* group();
|
Group* group();
|
||||||
const Group* group() const;
|
const Group* group() const;
|
||||||
void setGroup(Group* group);
|
void setGroup(Group* group);
|
||||||
|
const Database* database() const;
|
||||||
|
Database* database();
|
||||||
|
|
||||||
|
bool canUpdateTimeinfo() const;
|
||||||
void setUpdateTimeinfo(bool value);
|
void setUpdateTimeinfo(bool value);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@ -229,7 +239,6 @@ private:
|
|||||||
|
|
||||||
static EntryReferenceType referenceType(const QString& referenceStr);
|
static EntryReferenceType referenceType(const QString& referenceStr);
|
||||||
|
|
||||||
const Database* database() const;
|
|
||||||
template <class T> bool set(T& property, const T& value);
|
template <class T> bool set(T& property, const T& value);
|
||||||
|
|
||||||
QUuid m_uuid;
|
QUuid m_uuid;
|
||||||
@ -238,8 +247,8 @@ private:
|
|||||||
QPointer<EntryAttachments> m_attachments;
|
QPointer<EntryAttachments> m_attachments;
|
||||||
QPointer<AutoTypeAssociations> m_autoTypeAssociations;
|
QPointer<AutoTypeAssociations> m_autoTypeAssociations;
|
||||||
QPointer<CustomData> m_customData;
|
QPointer<CustomData> m_customData;
|
||||||
|
QList<Entry*> m_history; // Items sorted from oldest to newest
|
||||||
|
|
||||||
QList<Entry*> m_history;
|
|
||||||
Entry* m_tmpHistoryItem;
|
Entry* m_tmpHistoryItem;
|
||||||
bool m_modifiedSinceBegin;
|
bool m_modifiedSinceBegin;
|
||||||
QPointer<Group> m_group;
|
QPointer<Group> m_group;
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include "Group.h"
|
#include "Group.h"
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "core/DatabaseIcons.h"
|
#include "core/DatabaseIcons.h"
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
@ -40,7 +41,7 @@ Group::Group()
|
|||||||
m_data.isExpanded = true;
|
m_data.isExpanded = true;
|
||||||
m_data.autoTypeEnabled = Inherit;
|
m_data.autoTypeEnabled = Inherit;
|
||||||
m_data.searchingEnabled = Inherit;
|
m_data.searchingEnabled = Inherit;
|
||||||
m_data.mergeMode = ModeInherit;
|
m_data.mergeMode = Default;
|
||||||
|
|
||||||
connect(m_customData, SIGNAL(modified()), this, SIGNAL(modified()));
|
connect(m_customData, SIGNAL(modified()), this, SIGNAL(modified()));
|
||||||
connect(this, SIGNAL(modified()), SLOT(updateTimeinfo()));
|
connect(this, SIGNAL(modified()), SLOT(updateTimeinfo()));
|
||||||
@ -48,6 +49,7 @@ Group::Group()
|
|||||||
|
|
||||||
Group::~Group()
|
Group::~Group()
|
||||||
{
|
{
|
||||||
|
setUpdateTimeinfo(false);
|
||||||
// Destroy entries and children manually so DeletedObjects can be added
|
// Destroy entries and children manually so DeletedObjects can be added
|
||||||
// to database.
|
// to database.
|
||||||
const QList<Entry*> entries = m_entries;
|
const QList<Entry*> entries = m_entries;
|
||||||
@ -62,7 +64,7 @@ Group::~Group()
|
|||||||
|
|
||||||
if (m_db && m_parent) {
|
if (m_db && m_parent) {
|
||||||
DeletedObject delGroup;
|
DeletedObject delGroup;
|
||||||
delGroup.deletionTime = QDateTime::currentDateTimeUtc();
|
delGroup.deletionTime = Clock::currentDateTimeUtc();
|
||||||
delGroup.uuid = m_uuid;
|
delGroup.uuid = m_uuid;
|
||||||
m_db->addDeletedObject(delGroup);
|
m_db->addDeletedObject(delGroup);
|
||||||
}
|
}
|
||||||
@ -92,11 +94,16 @@ template <class P, class V> inline bool Group::set(P& property, const V& value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Group::canUpdateTimeinfo() const
|
||||||
|
{
|
||||||
|
return m_updateTimeinfo;
|
||||||
|
}
|
||||||
|
|
||||||
void Group::updateTimeinfo()
|
void Group::updateTimeinfo()
|
||||||
{
|
{
|
||||||
if (m_updateTimeinfo) {
|
if (m_updateTimeinfo) {
|
||||||
m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc());
|
||||||
m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +117,11 @@ const QUuid& Group::uuid() const
|
|||||||
return m_uuid;
|
return m_uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString Group::uuidToHex() const
|
||||||
|
{
|
||||||
|
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
|
||||||
|
}
|
||||||
|
|
||||||
QString Group::name() const
|
QString Group::name() const
|
||||||
{
|
{
|
||||||
return m_data.name;
|
return m_data.name;
|
||||||
@ -176,7 +188,7 @@ const QUuid& Group::iconUuid() const
|
|||||||
return m_data.customIcon;
|
return m_data.customIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeInfo Group::timeInfo() const
|
const TimeInfo& Group::timeInfo() const
|
||||||
{
|
{
|
||||||
return m_data.timeInfo;
|
return m_data.timeInfo;
|
||||||
}
|
}
|
||||||
@ -228,16 +240,14 @@ Group::TriState Group::searchingEnabled() const
|
|||||||
|
|
||||||
Group::MergeMode Group::mergeMode() const
|
Group::MergeMode Group::mergeMode() const
|
||||||
{
|
{
|
||||||
if (m_data.mergeMode == Group::MergeMode::ModeInherit) {
|
if (m_data.mergeMode == Group::MergeMode::Default) {
|
||||||
if (m_parent) {
|
if (m_parent) {
|
||||||
return m_parent->mergeMode();
|
return m_parent->mergeMode();
|
||||||
} else {
|
}
|
||||||
return Group::MergeMode::KeepNewer; // fallback
|
return Group::MergeMode::KeepNewer; // fallback
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return m_data.mergeMode;
|
return m_data.mergeMode;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Entry* Group::lastTopVisibleEntry() const
|
Entry* Group::lastTopVisibleEntry() const
|
||||||
{
|
{
|
||||||
@ -246,7 +256,7 @@ Entry* Group::lastTopVisibleEntry() const
|
|||||||
|
|
||||||
bool Group::isExpired() const
|
bool Group::isExpired() const
|
||||||
{
|
{
|
||||||
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc();
|
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomData* Group::customData()
|
CustomData* Group::customData()
|
||||||
@ -259,6 +269,39 @@ const CustomData* Group::customData() const
|
|||||||
return m_customData;
|
return m_customData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Group::equals(const Group* other, CompareItemOptions options) const
|
||||||
|
{
|
||||||
|
if (!other) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_uuid != other->m_uuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!m_data.equals(other->m_data, options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_customData != other->m_customData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_children.count() != other->m_children.count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_entries.count() != other->m_entries.count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < m_children.count(); ++i) {
|
||||||
|
if (m_children[i]->uuid() != other->m_children[i]->uuid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < m_entries.count(); ++i) {
|
||||||
|
if (m_entries[i]->uuid() != other->m_entries[i]->uuid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void Group::setUuid(const QUuid& uuid)
|
void Group::setUuid(const QUuid& uuid)
|
||||||
{
|
{
|
||||||
set(m_uuid, uuid);
|
set(m_uuid, uuid);
|
||||||
@ -418,7 +461,7 @@ void Group::setParent(Group* parent, int index)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_updateTimeinfo) {
|
if (m_updateTimeinfo) {
|
||||||
m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc());
|
m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit modified();
|
emit modified();
|
||||||
@ -536,7 +579,7 @@ Entry* Group::findEntry(QString entryId)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry* Group::findEntryByUuid(const QUuid& uuid)
|
Entry* Group::findEntryByUuid(const QUuid& uuid) const
|
||||||
{
|
{
|
||||||
Q_ASSERT(!uuid.isNull());
|
Q_ASSERT(!uuid.isNull());
|
||||||
for (Entry* entry : entriesRecursive(false)) {
|
for (Entry* entry : entriesRecursive(false)) {
|
||||||
@ -683,61 +726,7 @@ QSet<QUuid> Group::customIconsRecursive() const
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Group::merge(const Group* other)
|
Group* Group::findGroupByUuid(const QUuid& uuid)
|
||||||
{
|
|
||||||
|
|
||||||
Group* rootGroup = this;
|
|
||||||
while (rootGroup->parentGroup()) {
|
|
||||||
rootGroup = rootGroup->parentGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge entries
|
|
||||||
const QList<Entry*> dbEntries = other->entries();
|
|
||||||
for (Entry* entry : dbEntries) {
|
|
||||||
|
|
||||||
Entry* existingEntry = rootGroup->findEntryByUuid(entry->uuid());
|
|
||||||
|
|
||||||
if (!existingEntry) {
|
|
||||||
// This entry does not exist at all. Create it.
|
|
||||||
qDebug("New entry %s detected. Creating it.", qPrintable(entry->title()));
|
|
||||||
entry->clone(Entry::CloneIncludeHistory)->setGroup(this);
|
|
||||||
} else {
|
|
||||||
// Entry is already present in the database. Update it.
|
|
||||||
bool locationChanged = existingEntry->timeInfo().locationChanged() < entry->timeInfo().locationChanged();
|
|
||||||
if (locationChanged && existingEntry->group() != this) {
|
|
||||||
existingEntry->setGroup(this);
|
|
||||||
qDebug("Location changed for entry %s. Updating it", qPrintable(existingEntry->title()));
|
|
||||||
}
|
|
||||||
resolveEntryConflict(existingEntry, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge groups recursively
|
|
||||||
const QList<Group*> dbChildren = other->children();
|
|
||||||
for (Group* group : dbChildren) {
|
|
||||||
|
|
||||||
Group* existingGroup = rootGroup->findChildByUuid(group->uuid());
|
|
||||||
|
|
||||||
if (!existingGroup) {
|
|
||||||
qDebug("New group %s detected. Creating it.", qPrintable(group->name()));
|
|
||||||
Group* newGroup = group->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
|
|
||||||
newGroup->setParent(this);
|
|
||||||
newGroup->merge(group);
|
|
||||||
} else {
|
|
||||||
bool locationChanged = existingGroup->timeInfo().locationChanged() < group->timeInfo().locationChanged();
|
|
||||||
if (locationChanged && existingGroup->parent() != this) {
|
|
||||||
existingGroup->setParent(this);
|
|
||||||
qDebug("Location changed for group %s. Updating it", qPrintable(existingGroup->name()));
|
|
||||||
}
|
|
||||||
resolveGroupConflict(existingGroup, group);
|
|
||||||
existingGroup->merge(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit modified();
|
|
||||||
}
|
|
||||||
|
|
||||||
Group* Group::findChildByUuid(const QUuid& uuid)
|
|
||||||
{
|
{
|
||||||
Q_ASSERT(!uuid.isNull());
|
Q_ASSERT(!uuid.isNull());
|
||||||
for (Group* group : groupsRecursive(true)) {
|
for (Group* group : groupsRecursive(true)) {
|
||||||
@ -792,7 +781,7 @@ Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags)
|
|||||||
clonedGroup->setUpdateTimeinfo(true);
|
clonedGroup->setUpdateTimeinfo(true);
|
||||||
if (groupFlags & Group::CloneResetTimeInfo) {
|
if (groupFlags & Group::CloneResetTimeInfo) {
|
||||||
|
|
||||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
QDateTime now = Clock::currentDateTimeUtc();
|
||||||
clonedGroup->m_data.timeInfo.setCreationTime(now);
|
clonedGroup->m_data.timeInfo.setCreationTime(now);
|
||||||
clonedGroup->m_data.timeInfo.setLastModificationTime(now);
|
clonedGroup->m_data.timeInfo.setLastModificationTime(now);
|
||||||
clonedGroup->m_data.timeInfo.setLastAccessTime(now);
|
clonedGroup->m_data.timeInfo.setLastAccessTime(now);
|
||||||
@ -828,7 +817,9 @@ void Group::addEntry(Entry* entry)
|
|||||||
|
|
||||||
void Group::removeEntry(Entry* entry)
|
void Group::removeEntry(Entry* entry)
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_entries.contains(entry));
|
Q_ASSERT_X(m_entries.contains(entry),
|
||||||
|
Q_FUNC_INFO,
|
||||||
|
QString("Group %1 does not contain %2").arg(this->name()).arg(entry->title()).toLatin1());
|
||||||
|
|
||||||
emit entryAboutToRemove(entry);
|
emit entryAboutToRemove(entry);
|
||||||
|
|
||||||
@ -905,12 +896,6 @@ void Group::recCreateDelObjects()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Group::markOlderEntry(Entry* entry)
|
|
||||||
{
|
|
||||||
entry->attributes()->set(
|
|
||||||
"merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Group::resolveSearchingEnabled() const
|
bool Group::resolveSearchingEnabled() const
|
||||||
{
|
{
|
||||||
switch (m_data.searchingEnabled) {
|
switch (m_data.searchingEnabled) {
|
||||||
@ -949,63 +934,6 @@ bool Group::resolveAutoTypeEnabled() const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Group::resolveEntryConflict(Entry* existingEntry, Entry* otherEntry)
|
|
||||||
{
|
|
||||||
const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime();
|
|
||||||
const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime();
|
|
||||||
|
|
||||||
Entry* clonedEntry;
|
|
||||||
|
|
||||||
switch (mergeMode()) {
|
|
||||||
case KeepBoth:
|
|
||||||
// if one entry is newer, create a clone and add it to the group
|
|
||||||
if (timeExisting > timeOther) {
|
|
||||||
clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
|
|
||||||
clonedEntry->setGroup(this);
|
|
||||||
markOlderEntry(clonedEntry);
|
|
||||||
} else if (timeExisting < timeOther) {
|
|
||||||
clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
|
|
||||||
clonedEntry->setGroup(this);
|
|
||||||
markOlderEntry(existingEntry);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case KeepNewer:
|
|
||||||
if (timeExisting < timeOther) {
|
|
||||||
qDebug("Updating entry %s.", qPrintable(existingEntry->title()));
|
|
||||||
// only if other entry is newer, replace existing one
|
|
||||||
Group* currentGroup = existingEntry->group();
|
|
||||||
currentGroup->removeEntry(existingEntry);
|
|
||||||
otherEntry->clone(Entry::CloneIncludeHistory)->setGroup(currentGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case KeepExisting:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Group::resolveGroupConflict(Group* existingGroup, Group* otherGroup)
|
|
||||||
{
|
|
||||||
const QDateTime timeExisting = existingGroup->timeInfo().lastModificationTime();
|
|
||||||
const QDateTime timeOther = otherGroup->timeInfo().lastModificationTime();
|
|
||||||
|
|
||||||
// only if the other group is newer, update the existing one.
|
|
||||||
if (timeExisting < timeOther) {
|
|
||||||
qDebug("Updating group %s.", qPrintable(existingGroup->name()));
|
|
||||||
existingGroup->setName(otherGroup->name());
|
|
||||||
existingGroup->setNotes(otherGroup->notes());
|
|
||||||
if (otherGroup->iconNumber() == 0) {
|
|
||||||
existingGroup->setIcon(otherGroup->iconUuid());
|
|
||||||
} else {
|
|
||||||
existingGroup->setIcon(otherGroup->iconNumber());
|
|
||||||
}
|
|
||||||
existingGroup->setExpiryTime(otherGroup->timeInfo().expiryTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList Group::locate(QString locateTerm, QString currentPath)
|
QStringList Group::locate(QString locateTerm, QString currentPath)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!locateTerm.isNull());
|
Q_ASSERT(!locateTerm.isNull());
|
||||||
@ -1054,3 +982,49 @@ Entry* Group::addEntryWithPath(QString entryPath)
|
|||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Group::GroupData::operator==(const Group::GroupData& other) const
|
||||||
|
{
|
||||||
|
return equals(other, CompareItemDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Group::GroupData::operator!=(const Group::GroupData& other) const
|
||||||
|
{
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions options) const
|
||||||
|
{
|
||||||
|
if (::compare(name, other.name, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(notes, other.notes, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(iconNumber, other.iconNumber) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(customIcon, other.customIcon) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (timeInfo.equals(other.timeInfo, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO HNH: Some properties are configurable - should they be ignored?
|
||||||
|
if (::compare(isExpanded, other.isExpanded, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(searchingEnabled, other.searchingEnabled, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(mergeMode, other.mergeMode, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ -42,10 +42,12 @@ public:
|
|||||||
};
|
};
|
||||||
enum MergeMode
|
enum MergeMode
|
||||||
{
|
{
|
||||||
ModeInherit,
|
Default, // Determine merge strategy from parent or fallback (Synchronize)
|
||||||
KeepBoth,
|
Duplicate, // lossy strategy regarding deletions, duplicate older changes in a new entry
|
||||||
KeepNewer,
|
KeepLocal, // merge history forcing local as top regardless of age
|
||||||
KeepExisting
|
KeepRemote, // merge history forcing remote as top regardless of age
|
||||||
|
KeepNewer, // merge history
|
||||||
|
Synchronize, // merge history keeping most recent as top entry and appling deletions
|
||||||
};
|
};
|
||||||
|
|
||||||
enum CloneFlag
|
enum CloneFlag
|
||||||
@ -69,6 +71,10 @@ public:
|
|||||||
Group::TriState autoTypeEnabled;
|
Group::TriState autoTypeEnabled;
|
||||||
Group::TriState searchingEnabled;
|
Group::TriState searchingEnabled;
|
||||||
Group::MergeMode mergeMode;
|
Group::MergeMode mergeMode;
|
||||||
|
|
||||||
|
bool operator==(const GroupData& other) const;
|
||||||
|
bool operator!=(const GroupData& other) const;
|
||||||
|
bool equals(const GroupData& other, CompareItemOptions options) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
Group();
|
Group();
|
||||||
@ -77,6 +83,7 @@ public:
|
|||||||
static Group* createRecycleBin();
|
static Group* createRecycleBin();
|
||||||
|
|
||||||
const QUuid& uuid() const;
|
const QUuid& uuid() const;
|
||||||
|
const QString uuidToHex() const;
|
||||||
QString name() const;
|
QString name() const;
|
||||||
QString notes() const;
|
QString notes() const;
|
||||||
QImage icon() const;
|
QImage icon() const;
|
||||||
@ -84,7 +91,7 @@ public:
|
|||||||
QPixmap iconScaledPixmap() const;
|
QPixmap iconScaledPixmap() const;
|
||||||
int iconNumber() const;
|
int iconNumber() const;
|
||||||
const QUuid& iconUuid() const;
|
const QUuid& iconUuid() const;
|
||||||
TimeInfo timeInfo() const;
|
const TimeInfo& timeInfo() const;
|
||||||
bool isExpanded() const;
|
bool isExpanded() const;
|
||||||
QString defaultAutoTypeSequence() const;
|
QString defaultAutoTypeSequence() const;
|
||||||
QString effectiveAutoTypeSequence() const;
|
QString effectiveAutoTypeSequence() const;
|
||||||
@ -98,6 +105,8 @@ public:
|
|||||||
CustomData* customData();
|
CustomData* customData();
|
||||||
const CustomData* customData() const;
|
const CustomData* customData() const;
|
||||||
|
|
||||||
|
bool equals(const Group* other, CompareItemOptions options) const;
|
||||||
|
|
||||||
static const int DefaultIconNumber;
|
static const int DefaultIconNumber;
|
||||||
static const int RecycleBinIconNumber;
|
static const int RecycleBinIconNumber;
|
||||||
static CloneFlags DefaultCloneFlags;
|
static CloneFlags DefaultCloneFlags;
|
||||||
@ -105,10 +114,10 @@ public:
|
|||||||
static const QString RootAutoTypeSequence;
|
static const QString RootAutoTypeSequence;
|
||||||
|
|
||||||
Group* findChildByName(const QString& name);
|
Group* findChildByName(const QString& name);
|
||||||
Group* findChildByUuid(const QUuid& uuid);
|
|
||||||
Entry* findEntry(QString entryId);
|
Entry* findEntry(QString entryId);
|
||||||
Entry* findEntryByUuid(const QUuid& uuid);
|
Entry* findEntryByUuid(const QUuid& uuid) const;
|
||||||
Entry* findEntryByPath(QString entryPath, QString basePath = QString(""));
|
Entry* findEntryByPath(QString entryPath, QString basePath = QString(""));
|
||||||
|
Group* findGroupByUuid(const QUuid& uuid);
|
||||||
Group* findGroupByPath(QString groupPath);
|
Group* findGroupByPath(QString groupPath);
|
||||||
QStringList locate(QString locateTerm, QString currentPath = QString("/"));
|
QStringList locate(QString locateTerm, QString currentPath = QString("/"));
|
||||||
Entry* addEntryWithPath(QString entryPath);
|
Entry* addEntryWithPath(QString entryPath);
|
||||||
@ -127,6 +136,7 @@ public:
|
|||||||
void setExpiryTime(const QDateTime& dateTime);
|
void setExpiryTime(const QDateTime& dateTime);
|
||||||
void setMergeMode(MergeMode newMode);
|
void setMergeMode(MergeMode newMode);
|
||||||
|
|
||||||
|
bool canUpdateTimeinfo() const;
|
||||||
void setUpdateTimeinfo(bool value);
|
void setUpdateTimeinfo(bool value);
|
||||||
|
|
||||||
Group* parentGroup();
|
Group* parentGroup();
|
||||||
@ -153,9 +163,10 @@ public:
|
|||||||
CloneFlags groupFlags = DefaultCloneFlags) const;
|
CloneFlags groupFlags = DefaultCloneFlags) const;
|
||||||
|
|
||||||
void copyDataFrom(const Group* other);
|
void copyDataFrom(const Group* other);
|
||||||
void merge(const Group* other);
|
|
||||||
QString print(bool recursive = false, int depth = 0);
|
QString print(bool recursive = false, int depth = 0);
|
||||||
|
|
||||||
|
void addEntry(Entry* entry);
|
||||||
|
void removeEntry(Entry* entry);
|
||||||
signals:
|
signals:
|
||||||
void dataChanged(Group* group);
|
void dataChanged(Group* group);
|
||||||
|
|
||||||
@ -184,12 +195,7 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
template <class P, class V> bool set(P& property, const V& value);
|
template <class P, class V> bool set(P& property, const V& value);
|
||||||
|
|
||||||
void addEntry(Entry* entry);
|
|
||||||
void removeEntry(Entry* entry);
|
|
||||||
void setParent(Database* db);
|
void setParent(Database* db);
|
||||||
void markOlderEntry(Entry* entry);
|
|
||||||
void resolveEntryConflict(Entry* existingEntry, Entry* otherEntry);
|
|
||||||
void resolveGroupConflict(Group* existingGroup, Group* otherGroup);
|
|
||||||
|
|
||||||
void recSetDatabase(Database* db);
|
void recSetDatabase(Database* db);
|
||||||
void cleanupParent();
|
void cleanupParent();
|
||||||
|
613
src/core/Merger.cpp
Normal file
613
src/core/Merger.cpp
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
/*
|
||||||
|
* 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 "Merger.h"
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Entry.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
|
|
||||||
|
Merger::Merger(const Database* sourceDb, Database* targetDb)
|
||||||
|
: m_mode(Group::Default)
|
||||||
|
{
|
||||||
|
if (!sourceDb || !targetDb) {
|
||||||
|
Q_ASSERT(sourceDb && targetDb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_context = MergeContext{
|
||||||
|
sourceDb, targetDb, sourceDb->rootGroup(), targetDb->rootGroup(), sourceDb->rootGroup(), targetDb->rootGroup()};
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::Merger(const Group* sourceGroup, Group* targetGroup)
|
||||||
|
: m_mode(Group::Default)
|
||||||
|
{
|
||||||
|
if (!sourceGroup || !targetGroup) {
|
||||||
|
Q_ASSERT(sourceGroup && targetGroup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_context = MergeContext{sourceGroup->database(),
|
||||||
|
targetGroup->database(),
|
||||||
|
sourceGroup->database()->rootGroup(),
|
||||||
|
targetGroup->database()->rootGroup(),
|
||||||
|
sourceGroup,
|
||||||
|
targetGroup};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::setForcedMergeMode(Group::MergeMode mode)
|
||||||
|
{
|
||||||
|
m_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::resetForcedMergeMode()
|
||||||
|
{
|
||||||
|
m_mode = Group::Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Merger::merge()
|
||||||
|
{
|
||||||
|
// Order of merge steps is important - it is possible that we
|
||||||
|
// create some items before deleting them afterwards
|
||||||
|
ChangeList changes;
|
||||||
|
changes << mergeGroup(m_context);
|
||||||
|
changes << mergeDeletions(m_context);
|
||||||
|
changes << mergeMetadata(m_context);
|
||||||
|
|
||||||
|
// qDebug("Merged %s", qPrintable(changes.join("\n\t")));
|
||||||
|
|
||||||
|
// At this point we have a list of changes we may want to show the user
|
||||||
|
if (!changes.isEmpty()) {
|
||||||
|
m_context.m_targetDb->markAsModified();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
|
||||||
|
{
|
||||||
|
ChangeList changes;
|
||||||
|
// merge entries
|
||||||
|
const QList<Entry*> sourceEntries = context.m_sourceGroup->entries();
|
||||||
|
for (Entry* sourceEntry : sourceEntries) {
|
||||||
|
Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid());
|
||||||
|
if (!targetEntry) {
|
||||||
|
changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
|
||||||
|
// This entry does not exist at all. Create it.
|
||||||
|
targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||||
|
moveEntry(targetEntry, context.m_targetGroup);
|
||||||
|
} else {
|
||||||
|
// Entry is already present in the database. Update it.
|
||||||
|
const bool locationChanged = targetEntry->timeInfo().locationChanged() < sourceEntry->timeInfo().locationChanged();
|
||||||
|
if (locationChanged && targetEntry->group() != context.m_targetGroup) {
|
||||||
|
changes << tr("Relocating %1 [%2]").arg(sourceEntry->title()).arg(sourceEntry->uuidToHex());
|
||||||
|
moveEntry(targetEntry, context.m_targetGroup);
|
||||||
|
}
|
||||||
|
changes << resolveEntryConflict(context, sourceEntry, targetEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge groups recursively
|
||||||
|
const QList<Group*> sourceChildGroups = context.m_sourceGroup->children();
|
||||||
|
for (Group* sourceChildGroup : sourceChildGroups) {
|
||||||
|
Group* targetChildGroup = context.m_targetRootGroup->findGroupByUuid(sourceChildGroup->uuid());
|
||||||
|
if (!targetChildGroup) {
|
||||||
|
changes << tr("Creating missing %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
|
||||||
|
targetChildGroup = sourceChildGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
|
||||||
|
moveGroup(targetChildGroup, context.m_targetGroup);
|
||||||
|
TimeInfo timeinfo = targetChildGroup->timeInfo();
|
||||||
|
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
|
||||||
|
targetChildGroup->setTimeInfo(timeinfo);
|
||||||
|
} else {
|
||||||
|
bool locationChanged =
|
||||||
|
targetChildGroup->timeInfo().locationChanged() < sourceChildGroup->timeInfo().locationChanged();
|
||||||
|
if (locationChanged && targetChildGroup->parent() != context.m_targetGroup) {
|
||||||
|
changes << tr("Relocating %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
|
||||||
|
moveGroup(targetChildGroup, context.m_targetGroup);
|
||||||
|
TimeInfo timeinfo = targetChildGroup->timeInfo();
|
||||||
|
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
|
||||||
|
targetChildGroup->setTimeInfo(timeinfo);
|
||||||
|
}
|
||||||
|
changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup);
|
||||||
|
}
|
||||||
|
MergeContext subcontext{context.m_sourceDb,
|
||||||
|
context.m_targetDb,
|
||||||
|
context.m_sourceRootGroup,
|
||||||
|
context.m_targetRootGroup,
|
||||||
|
sourceChildGroup,
|
||||||
|
targetChildGroup};
|
||||||
|
changes << mergeGroup(subcontext);
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChildGroup, Group* targetChildGroup)
|
||||||
|
{
|
||||||
|
Q_UNUSED(context);
|
||||||
|
ChangeList changes;
|
||||||
|
|
||||||
|
const QDateTime timeExisting = targetChildGroup->timeInfo().lastModificationTime();
|
||||||
|
const QDateTime timeOther = sourceChildGroup->timeInfo().lastModificationTime();
|
||||||
|
|
||||||
|
// only if the other group is newer, update the existing one.
|
||||||
|
if (timeExisting < timeOther) {
|
||||||
|
changes << tr("Overwriting %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
|
||||||
|
targetChildGroup->setName(sourceChildGroup->name());
|
||||||
|
targetChildGroup->setNotes(sourceChildGroup->notes());
|
||||||
|
if (sourceChildGroup->iconNumber() == 0) {
|
||||||
|
targetChildGroup->setIcon(sourceChildGroup->iconUuid());
|
||||||
|
} else {
|
||||||
|
targetChildGroup->setIcon(sourceChildGroup->iconNumber());
|
||||||
|
}
|
||||||
|
targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime());
|
||||||
|
TimeInfo timeInfo = targetChildGroup->timeInfo();
|
||||||
|
timeInfo.setLastModificationTime(timeOther);
|
||||||
|
targetChildGroup->setTimeInfo(timeInfo);
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Merger::markOlderEntry(Entry* entry)
|
||||||
|
{
|
||||||
|
entry->attributes()->set(
|
||||||
|
"merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::moveEntry(Entry* entry, Group* targetGroup)
|
||||||
|
{
|
||||||
|
Q_ASSERT(entry);
|
||||||
|
Group* sourceGroup = entry->group();
|
||||||
|
if (sourceGroup == targetGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (sourceGroup) {
|
||||||
|
sourceGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (targetGroup) {
|
||||||
|
targetGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
const bool entryUpdateTimeInfo = entry->canUpdateTimeinfo();
|
||||||
|
entry->setUpdateTimeinfo(false);
|
||||||
|
|
||||||
|
entry->setGroup(targetGroup);
|
||||||
|
|
||||||
|
entry->setUpdateTimeinfo(entryUpdateTimeInfo);
|
||||||
|
if (targetGroup) {
|
||||||
|
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
if (sourceGroup) {
|
||||||
|
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::moveGroup(Group* group, Group* targetGroup)
|
||||||
|
{
|
||||||
|
Q_ASSERT(group);
|
||||||
|
Group* sourceGroup = group->parentGroup();
|
||||||
|
if (sourceGroup == targetGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (sourceGroup) {
|
||||||
|
sourceGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (targetGroup) {
|
||||||
|
targetGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
const bool groupUpdateTimeInfo = group->canUpdateTimeinfo();
|
||||||
|
group->setUpdateTimeinfo(false);
|
||||||
|
|
||||||
|
group->setParent(targetGroup);
|
||||||
|
|
||||||
|
group->setUpdateTimeinfo(groupUpdateTimeInfo);
|
||||||
|
if (targetGroup) {
|
||||||
|
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
if (sourceGroup) {
|
||||||
|
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::eraseEntry(Entry* entry)
|
||||||
|
{
|
||||||
|
Database* database = entry->database();
|
||||||
|
// most simple method to remove an item from DeletedObjects :(
|
||||||
|
const QList<DeletedObject> deletions = database->deletedObjects();
|
||||||
|
Group* parentGroup = entry->group();
|
||||||
|
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
delete entry;
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
database->setDeletedObjects(deletions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merger::eraseGroup(Group* group)
|
||||||
|
{
|
||||||
|
Database* database = group->database();
|
||||||
|
// most simple method to remove an item from DeletedObjects :(
|
||||||
|
const QList<DeletedObject> deletions = database->deletedObjects();
|
||||||
|
Group* parentGroup = group->parentGroup();
|
||||||
|
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup->setUpdateTimeinfo(false);
|
||||||
|
}
|
||||||
|
delete group;
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
|
||||||
|
}
|
||||||
|
database->setDeletedObjects(deletions);
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
|
||||||
|
{
|
||||||
|
ChangeList changes;
|
||||||
|
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
|
||||||
|
// if one entry is newer, create a clone and add it to the group
|
||||||
|
if (comparison < 0) {
|
||||||
|
Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
|
||||||
|
moveEntry(clonedEntry, context.m_targetGroup);
|
||||||
|
markOlderEntry(targetEntry);
|
||||||
|
changes << tr("Adding backup for older target %1 [%2]")
|
||||||
|
.arg(targetEntry->title())
|
||||||
|
.arg(targetEntry->uuidToHex());
|
||||||
|
} else if (comparison > 0) {
|
||||||
|
Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
|
||||||
|
moveEntry(clonedEntry, context.m_targetGroup);
|
||||||
|
markOlderEntry(clonedEntry);
|
||||||
|
changes << tr("Adding backup for older source %1 [%2]")
|
||||||
|
.arg(sourceEntry->title())
|
||||||
|
.arg(sourceEntry->uuidToHex());
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
|
||||||
|
{
|
||||||
|
Q_UNUSED(context);
|
||||||
|
ChangeList changes;
|
||||||
|
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
|
||||||
|
if (comparison < 0) {
|
||||||
|
// we need to make our older entry "newer" than the new entry - therefore
|
||||||
|
// we just create a new history entry without any changes - this preserves
|
||||||
|
// the old state before merging the new state and updates the timestamp
|
||||||
|
// the merge takes care, that the newer entry is sorted inbetween both entries
|
||||||
|
// this type of merge changes the database timestamp since reapplying the
|
||||||
|
// old entry is an active change of the database!
|
||||||
|
changes << tr("Reapplying older target entry on top of newer source %1 [%2]")
|
||||||
|
.arg(targetEntry->title())
|
||||||
|
.arg(targetEntry->uuidToHex());
|
||||||
|
Entry* agedTargetEntry = targetEntry->clone(Entry::CloneNoFlags);
|
||||||
|
targetEntry->addHistoryItem(agedTargetEntry);
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
|
||||||
|
{
|
||||||
|
Q_UNUSED(context);
|
||||||
|
ChangeList changes;
|
||||||
|
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
|
||||||
|
if (comparison > 0) {
|
||||||
|
// we need to make our older entry "newer" than the new entry - therefore
|
||||||
|
// we just create a new history entry without any changes - this preserves
|
||||||
|
// the old state before merging the new state and updates the timestamp
|
||||||
|
// the merge takes care, that the newer entry is sorted inbetween both entries
|
||||||
|
// this type of merge changes the database timestamp since reapplying the
|
||||||
|
// old entry is an active change of the database!
|
||||||
|
changes << tr("Reapplying older source entry on top of newer target %1 [%2]")
|
||||||
|
.arg(targetEntry->title())
|
||||||
|
.arg(targetEntry->uuidToHex());
|
||||||
|
targetEntry->beginUpdate();
|
||||||
|
targetEntry->copyDataFrom(sourceEntry);
|
||||||
|
targetEntry->endUpdate();
|
||||||
|
// History item is created by endUpdate since we should have changes
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod)
|
||||||
|
{
|
||||||
|
Q_UNUSED(context);
|
||||||
|
|
||||||
|
ChangeList changes;
|
||||||
|
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
|
||||||
|
if (comparison < 0) {
|
||||||
|
Group* currentGroup = targetEntry->group();
|
||||||
|
Entry* clonedEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||||
|
qDebug("Merge %s/%s with alien on top under %s",
|
||||||
|
qPrintable(targetEntry->title()),
|
||||||
|
qPrintable(sourceEntry->title()),
|
||||||
|
qPrintable(currentGroup->name()));
|
||||||
|
changes << tr("Synchronizing from newer source %1 [%2]")
|
||||||
|
.arg(targetEntry->title())
|
||||||
|
.arg(targetEntry->uuidToHex());
|
||||||
|
moveEntry(clonedEntry, currentGroup);
|
||||||
|
mergeHistory(targetEntry, clonedEntry, mergeMethod);
|
||||||
|
eraseEntry(targetEntry);
|
||||||
|
} else {
|
||||||
|
qDebug("Merge %s/%s with local on top/under %s",
|
||||||
|
qPrintable(targetEntry->title()),
|
||||||
|
qPrintable(sourceEntry->title()),
|
||||||
|
qPrintable(targetEntry->group()->name()));
|
||||||
|
const bool changed = mergeHistory(sourceEntry, targetEntry, mergeMethod);
|
||||||
|
if (changed) {
|
||||||
|
changes << tr("Synchronizing from older source %1 [%2]")
|
||||||
|
.arg(targetEntry->title())
|
||||||
|
.arg(targetEntry->uuidToHex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
|
||||||
|
{
|
||||||
|
ChangeList changes;
|
||||||
|
// We need to cut off the milliseconds since the persistent format only supports times down to seconds
|
||||||
|
// so when we import data from a remote source, it may represent the (or even some msec newer) data
|
||||||
|
// which may be discarded due to higher runtime precision
|
||||||
|
|
||||||
|
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
|
||||||
|
switch (mergeMode) {
|
||||||
|
case Group::Duplicate:
|
||||||
|
changes << resolveEntryConflict_Duplicate(context, sourceEntry, targetEntry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Group::KeepLocal:
|
||||||
|
changes << resolveEntryConflict_KeepLocal(context, sourceEntry, targetEntry);
|
||||||
|
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Group::KeepRemote:
|
||||||
|
changes << resolveEntryConflict_KeepRemote(context, sourceEntry, targetEntry);
|
||||||
|
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Group::Synchronize:
|
||||||
|
case Group::KeepNewer:
|
||||||
|
// nothing special to do since resolveEntryConflictMergeHistories takes care to use the newest entry
|
||||||
|
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Merger::mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod)
|
||||||
|
{
|
||||||
|
Q_UNUSED(mergeMethod);
|
||||||
|
const auto targetHistoryItems = targetEntry->historyItems();
|
||||||
|
const auto sourceHistoryItems = sourceEntry->historyItems();
|
||||||
|
const int comparison = compare(sourceEntry->timeInfo().lastModificationTime(), targetEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
|
||||||
|
const bool preferLocal = mergeMethod == Group::KeepLocal || comparison < 0;
|
||||||
|
const bool preferRemote = mergeMethod == Group::KeepRemote || comparison > 0;
|
||||||
|
|
||||||
|
QMap<QDateTime, Entry*> merged;
|
||||||
|
for (Entry* historyItem : targetHistoryItems) {
|
||||||
|
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
|
||||||
|
if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
|
||||||
|
::qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
|
||||||
|
qPrintable(sourceEntry->title()),
|
||||||
|
qPrintable(sourceEntry->uuidToHex()),
|
||||||
|
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
|
||||||
|
}
|
||||||
|
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
|
||||||
|
}
|
||||||
|
for (Entry* historyItem : sourceHistoryItems) {
|
||||||
|
// Items with same modification-time changes will be regarded as same (like KeePass2)
|
||||||
|
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
|
||||||
|
if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
|
||||||
|
::qWarning("History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
|
||||||
|
qPrintable(sourceEntry->title()),
|
||||||
|
qPrintable(sourceEntry->uuidToHex()),
|
||||||
|
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
|
||||||
|
}
|
||||||
|
if (preferRemote && merged.contains(modificationTime)) {
|
||||||
|
// forcefully apply the remote history item
|
||||||
|
delete merged.take(modificationTime);
|
||||||
|
}
|
||||||
|
if (!merged.contains(modificationTime)) {
|
||||||
|
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime targetModificationTime = Clock::serialized(targetEntry->timeInfo().lastModificationTime());
|
||||||
|
const QDateTime sourceModificationTime = Clock::serialized(sourceEntry->timeInfo().lastModificationTime());
|
||||||
|
if (targetModificationTime == sourceModificationTime && !targetEntry->equals(sourceEntry, CompareItemIgnoreMilliseconds | CompareItemIgnoreHistory | CompareItemIgnoreLocation)) {
|
||||||
|
::qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!",
|
||||||
|
qPrintable(sourceEntry->title()),
|
||||||
|
qPrintable(sourceEntry->uuidToHex()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetModificationTime < sourceModificationTime) {
|
||||||
|
if (preferLocal && merged.contains(targetModificationTime)) {
|
||||||
|
// forcefully apply the local history item
|
||||||
|
delete merged.take(targetModificationTime);
|
||||||
|
}
|
||||||
|
if (!merged.contains(targetModificationTime)) {
|
||||||
|
merged[targetModificationTime] = targetEntry->clone(Entry::CloneNoFlags);
|
||||||
|
}
|
||||||
|
} else if (targetModificationTime > sourceModificationTime) {
|
||||||
|
if (preferRemote && !merged.contains(sourceModificationTime)) {
|
||||||
|
// forcefully apply the remote history item
|
||||||
|
delete merged.take(sourceModificationTime);
|
||||||
|
}
|
||||||
|
if (!merged.contains(sourceModificationTime)) {
|
||||||
|
merged[sourceModificationTime] = sourceEntry->clone(Entry::CloneNoFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
const int maxItems = targetEntry->database()->metadata()->historyMaxItems();
|
||||||
|
const auto updatedHistoryItems = merged.values();
|
||||||
|
for (int i = 0; i < maxItems; ++i) {
|
||||||
|
const Entry* oldEntry = targetHistoryItems.value(targetHistoryItems.count() - i);
|
||||||
|
const Entry* newEntry = updatedHistoryItems.value(updatedHistoryItems.count() - i);
|
||||||
|
if (!oldEntry && !newEntry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldEntry && newEntry && oldEntry->equals(newEntry, CompareItemIgnoreMilliseconds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
qDeleteAll(updatedHistoryItems);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// We need to prevent any modification to the database since every change should be tracked either
|
||||||
|
// in a clone history item or in the Entry itself
|
||||||
|
const TimeInfo timeInfo = targetEntry->timeInfo();
|
||||||
|
const bool blockedSignals = targetEntry->blockSignals(true);
|
||||||
|
bool updateTimeInfo = targetEntry->canUpdateTimeinfo();
|
||||||
|
targetEntry->setUpdateTimeinfo(false);
|
||||||
|
targetEntry->removeHistoryItems(targetHistoryItems);
|
||||||
|
for (Entry* historyItem : merged.values()) {
|
||||||
|
Q_ASSERT(!historyItem->parent());
|
||||||
|
targetEntry->addHistoryItem(historyItem);
|
||||||
|
}
|
||||||
|
targetEntry->truncateHistory();
|
||||||
|
targetEntry->blockSignals(blockedSignals);
|
||||||
|
targetEntry->setUpdateTimeinfo(updateTimeInfo);
|
||||||
|
Q_ASSERT(timeInfo == targetEntry->timeInfo());
|
||||||
|
Q_UNUSED(timeInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
|
||||||
|
{
|
||||||
|
ChangeList changes;
|
||||||
|
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
|
||||||
|
if (mergeMode != Group::Synchronize) {
|
||||||
|
// no deletions are applied for any other strategy!
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto targetDeletions = context.m_targetDb->deletedObjects();
|
||||||
|
const auto sourceDeletions = context.m_sourceDb->deletedObjects();
|
||||||
|
|
||||||
|
QList<DeletedObject> deletions;
|
||||||
|
QMap<QUuid, DeletedObject> mergedDeletions;
|
||||||
|
QList<Entry*> entries;
|
||||||
|
QList<Group*> groups;
|
||||||
|
|
||||||
|
for (const auto& object : (targetDeletions + sourceDeletions)) {
|
||||||
|
if (!mergedDeletions.contains(object.uuid)) {
|
||||||
|
mergedDeletions[object.uuid] = object;
|
||||||
|
|
||||||
|
auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid);
|
||||||
|
if (entry) {
|
||||||
|
entries << entry;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto* group = context.m_targetRootGroup->findGroupByUuid(object.uuid);
|
||||||
|
if (group) {
|
||||||
|
groups << group;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deletions << object;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mergedDeletions[object.uuid].deletionTime > object.deletionTime) {
|
||||||
|
mergedDeletions[object.uuid] = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!entries.isEmpty()) {
|
||||||
|
auto* entry = entries.takeFirst();
|
||||||
|
const auto& object = mergedDeletions[entry->uuid()];
|
||||||
|
if (entry->timeInfo().lastModificationTime() > object.deletionTime) {
|
||||||
|
// keep deleted entry since it was changed after deletion date
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deletions << object;
|
||||||
|
if (entry->group()) {
|
||||||
|
changes << tr("Deleting child %1 [%2]").arg(entry->title()).arg(entry->uuidToHex());
|
||||||
|
} else {
|
||||||
|
changes << tr("Deleting orphan %1 [%2]").arg(entry->title()).arg(entry->uuidToHex());
|
||||||
|
}
|
||||||
|
// Entry is inserted into deletedObjects after deletions are processed
|
||||||
|
eraseEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!groups.isEmpty()) {
|
||||||
|
auto* group = groups.takeFirst();
|
||||||
|
if (!(group->children().toSet() & groups.toSet()).isEmpty()) {
|
||||||
|
// we need to finish all children before we are able to determine if the group can be removed
|
||||||
|
groups << group;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& object = mergedDeletions[group->uuid()];
|
||||||
|
if (group->timeInfo().lastModificationTime() > object.deletionTime) {
|
||||||
|
// keep deleted group since it was changed after deletion date
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!group->entriesRecursive(false).isEmpty() || !group->groupsRecursive(false).isEmpty()) {
|
||||||
|
// keep deleted group since it contains undeleted content
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deletions << object;
|
||||||
|
if (group->parentGroup()) {
|
||||||
|
changes << tr("Deleting child %1 [%2]").arg(group->name()).arg(group->uuidToHex());
|
||||||
|
} else {
|
||||||
|
changes << tr("Deleting orphan %1 [%2]").arg(group->name()).arg(group->uuidToHex());
|
||||||
|
}
|
||||||
|
eraseGroup(group);
|
||||||
|
}
|
||||||
|
// Put every deletion to the earliest date of deletion
|
||||||
|
if (deletions != context.m_targetDb->deletedObjects()) {
|
||||||
|
changes << tr("Changed deleted objects");
|
||||||
|
}
|
||||||
|
context.m_targetDb->setDeletedObjects(deletions);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
|
||||||
|
{
|
||||||
|
// TODO HNH: missing handling of recycle bin, names, templates for groups and entries,
|
||||||
|
// public data (entries of newer dict override keys of older dict - ignoring
|
||||||
|
// their own age - it is enough if one entry of the whole dict is newer) => possible lost update
|
||||||
|
// TODO HNH: CustomData is merged with entries of the new customData overwrite entries
|
||||||
|
// of the older CustomData - the dict with the newest entry is considered
|
||||||
|
// newer regardless of the age of the other entries => possible lost update
|
||||||
|
ChangeList changes;
|
||||||
|
auto* sourceMetadata = context.m_sourceDb->metadata();
|
||||||
|
auto* targetMetadata = context.m_targetDb->metadata();
|
||||||
|
|
||||||
|
for (QUuid customIconId : sourceMetadata->customIcons().keys()) {
|
||||||
|
QImage customIcon = sourceMetadata->customIcon(customIconId);
|
||||||
|
if (!targetMetadata->containsCustomIcon(customIconId)) {
|
||||||
|
targetMetadata->addCustomIcon(customIconId, customIcon);
|
||||||
|
changes << tr("Adding missing icon %1").arg(QString::fromLatin1(customIconId.toRfc4122().toHex()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
72
src/core/Merger.h
Normal file
72
src/core/Merger.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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_MERGER_H
|
||||||
|
#define KEEPASSXC_MERGER_H
|
||||||
|
|
||||||
|
#include "core/Group.h"
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
class Entry;
|
||||||
|
|
||||||
|
class Merger : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Merger(const Database* sourceDb, Database* targetDb);
|
||||||
|
Merger(const Group* sourceGroup, Group* targetGroup);
|
||||||
|
void setForcedMergeMode(Group::MergeMode mode);
|
||||||
|
void resetForcedMergeMode();
|
||||||
|
bool merge();
|
||||||
|
|
||||||
|
private:
|
||||||
|
typedef QString Change;
|
||||||
|
typedef QStringList ChangeList;
|
||||||
|
|
||||||
|
struct MergeContext
|
||||||
|
{
|
||||||
|
QPointer<const Database> m_sourceDb;
|
||||||
|
QPointer<Database> m_targetDb;
|
||||||
|
QPointer<const Group> m_sourceRootGroup;
|
||||||
|
QPointer<Group> m_targetRootGroup;
|
||||||
|
QPointer<const Group> m_sourceGroup;
|
||||||
|
QPointer<Group> m_targetGroup;
|
||||||
|
};
|
||||||
|
ChangeList mergeGroup(const MergeContext& context);
|
||||||
|
ChangeList mergeDeletions(const MergeContext& context);
|
||||||
|
ChangeList mergeMetadata(const MergeContext& context);
|
||||||
|
bool markOlderEntry(Entry* entry);
|
||||||
|
bool mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod);
|
||||||
|
void moveEntry(Entry* entry, Group* targetGroup);
|
||||||
|
void moveGroup(Group* group, Group* targetGroup);
|
||||||
|
void eraseEntry(Entry* entry); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries
|
||||||
|
void eraseGroup(Group* group); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries
|
||||||
|
ChangeList resolveEntryConflict(const MergeContext& context, const Entry* existingEntry, Entry* otherEntry);
|
||||||
|
ChangeList resolveGroupConflict(const MergeContext& context, const Group* existingGroup, Group* otherGroup);
|
||||||
|
Merger::ChangeList resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
|
||||||
|
Merger::ChangeList resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
|
||||||
|
Merger::ChangeList resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
|
||||||
|
Merger::ChangeList resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod);
|
||||||
|
|
||||||
|
private:
|
||||||
|
MergeContext m_context;
|
||||||
|
Group::MergeMode m_mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSXC_MERGER_H
|
@ -18,6 +18,7 @@
|
|||||||
#include "Metadata.h"
|
#include "Metadata.h"
|
||||||
#include <QtCore/QCryptographicHash>
|
#include <QtCore/QCryptographicHash>
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
@ -43,7 +44,7 @@ Metadata::Metadata(QObject* parent)
|
|||||||
m_data.protectUrl = false;
|
m_data.protectUrl = false;
|
||||||
m_data.protectNotes = false;
|
m_data.protectNotes = false;
|
||||||
|
|
||||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
QDateTime now = Clock::currentDateTimeUtc();
|
||||||
m_data.nameChanged = now;
|
m_data.nameChanged = now;
|
||||||
m_data.descriptionChanged = now;
|
m_data.descriptionChanged = now;
|
||||||
m_data.defaultUserNameChanged = now;
|
m_data.defaultUserNameChanged = now;
|
||||||
@ -71,7 +72,7 @@ template <class P, class V> bool Metadata::set(P& property, const V& value, QDat
|
|||||||
if (property != value) {
|
if (property != value) {
|
||||||
property = value;
|
property = value;
|
||||||
if (m_updateDatetime) {
|
if (m_updateDatetime) {
|
||||||
dateTime = QDateTime::currentDateTimeUtc();
|
dateTime = Clock::currentDateTimeUtc();
|
||||||
}
|
}
|
||||||
emit modified();
|
emit modified();
|
||||||
return true;
|
return true;
|
||||||
|
@ -17,11 +17,13 @@
|
|||||||
|
|
||||||
#include "TimeInfo.h"
|
#include "TimeInfo.h"
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
|
|
||||||
TimeInfo::TimeInfo()
|
TimeInfo::TimeInfo()
|
||||||
: m_expires(false)
|
: m_expires(false)
|
||||||
, m_usageCount(0)
|
, m_usageCount(0)
|
||||||
{
|
{
|
||||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
QDateTime now = Clock::currentDateTimeUtc();
|
||||||
m_lastModificationTime = now;
|
m_lastModificationTime = now;
|
||||||
m_creationTime = now;
|
m_creationTime = now;
|
||||||
m_lastAccessTime = now;
|
m_lastAccessTime = now;
|
||||||
@ -103,3 +105,38 @@ void TimeInfo::setLocationChanged(const QDateTime& dateTime)
|
|||||||
Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
|
Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
|
||||||
m_locationChanged = dateTime;
|
m_locationChanged = dateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TimeInfo::operator==(const TimeInfo& other) const
|
||||||
|
{
|
||||||
|
return equals(other, CompareItemDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TimeInfo::operator!=(const TimeInfo& other) const
|
||||||
|
{
|
||||||
|
return !this->operator==(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const
|
||||||
|
{
|
||||||
|
if (::compare(m_lastModificationTime, other.m_lastModificationTime, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(m_creationTime, other.m_creationTime, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options)
|
||||||
|
!= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(m_expires, m_expiryTime, other.m_expires, other.expiryTime(), options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_usageCount, other.m_usageCount, options) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options)
|
||||||
|
!= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
#define KEEPASSX_TIMEINFO_H
|
#define KEEPASSX_TIMEINFO_H
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QFlag>
|
||||||
|
|
||||||
|
#include "core/Compare.h"
|
||||||
|
|
||||||
class TimeInfo
|
class TimeInfo
|
||||||
{
|
{
|
||||||
@ -33,6 +36,10 @@ public:
|
|||||||
int usageCount() const;
|
int usageCount() const;
|
||||||
QDateTime locationChanged() const;
|
QDateTime locationChanged() const;
|
||||||
|
|
||||||
|
bool operator==(const TimeInfo& other) const;
|
||||||
|
bool operator!=(const TimeInfo& other) const;
|
||||||
|
bool equals(const TimeInfo& other, CompareItemOptions options = CompareItemDefault) const;
|
||||||
|
|
||||||
void setLastModificationTime(const QDateTime& dateTime);
|
void setLastModificationTime(const QDateTime& dateTime);
|
||||||
void setCreationTime(const QDateTime& dateTime);
|
void setCreationTime(const QDateTime& dateTime);
|
||||||
void setLastAccessTime(const QDateTime& dateTime);
|
void setLastAccessTime(const QDateTime& dateTime);
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "KdbxXmlReader.h"
|
#include "KdbxXmlReader.h"
|
||||||
#include "KeePass2RandomStream.h"
|
#include "KeePass2RandomStream.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/DatabaseIcons.h"
|
#include "core/DatabaseIcons.h"
|
||||||
#include "core/Endian.h"
|
#include "core/Endian.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
@ -1032,7 +1033,7 @@ QDateTime KdbxXmlReader::readDateTime()
|
|||||||
return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs);
|
return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs);
|
||||||
}
|
}
|
||||||
|
|
||||||
QDateTime dt = QDateTime::fromString(str, Qt::ISODate);
|
QDateTime dt = Clock::parse(str, Qt::ISODate);
|
||||||
if (dt.isValid()) {
|
if (dt.isValid()) {
|
||||||
return dt;
|
return dt;
|
||||||
}
|
}
|
||||||
@ -1041,7 +1042,7 @@ QDateTime KdbxXmlReader::readDateTime()
|
|||||||
raiseError(tr("Invalid date time value"));
|
raiseError(tr("Invalid date time value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return QDateTime::currentDateTimeUtc();
|
return Clock::currentDateTimeUtc();
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor KdbxXmlReader::readColor()
|
QColor KdbxXmlReader::readColor()
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
#include "core/EntrySearcher.h"
|
#include "core/EntrySearcher.h"
|
||||||
#include "core/FilePath.h"
|
#include "core/FilePath.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
|
#include "core/Merger.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "format/KeePass2Reader.h"
|
#include "format/KeePass2Reader.h"
|
||||||
@ -841,7 +842,8 @@ void DatabaseWidget::mergeDatabase(bool accepted)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_db->merge(srcDb);
|
Merger merger(srcDb, m_db);
|
||||||
|
merger.merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_databaseOpenMergeWidget->clearForms();
|
m_databaseOpenMergeWidget->clearForms();
|
||||||
@ -1244,7 +1246,7 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
|
|
||||||
if (mb == QMessageBox::No) {
|
if (mb == QMessageBox::No) {
|
||||||
// Notify everyone the database does not match the file
|
// Notify everyone the database does not match the file
|
||||||
emit m_db->modified();
|
m_db->markAsModified();
|
||||||
m_databaseModified = true;
|
m_databaseModified = true;
|
||||||
// Rewatch the database file
|
// Rewatch the database file
|
||||||
m_fileWatcher.addPath(m_filePath);
|
m_fileWatcher.addPath(m_filePath);
|
||||||
@ -1269,7 +1271,8 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
if (mb == QMessageBox::Yes) {
|
if (mb == QMessageBox::Yes) {
|
||||||
// Merge the old database into the new one
|
// Merge the old database into the new one
|
||||||
m_db->setEmitModified(false);
|
m_db->setEmitModified(false);
|
||||||
db->merge(m_db);
|
Merger merger(m_db, db);
|
||||||
|
merger.merge();
|
||||||
} else {
|
} else {
|
||||||
// Since we are accepting the new file as-is, internally mark as unmodified
|
// Since we are accepting the new file as-is, internally mark as unmodified
|
||||||
// TODO: when saving is moved out of DatabaseTabWidget, this should be replaced
|
// TODO: when saving is moved out of DatabaseTabWidget, this should be replaced
|
||||||
@ -1300,7 +1303,7 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
MessageWidget::Error);
|
MessageWidget::Error);
|
||||||
// HACK: Directly calling the database's signal
|
// HACK: Directly calling the database's signal
|
||||||
// Mark db as modified since existing data may differ from file or file was deleted
|
// Mark db as modified since existing data may differ from file or file was deleted
|
||||||
m_db->modified();
|
m_db->markAsModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewatch the database file
|
// Rewatch the database file
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include "TotpDialog.h"
|
#include "TotpDialog.h"
|
||||||
#include "ui_TotpDialog.h"
|
#include "ui_TotpDialog.h"
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "gui/Clipboard.h"
|
#include "gui/Clipboard.h"
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ void TotpDialog::updateProgressBar()
|
|||||||
|
|
||||||
void TotpDialog::updateSeconds()
|
void TotpDialog::updateSeconds()
|
||||||
{
|
{
|
||||||
uint epoch = QDateTime::currentDateTime().toTime_t() - 1;
|
uint epoch = Clock::currentSecondsSinceEpoch() - 1;
|
||||||
m_ui->timerLabel->setText(tr("Expires in <b>%n</b> second(s)", "", m_step - (epoch % m_step)));
|
m_ui->timerLabel->setText(tr("Expires in <b>%n</b> second(s)", "", m_step - (epoch % m_step)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +92,6 @@ void TotpDialog::updateTotp()
|
|||||||
|
|
||||||
void TotpDialog::resetCounter()
|
void TotpDialog::resetCounter()
|
||||||
{
|
{
|
||||||
uint epoch = QDateTime::currentDateTime().toTime_t();
|
uint epoch = Clock::currentSecondsSinceEpoch();
|
||||||
m_counter = static_cast<int>(static_cast<double>(epoch % m_step) / m_step * 100);
|
m_counter = static_cast<int>(static_cast<double>(epoch % m_step) / m_step * 100);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QSpacerItem>
|
#include <QSpacerItem>
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "format/KeePass2Writer.h"
|
#include "format/KeePass2Writer.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
#include "gui/MessageWidget.h"
|
#include "gui/MessageWidget.h"
|
||||||
@ -255,14 +256,13 @@ void CsvImportWidget::writeDatabase()
|
|||||||
if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) {
|
if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) {
|
||||||
qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong();
|
qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong();
|
||||||
if (lastModified) {
|
if (lastModified) {
|
||||||
timeInfo.setLastModificationTime(
|
timeInfo.setLastModificationTime(Clock::datetimeUtc(lastModified * 1000));
|
||||||
QDateTime::fromMSecsSinceEpoch(lastModified * 1000).toTimeSpec(Qt::UTC));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) {
|
if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) {
|
||||||
qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong();
|
qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong();
|
||||||
if (created) {
|
if (created) {
|
||||||
timeInfo.setCreationTime(QDateTime::fromMSecsSinceEpoch(created * 1000).toTimeSpec(Qt::UTC));
|
timeInfo.setCreationTime(Clock::datetimeUtc(created * 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry->setTimeInfo(timeInfo);
|
entry->setTimeInfo(timeInfo);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "DatabaseSettingsWidgetGeneral.h"
|
#include "DatabaseSettingsWidgetGeneral.h"
|
||||||
#include "ui_DatabaseSettingsWidgetGeneral.h"
|
#include "ui_DatabaseSettingsWidgetGeneral.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
@ -82,7 +83,7 @@ bool DatabaseSettingsWidgetGeneral::save()
|
|||||||
meta->setDescription(m_ui->dbDescriptionEdit->text());
|
meta->setDescription(m_ui->dbDescriptionEdit->text());
|
||||||
meta->setDefaultUserName(m_ui->defaultUsernameEdit->text());
|
meta->setDefaultUserName(m_ui->defaultUsernameEdit->text());
|
||||||
meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked());
|
meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked());
|
||||||
meta->setSettingsChanged(QDateTime::currentDateTimeUtc());
|
meta->setSettingsChanged(Clock::currentDateTimeUtc());
|
||||||
|
|
||||||
bool truncate = false;
|
bool truncate = false;
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include "ui_EditEntryWidgetMain.h"
|
#include "ui_EditEntryWidgetMain.h"
|
||||||
#include "ui_EditEntryWidgetSSHAgent.h"
|
#include "ui_EditEntryWidgetSSHAgent.h"
|
||||||
|
|
||||||
|
#include <QButtonGroup>
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
@ -32,9 +33,9 @@
|
|||||||
#include <QStackedLayout>
|
#include <QStackedLayout>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
#include <QButtonGroup>
|
|
||||||
|
|
||||||
#include "autotype/AutoType.h"
|
#include "autotype/AutoType.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
@ -619,7 +620,7 @@ void EditEntryWidget::useExpiryPreset(QAction* action)
|
|||||||
{
|
{
|
||||||
m_mainUi->expireCheck->setChecked(true);
|
m_mainUi->expireCheck->setChecked(true);
|
||||||
TimeDelta delta = action->data().value<TimeDelta>();
|
TimeDelta delta = action->data().value<TimeDelta>();
|
||||||
QDateTime now = QDateTime::currentDateTime();
|
QDateTime now = Clock::currentDateTime();
|
||||||
QDateTime expiryDateTime = now + delta;
|
QDateTime expiryDateTime = now + delta;
|
||||||
m_mainUi->expireDatePicker->setDateTime(expiryDateTime);
|
m_mainUi->expireDatePicker->setDateTime(expiryDateTime);
|
||||||
}
|
}
|
||||||
|
@ -60,11 +60,12 @@ private:
|
|||||||
Group::TriState triStateFromIndex(int index);
|
Group::TriState triStateFromIndex(int index);
|
||||||
|
|
||||||
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
|
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
|
||||||
QWidget* const m_editGroupWidgetMain;
|
QPointer<QWidget> m_editGroupWidgetMain;
|
||||||
EditWidgetIcons* const m_editGroupWidgetIcons;
|
QPointer<EditWidgetIcons> m_editGroupWidgetIcons;
|
||||||
EditWidgetProperties* const m_editWidgetProperties;
|
QPointer<EditWidgetProperties> m_editWidgetProperties;
|
||||||
Group* m_group;
|
|
||||||
Database* m_database;
|
QPointer<Group> m_group;
|
||||||
|
QPointer<Database> m_database;
|
||||||
|
|
||||||
Q_DISABLE_COPY(EditGroupWidget)
|
Q_DISABLE_COPY(EditGroupWidget)
|
||||||
};
|
};
|
||||||
|
@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
#include "totp.h"
|
#include "totp.h"
|
||||||
#include "core/Base32.h"
|
#include "core/Base32.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QDateTime>
|
|
||||||
#include <QMessageAuthenticationCode>
|
#include <QMessageAuthenticationCode>
|
||||||
#include <QRegExp>
|
#include <QRegExp>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
@ -133,8 +133,7 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings> settings, const
|
|||||||
|
|
||||||
quint64 current;
|
quint64 current;
|
||||||
if (time == 0) {
|
if (time == 0) {
|
||||||
// TODO: Replace toTime_t() with toSecsSinceEpoch() when minimum Qt >= 5.8
|
current = qToBigEndian(static_cast<quint64>(Clock::currentSecondsSinceEpoch()) / step);
|
||||||
current = qToBigEndian(static_cast<quint64>(QDateTime::currentDateTime().toTime_t()) / step);
|
|
||||||
} else {
|
} else {
|
||||||
current = qToBigEndian(time / step);
|
current = qToBigEndian(time / step);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ set(TEST_LIBRARIES
|
|||||||
${ZLIB_LIBRARIES}
|
${ZLIB_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp)
|
set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp)
|
||||||
add_library(testsupport STATIC ${testsupport_SOURCES})
|
add_library(testsupport STATIC ${testsupport_SOURCES})
|
||||||
target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test)
|
target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test)
|
||||||
|
|
||||||
@ -104,16 +104,16 @@ if(YUBIKEY_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_unit_test(NAME testgroup SOURCES TestGroup.cpp
|
add_unit_test(NAME testgroup SOURCES TestGroup.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testkdbx2 SOURCES TestKdbx2.cpp
|
add_unit_test(NAME testkdbx2 SOURCES TestKdbx2.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testkdbx3 SOURCES TestKeePass2Format.cpp FailDevice.cpp TestKdbx3.cpp
|
add_unit_test(NAME testkdbx3 SOURCES TestKeePass2Format.cpp FailDevice.cpp TestKdbx3.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testkdbx4 SOURCES TestKeePass2Format.cpp FailDevice.cpp mock/MockChallengeResponseKey.cpp TestKdbx4.cpp
|
add_unit_test(NAME testkdbx4 SOURCES TestKeePass2Format.cpp FailDevice.cpp mock/MockChallengeResponseKey.cpp TestKdbx4.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testkeys SOURCES TestKeys.cpp mock/MockChallengeResponseKey.cpp
|
add_unit_test(NAME testkeys SOURCES TestKeys.cpp mock/MockChallengeResponseKey.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
@ -137,7 +137,7 @@ add_unit_test(NAME testkeepass2randomstream SOURCES TestKeePass2RandomStream.cpp
|
|||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testmodified SOURCES TestModified.cpp
|
add_unit_test(NAME testmodified SOURCES TestModified.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp
|
add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
@ -163,7 +163,7 @@ add_unit_test(NAME testentry SOURCES TestEntry.cpp
|
|||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testmerge SOURCES TestMerge.cpp
|
add_unit_test(NAME testmerge SOURCES TestMerge.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testtotp SOURCES TestTotp.cpp
|
add_unit_test(NAME testtotp SOURCES TestTotp.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
#include "TestEntry.h"
|
#include "TestEntry.h"
|
||||||
#include "TestGlobal.h"
|
#include "TestGlobal.h"
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "crypto/Crypto.h"
|
#include "crypto/Crypto.h"
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestEntry)
|
QTEST_GUILESS_MAIN(TestEntry)
|
||||||
@ -88,9 +89,7 @@ void TestEntry::testClone()
|
|||||||
entryOrg->setTitle("New Title");
|
entryOrg->setTitle("New Title");
|
||||||
entryOrg->endUpdate();
|
entryOrg->endUpdate();
|
||||||
TimeInfo entryOrgTime = entryOrg->timeInfo();
|
TimeInfo entryOrgTime = entryOrg->timeInfo();
|
||||||
QDateTime dateTime;
|
QDateTime dateTime = Clock::datetimeUtc(60);
|
||||||
dateTime.setTimeSpec(Qt::UTC);
|
|
||||||
dateTime.setTime_t(60);
|
|
||||||
entryOrgTime.setCreationTime(dateTime);
|
entryOrgTime.setCreationTime(dateTime);
|
||||||
entryOrg->setTimeInfo(entryOrgTime);
|
entryOrg->setTimeInfo(entryOrgTime);
|
||||||
|
|
||||||
@ -225,7 +224,7 @@ void TestEntry::testResolveRecursivePlaceholders()
|
|||||||
entry2->setUuid(QUuid::createUuid());
|
entry2->setUuid(QUuid::createUuid());
|
||||||
entry2->setTitle("Entry2Title");
|
entry2->setTitle("Entry2Title");
|
||||||
entry2->setUsername("{S:CustomUserNameAttribute}");
|
entry2->setUsername("{S:CustomUserNameAttribute}");
|
||||||
entry2->setPassword(QString("{REF:P@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex())));
|
entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuidToHex()));
|
||||||
entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}");
|
entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}");
|
||||||
entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue");
|
entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue");
|
||||||
entry2->attributes()->set("IpAddress", "127.0.0.1");
|
entry2->attributes()->set("IpAddress", "127.0.0.1");
|
||||||
@ -235,10 +234,10 @@ void TestEntry::testResolveRecursivePlaceholders()
|
|||||||
auto* entry3 = new Entry();
|
auto* entry3 = new Entry();
|
||||||
entry3->setGroup(root);
|
entry3->setGroup(root);
|
||||||
entry3->setUuid(QUuid::createUuid());
|
entry3->setUuid(QUuid::createUuid());
|
||||||
entry3->setTitle(QString("{REF:T@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex())));
|
entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuidToHex()));
|
||||||
entry3->setUsername(QString("{REF:U@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex())));
|
entry3->setUsername(QString("{REF:U@I:%1}").arg(entry2->uuidToHex()));
|
||||||
entry3->setPassword(QString("{REF:P@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex())));
|
entry3->setPassword(QString("{REF:P@I:%1}").arg(entry2->uuidToHex()));
|
||||||
entry3->setUrl(QString("{REF:A@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex())));
|
entry3->setUrl(QString("{REF:A@I:%1}").arg(entry2->uuidToHex()));
|
||||||
|
|
||||||
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title"));
|
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title"));
|
||||||
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue"));
|
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue"));
|
||||||
@ -248,10 +247,10 @@ void TestEntry::testResolveRecursivePlaceholders()
|
|||||||
auto* entry4 = new Entry();
|
auto* entry4 = new Entry();
|
||||||
entry4->setGroup(root);
|
entry4->setGroup(root);
|
||||||
entry4->setUuid(QUuid::createUuid());
|
entry4->setUuid(QUuid::createUuid());
|
||||||
entry4->setTitle(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex()));
|
||||||
entry4->setUsername(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry4->setUsername(QString("{REF:U@I:%1}").arg(entry3->uuidToHex()));
|
||||||
entry4->setPassword(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry4->setPassword(QString("{REF:P@I:%1}").arg(entry3->uuidToHex()));
|
||||||
entry4->setUrl(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry4->setUrl(QString("{REF:A@I:%1}").arg(entry3->uuidToHex()));
|
||||||
|
|
||||||
QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title"));
|
QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title"));
|
||||||
QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue"));
|
QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue"));
|
||||||
@ -279,7 +278,7 @@ void TestEntry::testResolveRecursivePlaceholders()
|
|||||||
auto* entry6 = new Entry();
|
auto* entry6 = new Entry();
|
||||||
entry6->setGroup(root);
|
entry6->setGroup(root);
|
||||||
entry6->setUuid(QUuid::createUuid());
|
entry6->setUuid(QUuid::createUuid());
|
||||||
entry6->setTitle(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry6->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex()));
|
||||||
entry6->setUsername(QString("{TITLE}"));
|
entry6->setUsername(QString("{TITLE}"));
|
||||||
entry6->setPassword(QString("{PASSWORD}"));
|
entry6->setPassword(QString("{PASSWORD}"));
|
||||||
|
|
||||||
@ -290,7 +289,7 @@ void TestEntry::testResolveRecursivePlaceholders()
|
|||||||
auto* entry7 = new Entry();
|
auto* entry7 = new Entry();
|
||||||
entry7->setGroup(root);
|
entry7->setGroup(root);
|
||||||
entry7->setUuid(QUuid::createUuid());
|
entry7->setUuid(QUuid::createUuid());
|
||||||
entry7->setTitle(QString("{REF:T@I:%1} and something else").arg(QString(entry3->uuid().toRfc4122().toHex())));
|
entry7->setTitle(QString("{REF:T@I:%1} and something else").arg(entry3->uuidToHex()));
|
||||||
entry7->setUsername(QString("{TITLE}"));
|
entry7->setUsername(QString("{TITLE}"));
|
||||||
entry7->setPassword(QString("PASSWORD"));
|
entry7->setPassword(QString("PASSWORD"));
|
||||||
|
|
||||||
@ -344,7 +343,7 @@ void TestEntry::testResolveReferencePlaceholders()
|
|||||||
tstEntry->setGroup(root);
|
tstEntry->setGroup(root);
|
||||||
tstEntry->setUuid(QUuid::createUuid());
|
tstEntry->setUuid(QUuid::createUuid());
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex()))),
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())),
|
||||||
entry1->title());
|
entry1->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), entry1->title());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), entry1->title());
|
||||||
@ -355,7 +354,7 @@ void TestEntry::testResolveReferencePlaceholders()
|
|||||||
QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))),
|
QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))),
|
||||||
entry1->title());
|
entry1->title());
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex()))),
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())),
|
||||||
entry1->title());
|
entry1->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())),
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())),
|
||||||
@ -365,7 +364,7 @@ void TestEntry::testResolveReferencePlaceholders()
|
|||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), entry1->url());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), entry1->url());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), entry1->notes());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), entry1->notes());
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))),
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry2->uuidToHex())),
|
||||||
entry2->title());
|
entry2->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), entry2->title());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), entry2->title());
|
||||||
@ -384,23 +383,23 @@ void TestEntry::testResolveReferencePlaceholders()
|
|||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), entry2->url());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), entry2->url());
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), entry2->notes());
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), entry2->notes());
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeTitle"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeTitle"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeUsername"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeUsername"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributePassword"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributePassword"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeUrl"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeUrl"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeNotes"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeNotes"));
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeTitle"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeTitle"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeUsername"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeUsername"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributePassword"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributePassword"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeUrl"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeUrl"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeNotes"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeNotes"));
|
||||||
|
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeTitle"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeTitle"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeUsername"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeUsername"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributePassword"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributePassword"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeUrl"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeUrl"));
|
||||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeNotes"));
|
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeNotes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEntry::testResolveNonIdPlaceholdersToUuid()
|
void TestEntry::testResolveNonIdPlaceholdersToUuid()
|
||||||
@ -469,7 +468,7 @@ void TestEntry::testResolveNonIdPlaceholdersToUuid()
|
|||||||
|
|
||||||
const QString newEntryNotesResolved =
|
const QString newEntryNotesResolved =
|
||||||
newEntry->resolveMultiplePlaceholders(newEntry->notes());
|
newEntry->resolveMultiplePlaceholders(newEntry->notes());
|
||||||
QCOMPARE(newEntryNotesResolved, QString(referencedEntry->uuid().toRfc4122().toHex()));
|
QCOMPARE(newEntryNotesResolved, referencedEntry->uuidToHex());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,14 +43,4 @@ namespace QTest
|
|||||||
|
|
||||||
} // namespace QTest
|
} // namespace QTest
|
||||||
|
|
||||||
namespace Test
|
|
||||||
{
|
|
||||||
|
|
||||||
inline QDateTime datetime(int year, int month, int day, int hour, int min, int second)
|
|
||||||
{
|
|
||||||
return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::UTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Test
|
|
||||||
|
|
||||||
#endif // KEEPASSXC_TESTGLOBAL_H
|
#endif // KEEPASSXC_TESTGLOBAL_H
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include "TestGroup.h"
|
#include "TestGroup.h"
|
||||||
#include "TestGlobal.h"
|
#include "TestGlobal.h"
|
||||||
|
#include "stub/TestClock.h"
|
||||||
|
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
|
|
||||||
@ -26,6 +27,11 @@
|
|||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestGroup)
|
QTEST_GUILESS_MAIN(TestGroup)
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TestClock* m_clock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void TestGroup::initTestCase()
|
void TestGroup::initTestCase()
|
||||||
{
|
{
|
||||||
qRegisterMetaType<Entry*>("Entry*");
|
qRegisterMetaType<Entry*>("Entry*");
|
||||||
@ -33,6 +39,19 @@ void TestGroup::initTestCase()
|
|||||||
QVERIFY(Crypto::init());
|
QVERIFY(Crypto::init());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestGroup::init()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_clock == nullptr);
|
||||||
|
m_clock = new TestClock(2010, 5, 5, 10, 30, 10);
|
||||||
|
TestClock::setup(m_clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestGroup::cleanup()
|
||||||
|
{
|
||||||
|
TestClock::teardown();
|
||||||
|
m_clock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void TestGroup::testParenting()
|
void TestGroup::testParenting()
|
||||||
{
|
{
|
||||||
Database* db = new Database();
|
Database* db = new Database();
|
||||||
@ -389,7 +408,7 @@ void TestGroup::testClone()
|
|||||||
QVERIFY(clonedGroupNewUuid->uuid() != originalGroup->uuid());
|
QVERIFY(clonedGroupNewUuid->uuid() != originalGroup->uuid());
|
||||||
|
|
||||||
// Making sure the new modification date is not the same.
|
// Making sure the new modification date is not the same.
|
||||||
QTest::qSleep(1);
|
m_clock->advanceSecond(1);
|
||||||
|
|
||||||
QScopedPointer<Group> clonedGroupResetTimeInfo(
|
QScopedPointer<Group> clonedGroupResetTimeInfo(
|
||||||
originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid | Group::CloneResetTimeInfo));
|
originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid | Group::CloneResetTimeInfo));
|
||||||
@ -474,7 +493,7 @@ void TestGroup::testFindEntry()
|
|||||||
|
|
||||||
Entry* entry;
|
Entry* entry;
|
||||||
|
|
||||||
entry = db->rootGroup()->findEntry(entry1->uuid().toRfc4122().toHex());
|
entry = db->rootGroup()->findEntry(entry1->uuidToHex());
|
||||||
QVERIFY(entry != nullptr);
|
QVERIFY(entry != nullptr);
|
||||||
QCOMPARE(entry->title(), QString("entry1"));
|
QCOMPARE(entry->title(), QString("entry1"));
|
||||||
|
|
||||||
@ -491,7 +510,7 @@ void TestGroup::testFindEntry()
|
|||||||
entry = db->rootGroup()->findEntry(QString("//entry1"));
|
entry = db->rootGroup()->findEntry(QString("//entry1"));
|
||||||
QVERIFY(entry == nullptr);
|
QVERIFY(entry == nullptr);
|
||||||
|
|
||||||
entry = db->rootGroup()->findEntry(entry2->uuid().toRfc4122().toHex());
|
entry = db->rootGroup()->findEntry(entry2->uuidToHex());
|
||||||
QVERIFY(entry != nullptr);
|
QVERIFY(entry != nullptr);
|
||||||
QCOMPARE(entry->title(), QString("entry2"));
|
QCOMPARE(entry->title(), QString("entry2"));
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ class TestGroup : public QObject
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
|
void init();
|
||||||
|
void cleanup();
|
||||||
void testParenting();
|
void testParenting();
|
||||||
void testSignals();
|
void testSignals();
|
||||||
void testEntries();
|
void testEntries();
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "TestKeePass2Format.h"
|
#include "TestKeePass2Format.h"
|
||||||
#include "TestGlobal.h"
|
#include "TestGlobal.h"
|
||||||
|
#include "stub/TestClock.h"
|
||||||
|
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
#include "crypto/Crypto.h"
|
#include "crypto/Crypto.h"
|
||||||
@ -77,14 +78,14 @@ void TestKeePass2Format::testXmlMetadata()
|
|||||||
{
|
{
|
||||||
QCOMPARE(m_xmlDb->metadata()->generator(), QString("KeePass"));
|
QCOMPARE(m_xmlDb->metadata()->generator(), QString("KeePass"));
|
||||||
QCOMPARE(m_xmlDb->metadata()->name(), QString("ANAME"));
|
QCOMPARE(m_xmlDb->metadata()->name(), QString("ANAME"));
|
||||||
QCOMPARE(m_xmlDb->metadata()->nameChanged(), Test::datetime(2010, 8, 8, 17, 24, 53));
|
QCOMPARE(m_xmlDb->metadata()->nameChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 53));
|
||||||
QCOMPARE(m_xmlDb->metadata()->description(), QString("ADESC"));
|
QCOMPARE(m_xmlDb->metadata()->description(), QString("ADESC"));
|
||||||
QCOMPARE(m_xmlDb->metadata()->descriptionChanged(), Test::datetime(2010, 8, 8, 17, 27, 12));
|
QCOMPARE(m_xmlDb->metadata()->descriptionChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 27, 12));
|
||||||
QCOMPARE(m_xmlDb->metadata()->defaultUserName(), QString("DEFUSERNAME"));
|
QCOMPARE(m_xmlDb->metadata()->defaultUserName(), QString("DEFUSERNAME"));
|
||||||
QCOMPARE(m_xmlDb->metadata()->defaultUserNameChanged(), Test::datetime(2010, 8, 8, 17, 27, 45));
|
QCOMPARE(m_xmlDb->metadata()->defaultUserNameChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 27, 45));
|
||||||
QCOMPARE(m_xmlDb->metadata()->maintenanceHistoryDays(), 127);
|
QCOMPARE(m_xmlDb->metadata()->maintenanceHistoryDays(), 127);
|
||||||
QCOMPARE(m_xmlDb->metadata()->color(), QColor(0xff, 0xef, 0x00));
|
QCOMPARE(m_xmlDb->metadata()->color(), QColor(0xff, 0xef, 0x00));
|
||||||
QCOMPARE(m_xmlDb->metadata()->masterKeyChanged(), Test::datetime(2012, 4, 5, 17, 9, 34));
|
QCOMPARE(m_xmlDb->metadata()->masterKeyChanged(), TestClock::datetimeUtc(2012, 4, 5, 17, 9, 34));
|
||||||
QCOMPARE(m_xmlDb->metadata()->masterKeyChangeRec(), 101);
|
QCOMPARE(m_xmlDb->metadata()->masterKeyChangeRec(), 101);
|
||||||
QCOMPARE(m_xmlDb->metadata()->masterKeyChangeForce(), -1);
|
QCOMPARE(m_xmlDb->metadata()->masterKeyChangeForce(), -1);
|
||||||
QCOMPARE(m_xmlDb->metadata()->protectTitle(), false);
|
QCOMPARE(m_xmlDb->metadata()->protectTitle(), false);
|
||||||
@ -95,9 +96,9 @@ void TestKeePass2Format::testXmlMetadata()
|
|||||||
QCOMPARE(m_xmlDb->metadata()->recycleBinEnabled(), true);
|
QCOMPARE(m_xmlDb->metadata()->recycleBinEnabled(), true);
|
||||||
QVERIFY(m_xmlDb->metadata()->recycleBin() != nullptr);
|
QVERIFY(m_xmlDb->metadata()->recycleBin() != nullptr);
|
||||||
QCOMPARE(m_xmlDb->metadata()->recycleBin()->name(), QString("Recycle Bin"));
|
QCOMPARE(m_xmlDb->metadata()->recycleBin()->name(), QString("Recycle Bin"));
|
||||||
QCOMPARE(m_xmlDb->metadata()->recycleBinChanged(), Test::datetime(2010, 8, 25, 16, 12, 57));
|
QCOMPARE(m_xmlDb->metadata()->recycleBinChanged(), TestClock::datetimeUtc(2010, 8, 25, 16, 12, 57));
|
||||||
QVERIFY(m_xmlDb->metadata()->entryTemplatesGroup() == nullptr);
|
QVERIFY(m_xmlDb->metadata()->entryTemplatesGroup() == nullptr);
|
||||||
QCOMPARE(m_xmlDb->metadata()->entryTemplatesGroupChanged(), Test::datetime(2010, 8, 8, 17, 24, 19));
|
QCOMPARE(m_xmlDb->metadata()->entryTemplatesGroupChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 19));
|
||||||
QVERIFY(m_xmlDb->metadata()->lastSelectedGroup() != nullptr);
|
QVERIFY(m_xmlDb->metadata()->lastSelectedGroup() != nullptr);
|
||||||
QCOMPARE(m_xmlDb->metadata()->lastSelectedGroup()->name(), QString("NewDatabase"));
|
QCOMPARE(m_xmlDb->metadata()->lastSelectedGroup()->name(), QString("NewDatabase"));
|
||||||
QVERIFY(m_xmlDb->metadata()->lastTopVisibleGroup() == m_xmlDb->metadata()->lastSelectedGroup());
|
QVERIFY(m_xmlDb->metadata()->lastTopVisibleGroup() == m_xmlDb->metadata()->lastSelectedGroup());
|
||||||
@ -135,13 +136,13 @@ void TestKeePass2Format::testXmlGroupRoot()
|
|||||||
QCOMPARE(group->iconUuid(), QUuid());
|
QCOMPARE(group->iconUuid(), QUuid());
|
||||||
QVERIFY(group->isExpanded());
|
QVERIFY(group->isExpanded());
|
||||||
TimeInfo ti = group->timeInfo();
|
TimeInfo ti = group->timeInfo();
|
||||||
QCOMPARE(ti.lastModificationTime(), Test::datetime(2010, 8, 8, 17, 24, 27));
|
QCOMPARE(ti.lastModificationTime(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 27));
|
||||||
QCOMPARE(ti.creationTime(), Test::datetime(2010, 8, 7, 17, 24, 27));
|
QCOMPARE(ti.creationTime(), TestClock::datetimeUtc(2010, 8, 7, 17, 24, 27));
|
||||||
QCOMPARE(ti.lastAccessTime(), Test::datetime(2010, 8, 9, 9, 9, 44));
|
QCOMPARE(ti.lastAccessTime(), TestClock::datetimeUtc(2010, 8, 9, 9, 9, 44));
|
||||||
QCOMPARE(ti.expiryTime(), Test::datetime(2010, 8, 8, 17, 24, 17));
|
QCOMPARE(ti.expiryTime(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 17));
|
||||||
QVERIFY(!ti.expires());
|
QVERIFY(!ti.expires());
|
||||||
QCOMPARE(ti.usageCount(), 52);
|
QCOMPARE(ti.usageCount(), 52);
|
||||||
QCOMPARE(ti.locationChanged(), Test::datetime(2010, 8, 8, 17, 24, 27));
|
QCOMPARE(ti.locationChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 27));
|
||||||
QCOMPARE(group->defaultAutoTypeSequence(), QString(""));
|
QCOMPARE(group->defaultAutoTypeSequence(), QString(""));
|
||||||
QCOMPARE(group->autoTypeEnabled(), Group::Inherit);
|
QCOMPARE(group->autoTypeEnabled(), Group::Inherit);
|
||||||
QCOMPARE(group->searchingEnabled(), Group::Inherit);
|
QCOMPARE(group->searchingEnabled(), Group::Inherit);
|
||||||
@ -202,13 +203,13 @@ void TestKeePass2Format::testXmlEntry1()
|
|||||||
QCOMPARE(entry->tags(), QString("a b c"));
|
QCOMPARE(entry->tags(), QString("a b c"));
|
||||||
|
|
||||||
const TimeInfo ti = entry->timeInfo();
|
const TimeInfo ti = entry->timeInfo();
|
||||||
QCOMPARE(ti.lastModificationTime(), Test::datetime(2010, 8, 25, 16, 19, 25));
|
QCOMPARE(ti.lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 19, 25));
|
||||||
QCOMPARE(ti.creationTime(), Test::datetime(2010, 8, 25, 16, 13, 54));
|
QCOMPARE(ti.creationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54));
|
||||||
QCOMPARE(ti.lastAccessTime(), Test::datetime(2010, 8, 25, 16, 19, 25));
|
QCOMPARE(ti.lastAccessTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 19, 25));
|
||||||
QCOMPARE(ti.expiryTime(), Test::datetime(2010, 8, 25, 16, 12, 57));
|
QCOMPARE(ti.expiryTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 12, 57));
|
||||||
QVERIFY(!ti.expires());
|
QVERIFY(!ti.expires());
|
||||||
QCOMPARE(ti.usageCount(), 8);
|
QCOMPARE(ti.usageCount(), 8);
|
||||||
QCOMPARE(ti.locationChanged(), Test::datetime(2010, 8, 25, 16, 13, 54));
|
QCOMPARE(ti.locationChanged(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54));
|
||||||
|
|
||||||
QList<QString> attrs = entry->attributes()->keys();
|
QList<QString> attrs = entry->attributes()->keys();
|
||||||
QCOMPARE(entry->attributes()->value("Notes"), QString("Notes"));
|
QCOMPARE(entry->attributes()->value("Notes"), QString("Notes"));
|
||||||
@ -307,7 +308,7 @@ void TestKeePass2Format::testXmlEntryHistory()
|
|||||||
const Entry* entry = entryMain->historyItems().at(0);
|
const Entry* entry = entryMain->historyItems().at(0);
|
||||||
QCOMPARE(entry->uuid(), entryMain->uuid());
|
QCOMPARE(entry->uuid(), entryMain->uuid());
|
||||||
QVERIFY(!entry->parent());
|
QVERIFY(!entry->parent());
|
||||||
QCOMPARE(entry->timeInfo().lastModificationTime(), Test::datetime(2010, 8, 25, 16, 13, 54));
|
QCOMPARE(entry->timeInfo().lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54));
|
||||||
QCOMPARE(entry->timeInfo().usageCount(), 3);
|
QCOMPARE(entry->timeInfo().usageCount(), 3);
|
||||||
QCOMPARE(entry->title(), QString("Sample Entry"));
|
QCOMPARE(entry->title(), QString("Sample Entry"));
|
||||||
QCOMPARE(entry->url(), QString("http://www.somesite.com/"));
|
QCOMPARE(entry->url(), QString("http://www.somesite.com/"));
|
||||||
@ -317,7 +318,7 @@ void TestKeePass2Format::testXmlEntryHistory()
|
|||||||
const Entry* entry = entryMain->historyItems().at(1);
|
const Entry* entry = entryMain->historyItems().at(1);
|
||||||
QCOMPARE(entry->uuid(), entryMain->uuid());
|
QCOMPARE(entry->uuid(), entryMain->uuid());
|
||||||
QVERIFY(!entry->parent());
|
QVERIFY(!entry->parent());
|
||||||
QCOMPARE(entry->timeInfo().lastModificationTime(), Test::datetime(2010, 8, 25, 16, 15, 43));
|
QCOMPARE(entry->timeInfo().lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 15, 43));
|
||||||
QCOMPARE(entry->timeInfo().usageCount(), 7);
|
QCOMPARE(entry->timeInfo().usageCount(), 7);
|
||||||
QCOMPARE(entry->title(), QString("Sample Entry 1"));
|
QCOMPARE(entry->title(), QString("Sample Entry 1"));
|
||||||
QCOMPARE(entry->url(), QString("http://www.somesite.com/"));
|
QCOMPARE(entry->url(), QString("http://www.somesite.com/"));
|
||||||
@ -331,11 +332,11 @@ void TestKeePass2Format::testXmlDeletedObjects()
|
|||||||
|
|
||||||
delObj = objList.takeFirst();
|
delObj = objList.takeFirst();
|
||||||
QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("5K/bzWCSmkCv5OZxYl4N/w==")));
|
QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("5K/bzWCSmkCv5OZxYl4N/w==")));
|
||||||
QCOMPARE(delObj.deletionTime, Test::datetime(2010, 8, 25, 16, 14, 12));
|
QCOMPARE(delObj.deletionTime, TestClock::datetimeUtc(2010, 8, 25, 16, 14, 12));
|
||||||
|
|
||||||
delObj = objList.takeFirst();
|
delObj = objList.takeFirst();
|
||||||
QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("80h8uSNWgkKhKCp1TgXF7g==")));
|
QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("80h8uSNWgkKhKCp1TgXF7g==")));
|
||||||
QCOMPARE(delObj.deletionTime, Test::datetime(2010, 8, 25, 16, 14, 14));
|
QCOMPARE(delObj.deletionTime, TestClock::datetimeUtc(2010, 8, 25, 16, 14, 14));
|
||||||
|
|
||||||
QVERIFY(objList.isEmpty());
|
QVERIFY(objList.isEmpty());
|
||||||
}
|
}
|
||||||
|
1347
tests/TestMerge.cpp
1347
tests/TestMerge.cpp
File diff suppressed because it is too large
Load Diff
@ -19,20 +19,33 @@
|
|||||||
#define KEEPASSX_TESTMERGE_H
|
#define KEEPASSX_TESTMERGE_H
|
||||||
|
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QMap>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class TestMerge : public QObject
|
class TestMerge : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
|
void init();
|
||||||
|
void cleanup();
|
||||||
void testMergeIntoNew();
|
void testMergeIntoNew();
|
||||||
void testMergeNoChanges();
|
void testMergeNoChanges();
|
||||||
void testResolveConflictNewer();
|
void testResolveConflictNewer();
|
||||||
void testResolveConflictOlder();
|
void testResolveConflictExisting();
|
||||||
void testResolveGroupConflictOlder();
|
void testResolveGroupConflictOlder();
|
||||||
void testResolveConflictKeepBoth();
|
void testResolveConflictDuplicate();
|
||||||
|
void testResolveConflictEntry_Synchronize();
|
||||||
|
void testResolveConflictEntry_KeepLocal();
|
||||||
|
void testResolveConflictEntry_KeepRemote();
|
||||||
|
void testResolveConflictEntry_KeepNewer();
|
||||||
|
void testDeletionConflictEntry_Duplicate();
|
||||||
|
void testDeletionConflictEntry_Synchronized();
|
||||||
|
void testDeletionConflictEntry_KeepLocal();
|
||||||
|
void testDeletionConflictEntry_KeepRemote();
|
||||||
|
void testDeletionConflictEntry_KeepNewer();
|
||||||
void testMoveEntry();
|
void testMoveEntry();
|
||||||
void testMoveEntryPreserveChanges();
|
void testMoveEntryPreserveChanges();
|
||||||
void testMoveEntryIntoNewGroup();
|
void testMoveEntryIntoNewGroup();
|
||||||
@ -42,9 +55,24 @@ private slots:
|
|||||||
void testUpdateGroupLocation();
|
void testUpdateGroupLocation();
|
||||||
void testMergeAndSync();
|
void testMergeAndSync();
|
||||||
void testMergeCustomIcons();
|
void testMergeCustomIcons();
|
||||||
|
void testMetadata();
|
||||||
|
void testDeletedEntry();
|
||||||
|
void testDeletedGroup();
|
||||||
|
void testDeletedRevertedEntry();
|
||||||
|
void testDeletedRevertedGroup();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Database* createTestDatabase();
|
Database* createTestDatabase();
|
||||||
|
Database* createTestDatabaseStructureClone(Database* source, int entryFlags, int groupFlags);
|
||||||
|
void testResolveConflictTemplate(int mergeMode, std::function<void(Database*, const QMap<const char*, QDateTime>&)> verification);
|
||||||
|
void testDeletionConflictTemplate(int mergeMode, std::function<void(Database*, const QMap<QString, QUuid>&)> verification);
|
||||||
|
static void assertDeletionNewerOnly(Database *db, const QMap<QString, QUuid> &identifiers);
|
||||||
|
static void assertDeletionLocalOnly(Database *db, const QMap<QString, QUuid> &identifiers);
|
||||||
|
static void assertUpdateMergedEntry1(Entry *entry, const QMap<const char*, QDateTime> ×tamps);
|
||||||
|
static void assertUpdateReappliedEntry2(Entry *entry, const QMap<const char*, QDateTime> ×tamps);
|
||||||
|
static void assertUpdateReappliedEntry1(Entry *entry, const QMap<const char*, QDateTime> ×tamps);
|
||||||
|
static void assertUpdateMergedEntry2(Entry *entry, const QMap<const char *, QDateTime> ×tamps);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TESTMERGE_H
|
#endif // KEEPASSX_TESTMERGE_H
|
||||||
|
@ -16,10 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "TestModified.h"
|
#include "TestModified.h"
|
||||||
|
#include "stub/TestClock.h"
|
||||||
|
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
@ -27,11 +29,29 @@
|
|||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestModified)
|
QTEST_GUILESS_MAIN(TestModified)
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TestClock* m_clock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void TestModified::initTestCase()
|
void TestModified::initTestCase()
|
||||||
{
|
{
|
||||||
QVERIFY(Crypto::init());
|
QVERIFY(Crypto::init());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestModified::init()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_clock == nullptr);
|
||||||
|
m_clock = new TestClock(2010, 5, 5, 10, 30, 10);
|
||||||
|
TestClock::setup(m_clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestModified::cleanup()
|
||||||
|
{
|
||||||
|
TestClock::teardown();
|
||||||
|
m_clock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void TestModified::testSignals()
|
void TestModified::testSignals()
|
||||||
{
|
{
|
||||||
int spyCount = 0;
|
int spyCount = 0;
|
||||||
@ -230,7 +250,7 @@ void TestModified::testEntrySets()
|
|||||||
entry->setExpires(entry->timeInfo().expires());
|
entry->setExpires(entry->timeInfo().expires());
|
||||||
QCOMPARE(spyModified.count(), spyCount);
|
QCOMPARE(spyModified.count(), spyCount);
|
||||||
|
|
||||||
entry->setExpiryTime(QDateTime::currentDateTimeUtc().addYears(1));
|
entry->setExpiryTime(Clock::currentDateTimeUtc().addYears(1));
|
||||||
QCOMPARE(spyModified.count(), ++spyCount);
|
QCOMPARE(spyModified.count(), ++spyCount);
|
||||||
entry->setExpiryTime(entry->timeInfo().expiryTime());
|
entry->setExpiryTime(entry->timeInfo().expiryTime());
|
||||||
QCOMPARE(spyModified.count(), spyCount);
|
QCOMPARE(spyModified.count(), spyCount);
|
||||||
@ -300,7 +320,7 @@ void TestModified::testHistoryItems()
|
|||||||
QCOMPARE(entry->historyItems().size(), historyItemsSize);
|
QCOMPARE(entry->historyItems().size(), historyItemsSize);
|
||||||
|
|
||||||
QDateTime modified = entry->timeInfo().lastModificationTime();
|
QDateTime modified = entry->timeInfo().lastModificationTime();
|
||||||
QTest::qSleep(10);
|
m_clock->advanceSecond(10);
|
||||||
entry->beginUpdate();
|
entry->beginUpdate();
|
||||||
entry->setTitle("b");
|
entry->setTitle("b");
|
||||||
entry->endUpdate();
|
entry->endUpdate();
|
||||||
|
@ -26,6 +26,8 @@ class TestModified : public QObject
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
|
void init();
|
||||||
|
void cleanup();
|
||||||
void testSignals();
|
void testSignals();
|
||||||
void testGroupSets();
|
void testGroupSets();
|
||||||
void testEntrySets();
|
void testEntrySets();
|
||||||
|
@ -92,7 +92,6 @@ void TestGui::initTestCase()
|
|||||||
Tools::wait(50);
|
Tools::wait(50);
|
||||||
|
|
||||||
// Load the NewDatabase.kdbx file into temporary storage
|
// Load the NewDatabase.kdbx file into temporary storage
|
||||||
QByteArray tmpData;
|
|
||||||
QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
|
QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
|
||||||
QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
|
QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
|
||||||
QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
|
QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
|
||||||
@ -292,17 +291,17 @@ void TestGui::testAutoreloadDatabase()
|
|||||||
config()->set("AutoReloadOnChange", false);
|
config()->set("AutoReloadOnChange", false);
|
||||||
|
|
||||||
// Load the MergeDatabase.kdbx file into temporary storage
|
// Load the MergeDatabase.kdbx file into temporary storage
|
||||||
QByteArray tmpData;
|
QByteArray unmodifiedMergeDatabase;
|
||||||
QFile mergeDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
|
QFile mergeDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
|
||||||
QVERIFY(mergeDbFile.open(QIODevice::ReadOnly));
|
QVERIFY(mergeDbFile.open(QIODevice::ReadOnly));
|
||||||
QVERIFY(Tools::readAllFromDevice(&mergeDbFile, tmpData));
|
QVERIFY(Tools::readAllFromDevice(&mergeDbFile, unmodifiedMergeDatabase));
|
||||||
mergeDbFile.close();
|
mergeDbFile.close();
|
||||||
|
|
||||||
// Test accepting new file in autoreload
|
// Test accepting new file in autoreload
|
||||||
MessageBox::setNextAnswer(QMessageBox::Yes);
|
MessageBox::setNextAnswer(QMessageBox::Yes);
|
||||||
// Overwrite the current database with the temp data
|
// Overwrite the current database with the temp data
|
||||||
QVERIFY(m_dbFile.open());
|
QVERIFY(m_dbFile.open());
|
||||||
QVERIFY(m_dbFile.write(tmpData, static_cast<qint64>(tmpData.size())));
|
QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(unmodifiedMergeDatabase.size())));
|
||||||
m_dbFile.close();
|
m_dbFile.close();
|
||||||
Tools::wait(1500);
|
Tools::wait(1500);
|
||||||
|
|
||||||
@ -320,7 +319,7 @@ void TestGui::testAutoreloadDatabase()
|
|||||||
MessageBox::setNextAnswer(QMessageBox::No);
|
MessageBox::setNextAnswer(QMessageBox::No);
|
||||||
// Overwrite the current temp database with a new file
|
// Overwrite the current temp database with a new file
|
||||||
m_dbFile.open();
|
m_dbFile.open();
|
||||||
QVERIFY(m_dbFile.write(tmpData, static_cast<qint64>(tmpData.size())));
|
QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(unmodifiedMergeDatabase.size())));
|
||||||
m_dbFile.close();
|
m_dbFile.close();
|
||||||
Tools::wait(1500);
|
Tools::wait(1500);
|
||||||
|
|
||||||
@ -337,7 +336,6 @@ void TestGui::testAutoreloadDatabase()
|
|||||||
// Test accepting a merge of edits into autoreload
|
// Test accepting a merge of edits into autoreload
|
||||||
// Turn on autoload so we only get one messagebox (for the merge)
|
// Turn on autoload so we only get one messagebox (for the merge)
|
||||||
config()->set("AutoReloadOnChange", true);
|
config()->set("AutoReloadOnChange", true);
|
||||||
|
|
||||||
// Modify some entries
|
// Modify some entries
|
||||||
testEditEntry();
|
testEditEntry();
|
||||||
|
|
||||||
@ -345,7 +343,7 @@ void TestGui::testAutoreloadDatabase()
|
|||||||
MessageBox::setNextAnswer(QMessageBox::Yes);
|
MessageBox::setNextAnswer(QMessageBox::Yes);
|
||||||
// Overwrite the current database with the temp data
|
// Overwrite the current database with the temp data
|
||||||
QVERIFY(m_dbFile.open());
|
QVERIFY(m_dbFile.open());
|
||||||
QVERIFY(m_dbFile.write(tmpData, static_cast<qint64>(tmpData.size())));
|
QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(unmodifiedMergeDatabase.size())));
|
||||||
m_dbFile.close();
|
m_dbFile.close();
|
||||||
Tools::wait(1500);
|
Tools::wait(1500);
|
||||||
|
|
||||||
|
86
tests/stub/TestClock.cpp
Normal file
86
tests/stub/TestClock.cpp
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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 "TestClock.h"
|
||||||
|
|
||||||
|
TestClock::TestClock(int year, int month, int day, int hour, int min, int second)
|
||||||
|
: Clock()
|
||||||
|
, m_utcCurrent(datetimeUtc(year, month, day, hour, min, second))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TestClock::TestClock(QDateTime utcBase)
|
||||||
|
: Clock()
|
||||||
|
, m_utcCurrent(utcBase)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceSecond(int seconds)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addSecs(seconds);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceMinute(int minutes)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addSecs(minutes * 60);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceHour(int hours)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addSecs(hours * 60 * 60);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceDay(int days)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addDays(days);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceMonth(int months)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addMonths(months);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDateTime& TestClock::advanceYear(int years)
|
||||||
|
{
|
||||||
|
m_utcCurrent = m_utcCurrent.addYears(years);
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestClock::setup(Clock* clock)
|
||||||
|
{
|
||||||
|
Clock::setInstance(clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestClock::teardown()
|
||||||
|
{
|
||||||
|
Clock::resetInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime TestClock::currentDateTimeUtcImpl() const
|
||||||
|
{
|
||||||
|
return m_utcCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime TestClock::currentDateTimeImpl() const
|
||||||
|
{
|
||||||
|
return m_utcCurrent.toLocalTime();
|
||||||
|
}
|
50
tests/stub/TestClock.h
Normal file
50
tests/stub/TestClock.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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_TESTCLOCK_H
|
||||||
|
#define KEEPASSXC_TESTCLOCK_H
|
||||||
|
|
||||||
|
#include "core/Clock.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
class TestClock : public Clock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TestClock(int year, int month, int day, int hour, int min, int second);
|
||||||
|
|
||||||
|
TestClock(QDateTime utcBase = QDateTime::currentDateTimeUtc());
|
||||||
|
|
||||||
|
const QDateTime& advanceSecond(int seconds);
|
||||||
|
const QDateTime& advanceMinute(int minutes);
|
||||||
|
const QDateTime& advanceHour(int hours);
|
||||||
|
const QDateTime& advanceDay(int days);
|
||||||
|
const QDateTime& advanceMonth(int months);
|
||||||
|
const QDateTime& advanceYear(int years);
|
||||||
|
|
||||||
|
static void setup(Clock* clock);
|
||||||
|
static void teardown();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QDateTime currentDateTimeUtcImpl() const;
|
||||||
|
QDateTime currentDateTimeImpl() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDateTime m_utcCurrent;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSXC_TESTCLOCK_H
|
Loading…
x
Reference in New Issue
Block a user