diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index ef6440f88..f3c5e9c07 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1069,10 +1069,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) { @@ -1463,10 +1468,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); @@ -1690,7 +1693,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) diff --git a/src/core/Entry.h b/src/core/Entry.h index 4874f5937..417340f9d 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -190,13 +190,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) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 8b6cd75c5..de77fc131 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -77,11 +77,11 @@ Group::~Group() cleanupParent(); } -template inline bool Group::set(P& property, const V& value) +template 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); @@ -484,9 +493,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) { @@ -512,7 +520,7 @@ void Group::setParent(Group* parent, int index, bool trackPrevious) m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc()); } - emitModified(); + emitModifiedEx(true); if (!moveWithinDatabase) { emit groupAdded(); @@ -565,6 +573,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; @@ -961,12 +979,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) { @@ -998,7 +1020,7 @@ void Group::addEntry(Entry* entry) connect(entry, &Entry::modified, m_db, &Database::markAsModified); } - emitModified(); + emitModifiedEx(true); emit entryAdded(entry); } @@ -1015,7 +1037,7 @@ void Group::removeEntry(Entry* entry) entry->disconnect(m_db); } m_entries.removeAll(entry); - emitModified(); + emitModifiedEx(true); emit entryRemoved(entry); } @@ -1086,7 +1108,7 @@ void Group::cleanupParent() if (m_parent) { emit groupAboutToRemove(this); m_parent->m_children.removeAll(this); - emitModified(); + emitModifiedEx(true); emit groupRemoved(); } } @@ -1255,7 +1277,7 @@ void Group::sortChildrenRecursively(bool reverse) child->sortChildrenRecursively(reverse); } - emitModified(); + emitModifiedEx(true); } const Group* Group::previousParentGroup() const @@ -1273,7 +1295,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) diff --git a/src/core/Group.h b/src/core/Group.h index 01c0b2120..9117fca40 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -20,11 +20,24 @@ #define KEEPASSX_GROUP_H #include +#include +#include #include "core/CustomData.h" #include "core/Database.h" #include "core/Entry.h" + +class Entry; +class Group; + + +template concept CGroupVisitor = std::is_invocable_v; +template concept CGroupConstVisitor = std::is_invocable_v; +template concept CEntryVisitor = std::is_invocable_v; +template concept CEntryConstVisitor = std::is_invocable_v; + + 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) @@ -149,6 +168,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; @@ -160,6 +180,53 @@ public: QList entriesRecursive(bool includeHistoryItems = false) const; QList groupsRecursive(bool includeSelf) const; QList 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 + bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) + { + return walk( + includeSelf, std::forward(groupVisitor), std::forward(entryVisitor)); + } + template + bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const + { + return walk( + includeSelf, std::forward(groupVisitor), std::forward(entryVisitor)); + } + template bool walkGroups(bool includeSelf, TGroupCallable&& groupVisitor) const + { + return walk( + includeSelf, std::forward(groupVisitor), nullptr); + } + template bool walkGroups(bool includeSelf, TGroupCallable&& groupVisitor) + { + return walk( + includeSelf, std::forward(groupVisitor), nullptr); + } + template bool walkEntries(TEntryCallable&& entryVisitor) const + { + return walk( + true, nullptr, std::forward(entryVisitor)); + } + template bool walkEntries(TEntryCallable&& entryVisitor) + { + return walk( + true, nullptr, std::forward(entryVisitor)); + } + QSet customIconsRecursive() const; QList usernamesRecursive(int topN = -1) const; @@ -205,8 +272,11 @@ private slots: void updateTimeinfo(); private: - template bool set(P& property, const V& value); + template + bool walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const; + template bool set(P& property, const V& value, bool preserveTimeinfo = false); + void emitModifiedEx(bool preserveTimeinfo); void setParent(Database* db); void connectDatabaseSignalsRecursive(Database* db); @@ -234,4 +304,55 @@ private: Q_DECLARE_OPERATORS_FOR_FLAGS(Group::CloneFlags) +// helpers to support non-bool returning callables +template +bool visitorPredicateImpl(std::true_type, TCallable&& callable, Args&&... args) +{ + return callable(std::forward(args)...); +} + +template +bool visitorPredicateImpl(std::false_type, TCallable&& callable, Args&&... args) +{ + callable(std::forward(args)...); + return kDefaultRetVal; +} + +template +bool visitorPredicate(TCallable&& callable, Args&&... args) +{ + using RetType = decltype(callable(args...)); + return visitorPredicateImpl( + std::is_same{}, std::forward(callable), std::forward(args)...); +} + +template +bool Group::walk(bool includeSelf, TGroupCallable&& groupVisitor, TEntryCallable&& entryVisitor) const +{ + using GroupType = typename std::conditional::type; + QList groupsToVisit; + if (includeSelf) { + groupsToVisit.append(const_cast(this)); + } else { + groupsToVisit.append(m_children); + } + while (!groupsToVisit.isEmpty()) { + GroupType* group = groupsToVisit.takeLast(); // right-to-left + if constexpr (kVisitGroups) { + if (visitorPredicate(groupVisitor, group)) { + return true; + } + } + if constexpr (kVisitEntries) { + for (auto* entry : group->m_entries) { + if (visitorPredicate(entryVisitor, entry)) { + return true; + } + } + } + groupsToVisit.append(group->m_children); + } + return false; +} + #endif // KEEPASSX_GROUP_H diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 8e714e0f2..62a34ff40 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -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& 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); } } diff --git a/src/core/Metadata.h b/src/core/Metadata.h index 6e80ebc09..fbb9709f0 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -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& iconList, const Metadata* otherMetadata); QUuid findCustomIcon(const QByteArray& candidate); void setRecycleBinEnabled(bool value); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 25b96c5ae..7881a6aa3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -734,6 +734,10 @@ QList 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); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 8effd06f4..165aea4f1 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -52,6 +52,7 @@ public: ~MainWindow() override; QList getOpenDatabases(); + DatabaseWidget* currentDatabaseWidget(); void restoreConfigState(); void setAllowScreenCapture(bool state); diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 9ae51743d..96e0304d5 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -457,7 +457,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 diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index 18b926dc2..fac98b56e 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -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 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 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 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 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; diff --git a/src/gui/group/GroupView.cpp b/src/gui/group/GroupView.cpp index 46cc0af6a..7b00cef34 100644 --- a/src/gui/group/GroupView.cpp +++ b/src/gui/group/GroupView.cpp @@ -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& 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(parent)) { + return dbWidget; + } + } + } + return nullptr; + }; + + m_isDragEventSrcFromOtherDb = false; + if (GroupView* view = qobject_cast(eventSource)) { + m_isDragEventSrcFromOtherDb = view != this; + } else if (EntryView* view = qobject_cast(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(); } } diff --git a/src/gui/group/GroupView.h b/src/gui/group/GroupView.h index 15df853ff..387e25b3c 100644 --- a/src/gui/group/GroupView.h +++ b/src/gui/group/GroupView.h @@ -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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 25b116d8e..a8713dc2d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}) diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 18567001e..be9a1f0ca 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -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(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]; @@ -905,3 +933,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(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()); +} diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 953a7ce7b..800d6a50e 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -28,6 +28,8 @@ class TestEntry : public QObject private slots: void initTestCase(); + void init(); + void cleanup(); void testHistoryItemDeletion(); void testCopyDataFrom(); void testClone(); @@ -43,6 +45,7 @@ private slots: void testIsRecycled(); void testMoveUpDown(); void testPreviousParentGroup(); + void testTimeinfoChanges(); }; #endif // KEEPASSX_TESTENTRY_H diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 22807c878..467064709 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -20,6 +20,7 @@ #include "mock/MockClock.h" #include +#include #include #include @@ -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 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 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 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 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(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(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); +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index d3326e464..8442e7d37 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -50,6 +50,8 @@ private slots: void testMoveUpDown(); void testPreviousParentGroup(); void testAutoTypeState(); + void testTimeinfoChanges(); + void testWalk(); }; #endif // KEEPASSX_TESTGROUP_H