Merge 4efb857652ab09f3e7841f89ea7bce129b1c08bc into 31c0b2389007f85c03ae73870e3d6f2a648fa67b

This commit is contained in:
vuurvli3g 2025-03-11 15:53:23 +01:00 committed by GitHub
commit 66ea85fcdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 620 additions and 80 deletions

View File

@ -990,10 +990,15 @@ Entry* Entry::clone(CloneFlags flags) const
if (flags & CloneResetTimeInfo) {
QDateTime now = Clock::currentDateTimeUtc();
entry->m_data.timeInfo.setCreationTime(now);
entry->m_data.timeInfo.setLastModificationTime(now);
entry->m_data.timeInfo.setLastAccessTime(now);
entry->m_data.timeInfo.setLocationChanged(now);
if (flags & CloneResetCreationTime) {
entry->m_data.timeInfo.setCreationTime(now);
}
if (flags & CloneResetLastAccessTime) {
entry->m_data.timeInfo.setLastAccessTime(now);
}
if (flags & CloneResetLocationChangedTime) {
entry->m_data.timeInfo.setLocationChanged(now);
}
}
if (flags & CloneRenameTitle) {
@ -1370,10 +1375,8 @@ void Entry::setGroup(Group* group, bool trackPrevious)
m_group->database()->addDeletedObject(m_uuid);
// copy custom icon to the new database
if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid())
&& !group->database()->metadata()->hasCustomIcon(iconUuid())) {
group->database()->metadata()->addCustomIcon(iconUuid(),
m_group->database()->metadata()->customIcon(iconUuid()));
if (group->database()) {
group->database()->metadata()->copyCustomIcon(iconUuid(), m_group->database()->metadata());
}
} else if (trackPrevious && m_group->database() && group != m_group) {
setPreviousParentGroup(m_group);
@ -1596,7 +1599,10 @@ QUuid Entry::previousParentGroupUuid() const
void Entry::setPreviousParentGroupUuid(const QUuid& uuid)
{
bool prevUpdateTimeinfo = m_updateTimeinfo;
m_updateTimeinfo = false; // prevent update of LastModificationTime
set(m_data.previousParentGroupUuid, uuid);
m_updateTimeinfo = prevUpdateTimeinfo;
}
void Entry::setPreviousParentGroup(const Group* group)

View File

@ -182,13 +182,18 @@ public:
{
CloneNoFlags = 0,
CloneNewUuid = 1, // generate a random uuid for the clone
CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time
CloneIncludeHistory = 4, // clone the history items
CloneResetCreationTime = 2, // set timeInfo.CreationTime to the current time
CloneResetLastAccessTime = 4, // set timeInfo.LastAccessTime to the current time
CloneResetLocationChangedTime = 8, // set timeInfo.LocationChangedTime to the current time
CloneIncludeHistory = 16, // clone the history items
CloneRenameTitle = 32, // add "-Clone" after the original title
CloneUserAsRef = 64, // Add the user as a reference to the original entry
ClonePassAsRef = 128, // Add the password as a reference to the original entry
CloneResetTimeInfo = CloneResetCreationTime | CloneResetLastAccessTime | CloneResetLocationChangedTime,
CloneExactCopy = CloneIncludeHistory,
CloneCopy = CloneExactCopy | CloneNewUuid | CloneResetTimeInfo,
CloneDefault = CloneNewUuid | CloneResetTimeInfo,
CloneCopy = CloneNewUuid | CloneResetTimeInfo | CloneIncludeHistory,
CloneRenameTitle = 8, // add "-Clone" after the original title
CloneUserAsRef = 16, // Add the user as a reference to the original entry
ClonePassAsRef = 32, // Add the password as a reference to the original entry
};
Q_DECLARE_FLAGS(CloneFlags, CloneFlag)

View File

@ -77,11 +77,11 @@ Group::~Group()
cleanupParent();
}
template <class P, class V> inline bool Group::set(P& property, const V& value)
template <class P, class V> inline bool Group::set(P& property, const V& value, bool preserveTimeinfo)
{
if (property != value) {
property = value;
emitModified();
emitModifiedEx(preserveTimeinfo);
return true;
} else {
return false;
@ -454,6 +454,15 @@ const Group* Group::parentGroup() const
return m_parent;
}
void Group::emitModifiedEx(bool preserveTimeinfo) {
bool prevUpdateTimeinfo = m_updateTimeinfo;
if (preserveTimeinfo) {
m_updateTimeinfo = false; // prevent update of LastModificationTime
}
emitModified();
m_updateTimeinfo = prevUpdateTimeinfo;
}
void Group::setParent(Group* parent, int index, bool trackPrevious)
{
Q_ASSERT(parent);
@ -483,9 +492,8 @@ void Group::setParent(Group* parent, int index, bool trackPrevious)
recCreateDelObjects();
// copy custom icon to the new database
if (!iconUuid().isNull() && parent->m_db && m_db->metadata()->hasCustomIcon(iconUuid())
&& !parent->m_db->metadata()->hasCustomIcon(iconUuid())) {
parent->m_db->metadata()->addCustomIcon(iconUuid(), m_db->metadata()->customIcon(iconUuid()));
if (parent->m_db) {
parent->m_db->metadata()->copyCustomIcon(iconUuid(), m_db->metadata());
}
}
if (m_db != parent->m_db) {
@ -511,7 +519,7 @@ void Group::setParent(Group* parent, int index, bool trackPrevious)
m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
}
emitModified();
emitModifiedEx(true);
if (!moveWithinDatabase) {
emit groupAdded();
@ -564,6 +572,16 @@ bool Group::hasChildren() const
return !children().isEmpty();
}
bool Group::isDescendantOf(const Group* group) const
{
for(const Group* parent = m_parent; parent; parent = parent->m_parent) {
if (parent == group) {
return true;
}
}
return false;
}
Database* Group::database()
{
return m_db;
@ -960,12 +978,16 @@ Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags)
clonedGroup->setUpdateTimeinfo(true);
if (groupFlags & Group::CloneResetTimeInfo) {
QDateTime now = Clock::currentDateTimeUtc();
clonedGroup->m_data.timeInfo.setCreationTime(now);
clonedGroup->m_data.timeInfo.setLastModificationTime(now);
clonedGroup->m_data.timeInfo.setLastAccessTime(now);
clonedGroup->m_data.timeInfo.setLocationChanged(now);
if (groupFlags & Group::CloneResetCreationTime) {
clonedGroup->m_data.timeInfo.setCreationTime(now);
}
if (groupFlags & Group::CloneResetLastAccessTime) {
clonedGroup->m_data.timeInfo.setLastAccessTime(now);
}
if (groupFlags & Group::CloneResetLocationChangedTime) {
clonedGroup->m_data.timeInfo.setLocationChanged(now);
}
}
if (groupFlags & Group::CloneRenameTitle) {
@ -997,7 +1019,7 @@ void Group::addEntry(Entry* entry)
connect(entry, &Entry::modified, m_db, &Database::markAsModified);
}
emitModified();
emitModifiedEx(true);
emit entryAdded(entry);
}
@ -1014,7 +1036,7 @@ void Group::removeEntry(Entry* entry)
entry->disconnect(m_db);
}
m_entries.removeAll(entry);
emitModified();
emitModifiedEx(true);
emit entryRemoved(entry);
}
@ -1085,7 +1107,7 @@ void Group::cleanupParent()
if (m_parent) {
emit groupAboutToRemove(this);
m_parent->m_children.removeAll(this);
emitModified();
emitModifiedEx(true);
emit groupRemoved();
}
}
@ -1236,7 +1258,7 @@ void Group::sortChildrenRecursively(bool reverse)
child->sortChildrenRecursively(reverse);
}
emitModified();
emitModifiedEx(true);
}
const Group* Group::previousParentGroup() const
@ -1254,7 +1276,7 @@ QUuid Group::previousParentGroupUuid() const
void Group::setPreviousParentGroupUuid(const QUuid& uuid)
{
set(m_data.previousParentGroupUuid, uuid);
set(m_data.previousParentGroupUuid, uuid, true);
}
void Group::setPreviousParentGroup(const Group* group)

View File

@ -20,11 +20,24 @@
#define KEEPASSX_GROUP_H
#include <QPointer>
#include <QList>
#include <utility>
#include "core/CustomData.h"
#include "core/Database.h"
#include "core/Entry.h"
class Entry;
class Group;
template <typename TCallable> concept CGroupVisitor = std::is_invocable_v<TCallable, Group*>;
template <typename TCallable> concept CGroupConstVisitor = std::is_invocable_v<TCallable, const Group*>;
template <typename TCallable> concept CEntryVisitor = std::is_invocable_v<TCallable, Entry*>;
template <typename TCallable> concept CEntryConstVisitor = std::is_invocable_v<TCallable, const Entry*>;
class Group : public ModifiableObject
{
Q_OBJECT
@ -47,10 +60,16 @@ public:
{
CloneNoFlags = 0,
CloneNewUuid = 1, // generate a random uuid for the clone
CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time
CloneIncludeEntries = 4, // clone the group entries
CloneDefault = CloneNewUuid | CloneResetTimeInfo | CloneIncludeEntries,
CloneRenameTitle = 8, // add "- Clone" after the original title
CloneResetCreationTime = 2, // set timeInfo.CreationTime to the current time
CloneResetLastAccessTime = 4, // set timeInfo.LastAccessTime to the current time
CloneResetLocationChangedTime = 8, // set timeInfo.LocationChangedTime to the current time
CloneIncludeEntries = 16, // clone the group entries
CloneRenameTitle = 32, // add "- Clone" after the original title
CloneResetTimeInfo = CloneResetCreationTime | CloneResetLastAccessTime | CloneResetLocationChangedTime,
CloneExactCopy = CloneIncludeEntries,
CloneCopy = CloneExactCopy | CloneNewUuid | CloneResetTimeInfo,
CloneDefault = CloneCopy,
};
Q_DECLARE_FLAGS(CloneFlags, CloneFlag)
@ -148,6 +167,7 @@ public:
void setParent(Group* parent, int index = -1, bool trackPrevious = true);
QStringList hierarchy(int height = -1) const;
bool hasChildren() const;
bool isDescendantOf(const Group* group) const;
Database* database();
const Database* database() const;
@ -159,6 +179,53 @@ public:
QList<Entry*> entriesRecursive(bool includeHistoryItems = false) const;
QList<const Group*> groupsRecursive(bool includeSelf) const;
QList<Group*> groupsRecursive(bool includeSelf);
/**
* Walk methods for traversing the tree (depth-first search)
*
* @param[in] includeSelf is the current group to be included or excluded
* if `false` the current group's entries will not be included either
* @param[in] groupVisitor functor that takes a single argument: ([const] Group*)
* the functor may return a bool to indicate whether to stop=`true` or continue=`false` traversing
* for a non-`bool` return-type the value is ignored and the traversing will continue as if `false` had been returned
* @param[in] entryVisitor functor that takes a single argument: ([const] Entry*)
* the functor may return a bool to indicate whether to stop=`true` or continue=`false` traversing
* for a non-`bool` return-type the value is ignored and the traversing will continue as if `false` had been returned
* @return `false` if the traversing completed without stop, or `true` otherwise
*/
template <CGroupVisitor TGroupCallable, CEntryVisitor TEntryCallable>
bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor)
{
return walk<TGroupCallable, TEntryCallable, false, true, true>(
includeSelf, std::forward<TGroupCallable>(groupVisitor), std::forward<TEntryCallable>(entryVisitor));
}
template <CGroupConstVisitor TGroupCallable, CEntryConstVisitor TEntryCallable>
bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const
{
return walk<TGroupCallable, TEntryCallable, true, true, true>(
includeSelf, std::forward<TGroupCallable>(groupVisitor), std::forward<TEntryCallable>(entryVisitor));
}
template <CGroupConstVisitor TGroupCallable> bool walkGroups(bool includeSelf, TGroupCallable&& groupVisitor) const
{
return walk<TGroupCallable, void*, true, true, false>(
includeSelf, std::forward<TGroupCallable>(groupVisitor), nullptr);
}
template <CGroupVisitor TGroupCallable> bool walkGroups(bool includeSelf, TGroupCallable&& groupVisitor)
{
return walk<TGroupCallable, void*, false, true, false>(
includeSelf, std::forward<TGroupCallable>(groupVisitor), nullptr);
}
template <CEntryConstVisitor TEntryCallable> bool walkEntries(TEntryCallable&& entryVisitor) const
{
return walk<void*, TEntryCallable, true, false, true>(
true, nullptr, std::forward<TEntryCallable>(entryVisitor));
}
template <CEntryVisitor TEntryCallable> bool walkEntries(TEntryCallable&& entryVisitor)
{
return walk<void*, TEntryCallable, false, false, true>(
true, nullptr, std::forward<TEntryCallable>(entryVisitor));
}
QSet<QUuid> customIconsRecursive() const;
QList<QString> usernamesRecursive(int topN = -1) const;
@ -204,8 +271,11 @@ private slots:
void updateTimeinfo();
private:
template <class P, class V> bool set(P& property, const V& value);
template <typename TGroupCallable, typename TEntryCallable, bool kIsConst, bool kVisitGroups, bool kVisitEntries>
bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const;
template <class P, class V> bool set(P& property, const V& value, bool preserveTimeinfo = false);
void emitModifiedEx(bool preserveTimeinfo);
void setParent(Database* db);
void connectDatabaseSignalsRecursive(Database* db);
@ -233,4 +303,55 @@ private:
Q_DECLARE_OPERATORS_FOR_FLAGS(Group::CloneFlags)
// helpers to support non-bool returning callables
template <bool kDefaultRetVal, typename TCallable, typename... Args>
bool visitorPredicateImpl(std::true_type, TCallable&& callable, Args&&... args)
{
return callable(std::forward<Args>(args)...);
}
template <bool kDefaultRetVal, typename TCallable, typename... Args>
bool visitorPredicateImpl(std::false_type, TCallable&& callable, Args&&... args)
{
callable(std::forward<Args>(args)...);
return kDefaultRetVal;
}
template <bool kDefaultRetVal, typename TCallable, typename... Args>
bool visitorPredicate(TCallable&& callable, Args&&... args)
{
using RetType = decltype(callable(args...));
return visitorPredicateImpl<kDefaultRetVal>(
std::is_same<RetType, bool>{}, std::forward<TCallable>(callable), std::forward<Args>(args)...);
}
template<typename TGroupCallable, typename TEntryCallable, bool kIsConst, bool kVisitGroups, bool kVisitEntries>
bool Group::walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const
{
using GroupType = typename std::conditional<kIsConst,const Group, Group>::type;
QList<Group*> groupsToVisit;
if (includeSelf) {
groupsToVisit.append(const_cast<Group*>(this));
} else {
groupsToVisit.append(m_children);
}
while (!groupsToVisit.isEmpty()) {
GroupType* group = groupsToVisit.takeLast(); // right-to-left
if constexpr (kVisitGroups) {
if (visitorPredicate<false>(groupVisitor, group)) {
return true;
}
}
if constexpr (kVisitEntries) {
for (auto* entry : group->m_entries) {
if (visitorPredicate<false>(entryVisitor, entry)) {
return true;
}
}
}
groupsToVisit.append(group->m_children);
}
return false;
}
#endif // KEEPASSX_GROUP_H

View File

@ -419,14 +419,21 @@ QUuid Metadata::findCustomIcon(const QByteArray& candidate)
return m_customIconsHashes.value(hash, QUuid());
}
void Metadata::copyCustomIcon(const QUuid& iconUuid, const Metadata* otherMetadata)
{
if (iconUuid.isNull()) {
return;
}
Q_ASSERT(otherMetadata->hasCustomIcon(iconUuid));
if (!hasCustomIcon(iconUuid) && otherMetadata->hasCustomIcon(iconUuid)) {
addCustomIcon(iconUuid, otherMetadata->customIcon(iconUuid));
}
}
void Metadata::copyCustomIcons(const QSet<QUuid>& iconList, const Metadata* otherMetadata)
{
for (const QUuid& uuid : iconList) {
Q_ASSERT(otherMetadata->hasCustomIcon(uuid));
if (!hasCustomIcon(uuid) && otherMetadata->hasCustomIcon(uuid)) {
addCustomIcon(uuid, otherMetadata->customIcon(uuid));
}
copyCustomIcon(uuid, otherMetadata);
}
}

View File

@ -138,6 +138,7 @@ public:
const QString& name = {},
const QDateTime& lastModified = {});
void removeCustomIcon(const QUuid& uuid);
void copyCustomIcon(const QUuid& iconUuid, const Metadata* otherMetadata);
void copyCustomIcons(const QSet<QUuid>& iconList, const Metadata* otherMetadata);
QUuid findCustomIcon(const QByteArray& candidate);
void setRecycleBinEnabled(bool value);

View File

@ -724,6 +724,10 @@ QList<DatabaseWidget*> MainWindow::getOpenDatabases()
return dbWidgets;
}
DatabaseWidget* MainWindow::currentDatabaseWidget() {
return m_ui->tabWidget->currentDatabaseWidget();
}
void MainWindow::showErrorMessage(const QString& message)
{
m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error);

View File

@ -52,6 +52,7 @@ public:
~MainWindow() override;
QList<DatabaseWidget*> getOpenDatabases();
DatabaseWidget* currentDatabaseWidget();
void restoreConfigState();
void setAllowScreenCapture(bool state);

View File

@ -466,7 +466,7 @@ Qt::DropActions EntryModel::supportedDropActions() const
Qt::DropActions EntryModel::supportedDragActions() const
{
return (Qt::MoveAction | Qt::CopyAction);
return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
}
Qt::ItemFlags EntryModel::flags(const QModelIndex& modelIndex) const

View File

@ -25,6 +25,7 @@
#include "core/Tools.h"
#include "gui/DatabaseIcons.h"
#include "gui/Icons.h"
#include "gui/MainWindow.h"
#include "keeshare/KeeShare.h"
GroupModel::GroupModel(Database* db, QObject* parent)
@ -180,7 +181,7 @@ Group* GroupModel::groupFromIndex(const QModelIndex& index) const
Qt::DropActions GroupModel::supportedDropActions() const
{
return Qt::MoveAction | Qt::CopyAction;
return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
}
Qt::ItemFlags GroupModel::flags(const QModelIndex& modelIndex) const
@ -204,9 +205,11 @@ bool GroupModel::dropMimeData(const QMimeData* data,
if (action == Qt::IgnoreAction) {
return true;
} else if (action != Qt::MoveAction && action != Qt::CopyAction && action != ::Qt::LinkAction) {
return false;
}
if (!data || (action != Qt::MoveAction && action != Qt::CopyAction) || !parent.isValid()) {
if (!data || !parent.isValid()) {
return false;
}
@ -223,6 +226,12 @@ bool GroupModel::dropMimeData(const QMimeData* data,
row = rowCount(parent);
}
auto showErrorMessage = [](const QString& errorMessage){
if(auto dbWidget = getMainWindow()->currentDatabaseWidget()) {
dbWidget->showErrorMessage(errorMessage);
}
};
// decode and insert
QByteArray encoded = data->data(isGroup ? types.at(0) : types.at(1));
QDataStream stream(&encoded, QIODevice::ReadOnly);
@ -234,17 +243,17 @@ bool GroupModel::dropMimeData(const QMimeData* data,
QUuid groupUuid;
stream >> dbUuid >> groupUuid;
Database* db = Database::databaseByUuid(dbUuid);
if (!db) {
Database* sourceDb = Database::databaseByUuid(dbUuid);
if (!sourceDb) {
return false;
}
Group* dragGroup = db->rootGroup()->findGroupByUuid(groupUuid);
if (!dragGroup || !db->rootGroup()->findGroupByUuid(dragGroup->uuid()) || dragGroup == db->rootGroup()) {
Group* dragGroup = sourceDb->rootGroup()->findGroupByUuid(groupUuid);
if (!dragGroup || dragGroup == sourceDb->rootGroup()) {
return false;
}
if (dragGroup == parentGroup || dragGroup->findGroupByUuid(parentGroup->uuid())) {
if (dragGroup == parentGroup || parentGroup->isDescendantOf(dragGroup)) {
return false;
}
@ -252,21 +261,64 @@ bool GroupModel::dropMimeData(const QMimeData* data,
row--;
}
Database* sourceDb = dragGroup->database();
Database* targetDb = parentGroup->database();
Group* group = dragGroup;
if (sourceDb != targetDb) {
QSet<QUuid> customIcons = group->customIconsRecursive();
targetDb->metadata()->copyCustomIcons(customIcons, sourceDb->metadata());
if (action == Qt::MoveAction || action == Qt::LinkAction) { // clang-format off
// Always clone the group across db's to reset UUIDs
group = dragGroup->clone(Entry::CloneDefault | Entry::CloneIncludeHistory);
if (action == Qt::MoveAction) {
// Remove the original group from the sourceDb
Group* binGroup = sourceDb->metadata()->recycleBin();
if(binGroup && binGroup->uuid() == dragGroup->uuid()) {
showErrorMessage(tr("Move error: \"%1\" group cannot be moved").arg(binGroup->name()));
return true;
}
// Collect all UUID(s) or short-circuit when UUID is deleted in targetDb
QSet<QUuid> uuidSet;
bool complexMove = group->walk(true,
[&](const Group* group) {
uuidSet.insert(group->uuid());
return targetDb->containsDeletedObject(group->uuid());
},
[&](const Entry* entry) {
uuidSet.insert(entry->uuid());
return targetDb->containsDeletedObject(entry->uuid());
}
);
// Unable to handle complex moves until the Merger interface supports single group/entry merging
if (complexMove || targetDb->rootGroup()->walk(true,
[&](const Group* group)-> bool {
return uuidSet.contains(group->uuid());
},
[&](const Entry* entry) -> bool {
return uuidSet.contains(entry->uuid());
}
)) {
showErrorMessage(tr("Move error: the group or one of it's descendants is already present in this database"));
return true;
}
} // clang-format on
if (action == Qt::MoveAction) { // -- Tracked move
// A clone with new UUID but original CreationTime
group = dragGroup->clone(Entry::CloneFlags(Entry::CloneCopy & ~Entry::CloneResetCreationTime),
Group::CloneFlags(Group::CloneCopy & ~Group::CloneResetCreationTime));
// Original UUID is marked as deleted to propagate the move to dbs that merge with this one
delete dragGroup;
} else if (action == Qt::LinkAction) { // -- Untracked move
QList<DeletedObject> deletedObjects(sourceDb->deletedObjects());
group = dragGroup->clone(Entry::CloneExactCopy, Group::CloneExactCopy);
delete dragGroup;
// Unmark UUID(s) as deleted by restoring the previous list
sourceDb->setDeletedObjects(deletedObjects);
} else {
group = dragGroup->clone(Entry::CloneCopy);
}
targetDb->metadata()->copyCustomIcons(group->customIconsRecursive(), sourceDb->metadata());
} else if (action == Qt::CopyAction) {
group = dragGroup->clone(Entry::CloneCopy);
}
@ -277,43 +329,69 @@ bool GroupModel::dropMimeData(const QMimeData* data,
return false;
}
int entries{0}, entriesNotMoved{0};
while (!stream.atEnd()) {
QUuid dbUuid;
QUuid entryUuid;
stream >> dbUuid >> entryUuid;
++entries;
Database* db = Database::databaseByUuid(dbUuid);
if (!db) {
Database* sourceDb = Database::databaseByUuid(dbUuid);
if (!sourceDb) {
continue;
}
Entry* dragEntry = db->rootGroup()->findEntryByUuid(entryUuid);
if (!dragEntry || !db->rootGroup()->findEntryByUuid(dragEntry->uuid())) {
Entry* dragEntry = sourceDb->rootGroup()->findEntryByUuid(entryUuid);
if (!dragEntry) {
continue;
}
Database* sourceDb = dragEntry->group()->database();
Database* targetDb = parentGroup->database();
Entry* entry = dragEntry;
if (sourceDb != targetDb) {
QUuid customIcon = entry->iconUuid();
if (!customIcon.isNull() && !targetDb->metadata()->hasCustomIcon(customIcon)) {
targetDb->metadata()->addCustomIcon(customIcon, sourceDb->metadata()->customIcon(customIcon).data);
if (action == Qt::MoveAction || action == Qt::LinkAction) { // clang-format off
// Unable to handle complex moves until the Merger interface supports single group/entry merging
if (targetDb->containsDeletedObject(dragEntry->uuid()) ||
targetDb->rootGroup()->walkEntries([=](const Entry* entry) {
return dragEntry->uuid() == entry->uuid();
}
)) {
++entriesNotMoved;
continue;
}
} // clang-format on
if (action == Qt::MoveAction) { // -- Tracked move
// A clone with new UUID but original CreationTime
entry = dragEntry->clone(Entry::CloneFlags(Entry::CloneCopy & ~Entry::CloneResetCreationTime));
// Original UUID is marked as deleted to propagate the move to dbs that merge with this one
delete dragEntry;
} else if (action == Qt::LinkAction) { // -- Untracked move
QList<DeletedObject> deletedObjects(sourceDb->deletedObjects());
entry = dragEntry->clone(Entry::CloneExactCopy);
delete dragEntry;
// Unmark UUID as deleted by restoring the previous list
sourceDb->setDeletedObjects(deletedObjects);
} else {
entry = dragEntry->clone(Entry::CloneCopy);
}
// Reset the UUID when moving across db boundary
entry = dragEntry->clone(Entry::CloneDefault | Entry::CloneIncludeHistory);
if (action == Qt::MoveAction) {
delete dragEntry;
}
targetDb->metadata()->copyCustomIcon(entry->iconUuid(), sourceDb->metadata());
} else if (action == Qt::CopyAction) {
entry = dragEntry->clone(Entry::CloneCopy);
}
entry->setGroup(parentGroup);
}
if (entriesNotMoved) {
showErrorMessage(
tr("Move error: %1 of %2 entry(s) are already present in this database").arg(entriesNotMoved).arg(entries));
}
}
return true;

View File

@ -24,11 +24,15 @@
#include "core/Config.h"
#include "core/Group.h"
#include "gui/group/GroupModel.h"
#include "gui/entry/EntryView.h"
#include "gui/DatabaseWidget.h"
GroupView::GroupView(Database* db, QWidget* parent)
: QTreeView(parent)
, m_model(new GroupModel(db, this))
, m_updatingExpanded(false)
, m_isDragEventSrcFromOtherDb(false)
, m_lastAcceptedDropAction(Qt::IgnoreAction)
{
QTreeView::setModel(m_model);
setHeaderHidden(true);
@ -96,20 +100,83 @@ void GroupView::changeDatabase(const QSharedPointer<Database>& newDb)
setColumnWidth(0, sizeHintForColumn(0));
}
void GroupView::dragMoveEvent(QDragMoveEvent* event)
void GroupView::dragEnterEvent(QDragEnterEvent *event)
{
if (event->keyboardModifiers() & Qt::ControlModifier) {
event->setDropAction(Qt::CopyAction);
} else {
event->setDropAction(Qt::MoveAction);
event->ignore(); // default to ignore
auto const eventSource = event->source();
// ignore events from other processes
if (!eventSource) {
return;
}
// ignore events with unsupported mime-types
auto supportedFormats = m_model->mimeTypes().toSet();
if (!supportedFormats.intersects(event->mimeData()->formats().toSet())) {
return;
}
auto firstAncestorOfTypeDatabaseWidget = [](QObject* object) -> DatabaseWidget* {
if (object) {
for (auto parent = object->parent(); parent; parent = parent->parent()) {
if (auto dbWidget = qobject_cast<DatabaseWidget*>(parent)) {
return dbWidget;
}
}
}
return nullptr;
};
m_isDragEventSrcFromOtherDb = false;
if (GroupView* view = qobject_cast<GroupView*>(eventSource)) {
m_isDragEventSrcFromOtherDb = view != this;
} else if (EntryView* view = qobject_cast<EntryView*>(eventSource)) {
auto targetDbWidget = firstAncestorOfTypeDatabaseWidget(this);
auto sourceDbWidget = firstAncestorOfTypeDatabaseWidget(view);
m_isDragEventSrcFromOtherDb = sourceDbWidget != targetDbWidget;
}
QTreeView::dragEnterEvent(event);
}
void GroupView::dragMoveEvent(QDragMoveEvent* event)
{
QTreeView::dragMoveEvent(event);
if (!event->isAccepted()) {
return;
}
// entries may only be dropped on groups
if (event->isAccepted() && event->mimeData()->hasFormat("application/x-keepassx-entry")
if (event->mimeData()->hasFormat("application/x-keepassx-entry")
&& (dropIndicatorPosition() == AboveItem || dropIndicatorPosition() == BelowItem)) {
event->ignore();
return;
}
// figure out which dropaction should be used
Qt::DropAction dropAction = Qt::MoveAction;
if (event->keyboardModifiers() & Qt::ControlModifier) {
dropAction = Qt::CopyAction;
} else if (event->keyboardModifiers() & Qt::AltModifier) {
dropAction = m_isDragEventSrcFromOtherDb ? Qt::LinkAction : Qt::IgnoreAction;
}
if (dropAction != Qt::IgnoreAction && event->possibleActions() & dropAction) {
event->setDropAction(dropAction);
m_lastAcceptedDropAction = event->dropAction();
} else {
event->ignore();
}
}
void GroupView::dropEvent(QDropEvent* event)
{
if (m_lastAcceptedDropAction != Qt::IgnoreAction) {
event->setDropAction(m_lastAcceptedDropAction);
QTreeView::dropEvent(event);
} else {
event->ignore();
}
}

View File

@ -50,7 +50,9 @@ private slots:
void selectNextGroup();
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dropEvent(QDropEvent* event) override;
void focusInEvent(QFocusEvent* event) override;
private:
@ -58,6 +60,8 @@ private:
GroupModel* const m_model;
bool m_updatingExpanded;
bool m_isDragEventSrcFromOtherDb;
Qt::DropAction m_lastAcceptedDropAction;
};
#endif // KEEPASSX_GROUPVIEW_H

View File

@ -168,7 +168,7 @@ if(WITH_XC_SSHAGENT)
endif()
add_unit_test(NAME testentry SOURCES TestEntry.cpp
LIBS ${TEST_LIBRARIES})
LIBS testsupport ${TEST_LIBRARIES})
add_unit_test(NAME testmerge SOURCES TestMerge.cpp
LIBS testsupport ${TEST_LIBRARIES})

View File

@ -25,13 +25,34 @@
#include "core/TimeInfo.h"
#include "crypto/Crypto.h"
#include "mock/MockClock.h"
QTEST_GUILESS_MAIN(TestEntry)
namespace
{
MockClock* m_clock = nullptr;
}
void TestEntry::initTestCase()
{
QVERIFY(Crypto::init());
}
void TestEntry::init()
{
Q_ASSERT(m_clock == nullptr);
m_clock = new MockClock(2010, 5, 5, 10, 30, 10);
MockClock::setup(m_clock);
}
void TestEntry::cleanup()
{
MockClock::teardown();
m_clock = nullptr;
}
void TestEntry::testHistoryItemDeletion()
{
QScopedPointer<Entry> entry(new Entry());
@ -110,6 +131,8 @@ void TestEntry::testClone()
QCOMPARE(entryCloneNewUuid->timeInfo().creationTime(), entryOrg->timeInfo().creationTime());
// Reset modification time
entryOrgTime.setLastAccessTime(Clock::datetimeUtc(60));
entryOrgTime.setLocationChanged(Clock::datetimeUtc(60));
entryOrgTime.setLastModificationTime(Clock::datetimeUtc(60));
entryOrg->setTimeInfo(entryOrgTime);
@ -123,7 +146,12 @@ void TestEntry::testClone()
QCOMPARE(entryCloneResetTime->uuid(), entryOrg->uuid());
QCOMPARE(entryCloneResetTime->title(), QString("New Title"));
QCOMPARE(entryCloneResetTime->historyItems().size(), 0);
// Cloning with CloneResetTimeInfo should affect the CreationTime, LocationChanged, LastAccessTime
QVERIFY(entryCloneResetTime->timeInfo().creationTime() != entryOrg->timeInfo().creationTime());
QVERIFY(entryCloneResetTime->timeInfo().locationChanged() != entryOrg->timeInfo().locationChanged());
QVERIFY(entryCloneResetTime->timeInfo().lastAccessTime() != entryOrg->timeInfo().lastAccessTime());
// Cloning with CloneResetTimeInfo should not affect the LastModificationTime
QCOMPARE(entryCloneResetTime->timeInfo().lastModificationTime(), entryOrg->timeInfo().lastModificationTime());
// Date back history of original entry
Entry* firstHistoryItem = entryOrg->historyItems()[0];
@ -851,3 +879,33 @@ void TestEntry::testPreviousParentGroup()
QVERIFY(entry->previousParentGroupUuid() == group1->uuid());
QVERIFY(entry->previousParentGroup() == group1);
}
void TestEntry::testTimeinfoChanges()
{
Database db;
auto* root = db.rootGroup();
auto* subgroup = new Group();
subgroup->setUuid(QUuid::createUuid());
subgroup->setParent(root);
QDateTime startTime = Clock::currentDateTimeUtc();
TimeInfo startTimeinfo;
startTimeinfo.setCreationTime(startTime);
startTimeinfo.setLastModificationTime(startTime);
startTimeinfo.setLocationChanged(startTime);
startTimeinfo.setLastAccessTime(startTime);
m_clock->advanceMinute(1);
QScopedPointer<Entry> entry(new Entry());
entry->setUuid(QUuid::createUuid());
entry->setGroup(root);
entry->setTimeInfo(startTimeinfo);
entry->setPreviousParentGroup(subgroup);
// setting previous parent group should not affect the LastModificationTime
QCOMPARE(entry->timeInfo().lastModificationTime(), startTime);
entry->setGroup(subgroup);
// changing group should not affect LastModicationTime, CreationTime
QCOMPARE(entry->timeInfo().creationTime(), startTime);
QCOMPARE(entry->timeInfo().lastModificationTime(), startTime);
// changing group should affect the LocationChanged time
QCOMPARE(entry->timeInfo().locationChanged(), Clock::currentDateTimeUtc());
}

View File

@ -28,6 +28,8 @@ class TestEntry : public QObject
private slots:
void initTestCase();
void init();
void cleanup();
void testHistoryItemDeletion();
void testCopyDataFrom();
void testClone();
@ -42,6 +44,7 @@ private slots:
void testIsRecycled();
void testMoveUpDown();
void testPreviousParentGroup();
void testTimeinfoChanges();
};
#endif // KEEPASSX_TESTENTRY_H

View File

@ -20,6 +20,7 @@
#include "mock/MockClock.h"
#include <QSet>
#include <QVector>
#include <QSignalSpy>
#include <QtTestGui>
@ -382,18 +383,21 @@ void TestGroup::testClone()
QCOMPARE(clonedGroup->iconNumber(), 42);
QCOMPARE(clonedGroup->children().size(), 1);
QCOMPARE(clonedGroup->entries().size(), 1);
QCOMPARE(clonedGroup->timeInfo(), originalGroup->timeInfo());
Entry* clonedGroupEntry = clonedGroup->entries().at(0);
QVERIFY(clonedGroupEntry->uuid() != originalGroupEntry->uuid());
QCOMPARE(clonedGroupEntry->title(), QString("GroupEntry"));
QCOMPARE(clonedGroupEntry->iconNumber(), 43);
QCOMPARE(clonedGroupEntry->historyItems().size(), 0);
QCOMPARE(clonedGroupEntry->timeInfo(), originalGroupEntry->timeInfo());
Group* clonedSubGroup = clonedGroup->children().at(0);
QVERIFY(clonedSubGroup->uuid() != subGroup->uuid());
QCOMPARE(clonedSubGroup->name(), QString("SubGroup"));
QCOMPARE(clonedSubGroup->children().size(), 0);
QCOMPARE(clonedSubGroup->entries().size(), 1);
QCOMPARE(clonedSubGroup->timeInfo(), subGroup->timeInfo());
Entry* clonedSubGroupEntry = clonedSubGroup->entries().at(0);
QVERIFY(clonedSubGroupEntry->uuid() != subGroupEntry->uuid());
@ -411,15 +415,17 @@ void TestGroup::testClone()
QCOMPARE(clonedGroupNewUuid->entries().size(), 0);
QVERIFY(clonedGroupNewUuid->uuid() != originalGroup->uuid());
// Making sure the new modification date is not the same.
// Verify Timeinfo modifications for CloneResetTimeInfo
m_clock->advanceSecond(1);
QScopedPointer<Group> clonedGroupResetTimeInfo(
originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid | Group::CloneResetTimeInfo));
QCOMPARE(clonedGroupResetTimeInfo->entries().size(), 0);
QVERIFY(clonedGroupResetTimeInfo->uuid() != originalGroup->uuid());
QVERIFY(clonedGroupResetTimeInfo->timeInfo().lastModificationTime()
!= originalGroup->timeInfo().lastModificationTime());
QVERIFY(clonedGroupResetTimeInfo->timeInfo().creationTime() != originalGroup->timeInfo().creationTime());
QVERIFY(clonedGroupResetTimeInfo->timeInfo().lastAccessTime() != originalGroup->timeInfo().lastAccessTime());
QVERIFY(clonedGroupResetTimeInfo->timeInfo().locationChanged() != originalGroup->timeInfo().locationChanged());
QCOMPARE(clonedGroupResetTimeInfo->timeInfo().lastModificationTime(), originalGroup->timeInfo().lastModificationTime());
}
void TestGroup::testCopyCustomIcons()
@ -1319,3 +1325,158 @@ void TestGroup::testAutoTypeState()
QVERIFY(!entry1->groupAutoTypeEnabled());
QVERIFY(entry2->groupAutoTypeEnabled());
}
void TestGroup::testTimeinfoChanges()
{
Database db, db2;
auto* root = db.rootGroup();
auto* subgroup1 = new Group();
auto* subgroup2 = new Group();
subgroup1->setUuid(QUuid::createUuid());
subgroup1->setParent(root);
subgroup2->setUuid(QUuid::createUuid());
subgroup2->setParent(root);
QDateTime startTime = Clock::currentDateTimeUtc();
TimeInfo startTimeinfo;
startTimeinfo.setCreationTime(startTime);
startTimeinfo.setLastModificationTime(startTime);
startTimeinfo.setLocationChanged(startTime);
startTimeinfo.setLastAccessTime(startTime);
m_clock->advanceMinute(1);
root->setTimeInfo(startTimeinfo);
subgroup1->setTimeInfo(startTimeinfo);
subgroup2->setTimeInfo(startTimeinfo);
subgroup2->setPreviousParentGroup(subgroup1);
// setting previous parent group should not affect the LastModificationTime
QCOMPARE(subgroup2->timeInfo().lastModificationTime(), startTime);
subgroup2->setPreviousParentGroup(nullptr);
subgroup2->setParent(subgroup1);
QCOMPARE(root->timeInfo(), startTimeinfo);
QCOMPARE(subgroup1->timeInfo(), startTimeinfo);
// changing group should not affect LastModificationTime, CreationTime
QCOMPARE(subgroup2->timeInfo().creationTime(), startTime);
QCOMPARE(subgroup2->timeInfo().lastModificationTime(), startTime);
// changing group should affect the LocationChanged time
QCOMPARE(subgroup2->timeInfo().locationChanged(), Clock::currentDateTimeUtc());
// cross-db move
db2.rootGroup()->setTimeInfo(startTimeinfo);
m_clock->advanceMinute(1);
subgroup2->setParent(db2.rootGroup());
QCOMPARE(subgroup2->timeInfo().creationTime(), startTime);
QCOMPARE(subgroup2->timeInfo().lastModificationTime(), startTime);
QCOMPARE(subgroup2->timeInfo().locationChanged(), Clock::currentDateTimeUtc());
QCOMPARE(db2.rootGroup()->timeInfo(), startTimeinfo);
QScopedPointer<Entry> entry1(new Entry());
entry1->setGroup(subgroup1);
// adding/removing an entry should not affect the LastModificationTime
QCOMPARE(subgroup1->timeInfo().lastModificationTime(), startTime);
entry1.reset(); // delete
QCOMPARE(subgroup1->timeInfo().lastModificationTime(), startTime);
// sorting should not affect the LastModificationTime
root->sortChildrenRecursively(true);
root->sortChildrenRecursively(false);
QCOMPARE(root->timeInfo().lastModificationTime(), startTime);
QCOMPARE(subgroup1->timeInfo().lastModificationTime(), startTime);
}
void TestGroup::testWalk()
{
QScopedPointer<Group> root(new Group());
size_t totalGroups{1}, totalEntries{0};
for (int i = 0; i < 3; ++i) {
Group* subgroup = new Group();
subgroup->setParent(root.data());
++totalGroups;
int rows = i + 1;
int columns = i;
QVector<Group*> groupsVec;
groupsVec.resize(rows * columns);
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < columns; ++c) {
int index = r * columns + c;
Group* group = new Group();
groupsVec[index] = group;
group->setParent(c > 0 ? groupsVec[index - 1] : subgroup);
int entryCount = std::max<int>(1, c + 1 >= columns ? 20 - (i * 3) : c + r);
for (int e = 0; e < entryCount; ++e) {
Entry* entry = new Entry();
entry->setGroup(group);
}
totalEntries += entryCount;
}
}
totalGroups += groupsVec.size();
}
size_t groupCount{0}, entryCount{0};
auto groupCounter = [&](Group* group){ groupCount += 1; };
auto entryCounter = [&](const Entry* entry){ entryCount += 1; };
bool shouldHaveStopped = false;
bool calledAfterStopped = false;
auto groupStopHalfway = [&](Group* group) {
groupCounter(group);
if (groupCount >= totalGroups / 2) {
if (shouldHaveStopped) {
calledAfterStopped = true;
} else {
shouldHaveStopped = true;
calledAfterStopped = false;
}
return true;
}
return false;
};
auto entryStopHalfWay = [&](Entry* entry) {
entryCounter(entry);
if (entryCount >= totalEntries / 2) {
if (shouldHaveStopped) {
calledAfterStopped = true;
} else {
shouldHaveStopped = true;
calledAfterStopped = false;
}
return true;
}
return false;
};
bool result = root->walk(true, groupCounter, entryCounter);
// walk should not stopped
QCOMPARE(result, false);
// walk should have visited all groups & entries
QCOMPARE(groupCount, totalGroups);
QCOMPARE(entryCount, totalEntries);
groupCount = entryCount = 0;
result = root->walkGroups(true, groupCounter);
QCOMPARE(result, false);
QCOMPARE(groupCount, totalGroups);
result = const_cast<const Group*>(root.data())->walkEntries(entryCounter);
QCOMPARE(result, false);
QCOMPARE(entryCount, totalEntries);
groupCount = entryCount = 0;
result = root->walk(false, groupStopHalfway, entryStopHalfWay);
// should have stopped
QCOMPARE(result, true);
// should not have been called after stopped
QCOMPARE(calledAfterStopped, false);
groupCount = entryCount = 0;
shouldHaveStopped = false;
result = root->walkGroups(false, groupStopHalfway);
QCOMPARE(result, true);
QCOMPARE(calledAfterStopped, false);
groupCount = entryCount = 0;
shouldHaveStopped = false;
result = root->walkEntries(entryStopHalfWay);
QCOMPARE(result, true);
QCOMPARE(calledAfterStopped, false);
}

View File

@ -50,6 +50,8 @@ private slots:
void testMoveUpDown();
void testPreviousParentGroup();
void testAutoTypeState();
void testTimeinfoChanges();
void testWalk();
};
#endif // KEEPASSX_TESTGROUP_H