1 /*
2  *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
3  *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 2 or (at your option)
8  *  version 3 of the License.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "Group.h"
20 #include "config-keepassx.h"
21 
22 #include "core/Clock.h"
23 #include "core/Config.h"
24 #include "core/DatabaseIcons.h"
25 #include "core/Global.h"
26 #include "core/Metadata.h"
27 #include "core/Tools.h"
28 
29 #ifdef WITH_XC_KEESHARE
30 #include "keeshare/KeeShare.h"
31 #endif
32 
33 #include <QtConcurrent>
34 
35 const int Group::DefaultIconNumber = 48;
36 const int Group::RecycleBinIconNumber = 43;
37 const QString Group::RootAutoTypeSequence = "{USERNAME}{TAB}{PASSWORD}{ENTER}";
38 
Group()39 Group::Group()
40     : m_customData(new CustomData(this))
41     , m_updateTimeinfo(true)
42 {
43     m_data.iconNumber = DefaultIconNumber;
44     m_data.isExpanded = true;
45     m_data.autoTypeEnabled = Inherit;
46     m_data.searchingEnabled = Inherit;
47     m_data.mergeMode = Default;
48 
49     connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(groupModified()));
50     connect(this, SIGNAL(groupModified()), SLOT(updateTimeinfo()));
51     connect(this, SIGNAL(groupNonDataChange()), SLOT(updateTimeinfo()));
52 }
53 
~Group()54 Group::~Group()
55 {
56     setUpdateTimeinfo(false);
57     // Destroy entries and children manually so DeletedObjects can be added
58     // to database.
59     const QList<Entry*> entries = m_entries;
60     for (Entry* entry : entries) {
61         delete entry;
62     }
63 
64     const QList<Group*> children = m_children;
65     for (Group* group : children) {
66         delete group;
67     }
68 
69     if (m_db && m_parent) {
70         DeletedObject delGroup;
71         delGroup.deletionTime = Clock::currentDateTimeUtc();
72         delGroup.uuid = m_uuid;
73         m_db->addDeletedObject(delGroup);
74     }
75 
76     cleanupParent();
77 }
78 
set(P & property,const V & value)79 template <class P, class V> inline bool Group::set(P& property, const V& value)
80 {
81     if (property != value) {
82         property = value;
83         emit groupModified();
84         return true;
85     } else {
86         return false;
87     }
88 }
89 
canUpdateTimeinfo() const90 bool Group::canUpdateTimeinfo() const
91 {
92     return m_updateTimeinfo;
93 }
94 
updateTimeinfo()95 void Group::updateTimeinfo()
96 {
97     if (m_updateTimeinfo) {
98         m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc());
99         m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc());
100     }
101 }
102 
setUpdateTimeinfo(bool value)103 void Group::setUpdateTimeinfo(bool value)
104 {
105     m_updateTimeinfo = value;
106 }
107 
uuid() const108 const QUuid& Group::uuid() const
109 {
110     return m_uuid;
111 }
112 
uuidToHex() const113 const QString Group::uuidToHex() const
114 {
115     return Tools::uuidToHex(m_uuid);
116 }
117 
name() const118 QString Group::name() const
119 {
120     return m_data.name;
121 }
122 
notes() const123 QString Group::notes() const
124 {
125     return m_data.notes;
126 }
127 
icon() const128 QImage Group::icon() const
129 {
130     if (m_data.customIcon.isNull()) {
131         return databaseIcons()->icon(m_data.iconNumber).toImage();
132     } else {
133         Q_ASSERT(m_db);
134         if (m_db) {
135             return m_db->metadata()->customIcon(m_data.customIcon);
136         } else {
137             return QImage();
138         }
139     }
140 }
141 
iconPixmap(IconSize size) const142 QPixmap Group::iconPixmap(IconSize size) const
143 {
144     QPixmap icon(size, size);
145     if (m_data.customIcon.isNull()) {
146         icon = databaseIcons()->icon(m_data.iconNumber, size);
147     } else {
148         Q_ASSERT(m_db);
149         if (m_db) {
150             icon = m_db->metadata()->customIconPixmap(m_data.customIcon, size);
151         }
152     }
153 
154     if (isExpired()) {
155         icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired);
156     }
157 #ifdef WITH_XC_KEESHARE
158     else if (KeeShare::isShared(this)) {
159         icon = KeeShare::indicatorBadge(this, icon);
160     }
161 #endif
162 
163     return icon;
164 }
165 
iconNumber() const166 int Group::iconNumber() const
167 {
168     return m_data.iconNumber;
169 }
170 
iconUuid() const171 const QUuid& Group::iconUuid() const
172 {
173     return m_data.customIcon;
174 }
175 
timeInfo() const176 const TimeInfo& Group::timeInfo() const
177 {
178     return m_data.timeInfo;
179 }
180 
isExpanded() const181 bool Group::isExpanded() const
182 {
183     return m_data.isExpanded;
184 }
185 
defaultAutoTypeSequence() const186 QString Group::defaultAutoTypeSequence() const
187 {
188     return m_data.defaultAutoTypeSequence;
189 }
190 
191 /**
192  * Determine the effective sequence that will be injected
193  * This function return an empty string if the current group or any parent has autotype disabled
194  */
effectiveAutoTypeSequence() const195 QString Group::effectiveAutoTypeSequence() const
196 {
197     QString sequence;
198 
199     const Group* group = this;
200     do {
201         if (group->autoTypeEnabled() == Group::Disable) {
202             return QString();
203         }
204 
205         sequence = group->defaultAutoTypeSequence();
206         group = group->parentGroup();
207     } while (group && sequence.isEmpty());
208 
209     if (sequence.isEmpty()) {
210         sequence = RootAutoTypeSequence;
211     }
212 
213     return sequence;
214 }
215 
autoTypeEnabled() const216 Group::TriState Group::autoTypeEnabled() const
217 {
218     return m_data.autoTypeEnabled;
219 }
220 
searchingEnabled() const221 Group::TriState Group::searchingEnabled() const
222 {
223     return m_data.searchingEnabled;
224 }
225 
mergeMode() const226 Group::MergeMode Group::mergeMode() const
227 {
228     if (m_data.mergeMode == Group::MergeMode::Default) {
229         if (m_parent) {
230             return m_parent->mergeMode();
231         }
232         return Group::MergeMode::KeepNewer; // fallback
233     }
234     return m_data.mergeMode;
235 }
236 
lastTopVisibleEntry() const237 Entry* Group::lastTopVisibleEntry() const
238 {
239     return m_lastTopVisibleEntry;
240 }
241 
isRecycled() const242 bool Group::isRecycled() const
243 {
244     auto group = this;
245     if (!group->database()) {
246         return false;
247     }
248 
249     do {
250         if (group->m_parent && group->m_db->metadata()) {
251             if (group->m_parent == group->m_db->metadata()->recycleBin()) {
252                 return true;
253             }
254         }
255         group = group->m_parent;
256     } while (group && group->m_parent && group->m_parent != group->m_db->rootGroup());
257 
258     return false;
259 }
260 
isExpired() const261 bool Group::isExpired() const
262 {
263     return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
264 }
265 
isEmpty() const266 bool Group::isEmpty() const
267 {
268     return !hasChildren() && m_entries.isEmpty();
269 }
270 
customData()271 CustomData* Group::customData()
272 {
273     return m_customData;
274 }
275 
customData() const276 const CustomData* Group::customData() const
277 {
278     return m_customData;
279 }
280 
equals(const Group * other,CompareItemOptions options) const281 bool Group::equals(const Group* other, CompareItemOptions options) const
282 {
283     if (!other) {
284         return false;
285     }
286     if (m_uuid != other->m_uuid) {
287         return false;
288     }
289     if (!m_data.equals(other->m_data, options)) {
290         return false;
291     }
292     if (m_customData != other->m_customData) {
293         return false;
294     }
295     if (m_children.count() != other->m_children.count()) {
296         return false;
297     }
298     if (m_entries.count() != other->m_entries.count()) {
299         return false;
300     }
301     for (int i = 0; i < m_children.count(); ++i) {
302         if (m_children[i]->uuid() != other->m_children[i]->uuid()) {
303             return false;
304         }
305     }
306     for (int i = 0; i < m_entries.count(); ++i) {
307         if (m_entries[i]->uuid() != other->m_entries[i]->uuid()) {
308             return false;
309         }
310     }
311     return true;
312 }
313 
setUuid(const QUuid & uuid)314 void Group::setUuid(const QUuid& uuid)
315 {
316     set(m_uuid, uuid);
317 }
318 
setName(const QString & name)319 void Group::setName(const QString& name)
320 {
321     if (set(m_data.name, name)) {
322         emit groupDataChanged(this);
323     }
324 }
325 
setNotes(const QString & notes)326 void Group::setNotes(const QString& notes)
327 {
328     set(m_data.notes, notes);
329 }
330 
setIcon(int iconNumber)331 void Group::setIcon(int iconNumber)
332 {
333     if (iconNumber >= 0 && (m_data.iconNumber != iconNumber || !m_data.customIcon.isNull())) {
334         m_data.iconNumber = iconNumber;
335         m_data.customIcon = QUuid();
336         emit groupModified();
337         emit groupDataChanged(this);
338     }
339 }
340 
setIcon(const QUuid & uuid)341 void Group::setIcon(const QUuid& uuid)
342 {
343     if (!uuid.isNull() && m_data.customIcon != uuid) {
344         m_data.customIcon = uuid;
345         m_data.iconNumber = 0;
346         emit groupModified();
347         emit groupDataChanged(this);
348     }
349 }
350 
setTimeInfo(const TimeInfo & timeInfo)351 void Group::setTimeInfo(const TimeInfo& timeInfo)
352 {
353     m_data.timeInfo = timeInfo;
354 }
355 
setExpanded(bool expanded)356 void Group::setExpanded(bool expanded)
357 {
358     if (m_data.isExpanded != expanded) {
359         m_data.isExpanded = expanded;
360         emit groupNonDataChange();
361     }
362 }
363 
setDefaultAutoTypeSequence(const QString & sequence)364 void Group::setDefaultAutoTypeSequence(const QString& sequence)
365 {
366     set(m_data.defaultAutoTypeSequence, sequence);
367 }
368 
setAutoTypeEnabled(TriState enable)369 void Group::setAutoTypeEnabled(TriState enable)
370 {
371     set(m_data.autoTypeEnabled, enable);
372 }
373 
setSearchingEnabled(TriState enable)374 void Group::setSearchingEnabled(TriState enable)
375 {
376     set(m_data.searchingEnabled, enable);
377 }
378 
setLastTopVisibleEntry(Entry * entry)379 void Group::setLastTopVisibleEntry(Entry* entry)
380 {
381     set(m_lastTopVisibleEntry, entry);
382 }
383 
setExpires(bool value)384 void Group::setExpires(bool value)
385 {
386     if (m_data.timeInfo.expires() != value) {
387         m_data.timeInfo.setExpires(value);
388         emit groupModified();
389     }
390 }
391 
setExpiryTime(const QDateTime & dateTime)392 void Group::setExpiryTime(const QDateTime& dateTime)
393 {
394     if (m_data.timeInfo.expiryTime() != dateTime) {
395         m_data.timeInfo.setExpiryTime(dateTime);
396         emit groupModified();
397     }
398 }
399 
setMergeMode(MergeMode newMode)400 void Group::setMergeMode(MergeMode newMode)
401 {
402     set(m_data.mergeMode, newMode);
403 }
404 
parentGroup()405 Group* Group::parentGroup()
406 {
407     return m_parent;
408 }
409 
parentGroup() const410 const Group* Group::parentGroup() const
411 {
412     return m_parent;
413 }
414 
setParent(Group * parent,int index)415 void Group::setParent(Group* parent, int index)
416 {
417     Q_ASSERT(parent);
418     Q_ASSERT(index >= -1 && index <= parent->children().size());
419     // setting a new parent for root groups is not allowed
420     Q_ASSERT(!m_db || (m_db->rootGroup() != this));
421 
422     bool moveWithinDatabase = (m_db && m_db == parent->m_db);
423 
424     if (index == -1) {
425         index = parent->children().size();
426 
427         if (parentGroup() == parent) {
428             index--;
429         }
430     }
431 
432     if (m_parent == parent && parent->children().indexOf(this) == index) {
433         return;
434     }
435 
436     if (!moveWithinDatabase) {
437         cleanupParent();
438         m_parent = parent;
439         if (m_db) {
440             recCreateDelObjects();
441 
442             // copy custom icon to the new database
443             if (!iconUuid().isNull() && parent->m_db && m_db->metadata()->hasCustomIcon(iconUuid())
444                 && !parent->m_db->metadata()->hasCustomIcon(iconUuid())) {
445                 parent->m_db->metadata()->addCustomIcon(iconUuid(), icon());
446             }
447         }
448         if (m_db != parent->m_db) {
449             connectDatabaseSignalsRecursive(parent->m_db);
450         }
451         QObject::setParent(parent);
452         emit groupAboutToAdd(this, index);
453         Q_ASSERT(index <= parent->m_children.size());
454         parent->m_children.insert(index, this);
455     } else {
456         emit aboutToMove(this, parent, index);
457         m_parent->m_children.removeAll(this);
458         m_parent = parent;
459         QObject::setParent(parent);
460         Q_ASSERT(index <= parent->m_children.size());
461         parent->m_children.insert(index, this);
462     }
463 
464     if (m_updateTimeinfo) {
465         m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
466     }
467 
468     emit groupModified();
469 
470     if (!moveWithinDatabase) {
471         emit groupAdded();
472     } else {
473         emit groupMoved();
474     }
475 }
476 
setParent(Database * db)477 void Group::setParent(Database* db)
478 {
479     Q_ASSERT(db);
480     Q_ASSERT(db->rootGroup() == this);
481 
482     cleanupParent();
483 
484     m_parent = nullptr;
485     connectDatabaseSignalsRecursive(db);
486 
487     QObject::setParent(db);
488 }
489 
hierarchy(int height) const490 QStringList Group::hierarchy(int height) const
491 {
492     QStringList hierarchy;
493     const Group* group = this;
494     const Group* parent = m_parent;
495 
496     if (height == 0) {
497         return hierarchy;
498     }
499 
500     hierarchy.prepend(group->name());
501 
502     int level = 1;
503     bool heightReached = level == height;
504 
505     while (parent && !heightReached) {
506         group = group->parentGroup();
507         parent = group->parentGroup();
508         heightReached = ++level == height;
509 
510         hierarchy.prepend(group->name());
511     }
512 
513     return hierarchy;
514 }
515 
hasChildren() const516 bool Group::hasChildren() const
517 {
518     return !children().isEmpty();
519 }
520 
database()521 Database* Group::database()
522 {
523     return m_db;
524 }
525 
database() const526 const Database* Group::database() const
527 {
528     return m_db;
529 }
530 
children()531 QList<Group*> Group::children()
532 {
533     return m_children;
534 }
535 
children() const536 const QList<Group*>& Group::children() const
537 {
538     return m_children;
539 }
540 
entries()541 QList<Entry*> Group::entries()
542 {
543     return m_entries;
544 }
545 
entries() const546 const QList<Entry*>& Group::entries() const
547 {
548     return m_entries;
549 }
550 
entriesRecursive(bool includeHistoryItems) const551 QList<Entry*> Group::entriesRecursive(bool includeHistoryItems) const
552 {
553     QList<Entry*> entryList;
554 
555     entryList.append(m_entries);
556 
557     if (includeHistoryItems) {
558         for (Entry* entry : m_entries) {
559             entryList.append(entry->historyItems());
560         }
561     }
562 
563     for (Group* group : m_children) {
564         entryList.append(group->entriesRecursive(includeHistoryItems));
565     }
566 
567     return entryList;
568 }
569 
referencesRecursive(const Entry * entry) const570 QList<Entry*> Group::referencesRecursive(const Entry* entry) const
571 {
572     auto entries = entriesRecursive();
573     return QtConcurrent::blockingFiltered(entries,
574                                           [entry](const Entry* e) { return e->hasReferencesTo(entry->uuid()); });
575 }
576 
findEntryByUuid(const QUuid & uuid,bool recursive) const577 Entry* Group::findEntryByUuid(const QUuid& uuid, bool recursive) const
578 {
579     if (uuid.isNull()) {
580         return nullptr;
581     }
582 
583     auto entries = m_entries;
584     if (recursive) {
585         entries = entriesRecursive(false);
586     }
587 
588     for (auto entry : entries) {
589         if (entry->uuid() == uuid) {
590             return entry;
591         }
592     }
593 
594     return nullptr;
595 }
596 
findEntryByPath(const QString & entryPath)597 Entry* Group::findEntryByPath(const QString& entryPath)
598 {
599     if (entryPath.isEmpty()) {
600         return nullptr;
601     }
602 
603     // Add a beginning slash if the search string contains a slash
604     // We don't add a slash by default to allow searching by entry title
605     QString normalizedEntryPath = entryPath;
606     if (!normalizedEntryPath.startsWith("/") && normalizedEntryPath.contains("/")) {
607         normalizedEntryPath = "/" + normalizedEntryPath;
608     }
609     return findEntryByPathRecursive(normalizedEntryPath, "/");
610 }
611 
findEntryBySearchTerm(const QString & term,EntryReferenceType referenceType)612 Entry* Group::findEntryBySearchTerm(const QString& term, EntryReferenceType referenceType)
613 {
614     Q_ASSERT_X(referenceType != EntryReferenceType::Unknown,
615                "Database::findEntryRecursive",
616                "Can't search entry with \"referenceType\" parameter equal to \"Unknown\"");
617 
618     const QList<Group*> groups = groupsRecursive(true);
619 
620     for (const Group* group : groups) {
621         bool found = false;
622         const QList<Entry*>& entryList = group->entries();
623         for (Entry* entry : entryList) {
624             switch (referenceType) {
625             case EntryReferenceType::Unknown:
626                 return nullptr;
627             case EntryReferenceType::Title:
628                 found = entry->title() == term;
629                 break;
630             case EntryReferenceType::UserName:
631                 found = entry->username() == term;
632                 break;
633             case EntryReferenceType::Password:
634                 found = entry->password() == term;
635                 break;
636             case EntryReferenceType::Url:
637                 found = entry->url() == term;
638                 break;
639             case EntryReferenceType::Notes:
640                 found = entry->notes() == term;
641                 break;
642             case EntryReferenceType::QUuid:
643                 found = entry->uuid() == QUuid::fromRfc4122(QByteArray::fromHex(term.toLatin1()));
644                 break;
645             case EntryReferenceType::CustomAttributes:
646                 found = entry->attributes()->containsValue(term);
647                 break;
648             }
649 
650             if (found) {
651                 return entry;
652             }
653         }
654     }
655 
656     return nullptr;
657 }
658 
findEntryByPathRecursive(const QString & entryPath,const QString & basePath)659 Entry* Group::findEntryByPathRecursive(const QString& entryPath, const QString& basePath)
660 {
661     // Return the first entry that matches the full path OR if there is no leading
662     // slash, return the first entry title that matches
663     for (Entry* entry : entries()) {
664         // clang-format off
665         if (entryPath == (basePath + entry->title())
666             || (!entryPath.startsWith("/") && entry->title() == entryPath)) {
667             return entry;
668         }
669         // clang-format on
670     }
671 
672     for (Group* group : children()) {
673         Entry* entry = group->findEntryByPathRecursive(entryPath, basePath + group->name() + "/");
674         if (entry != nullptr) {
675             return entry;
676         }
677     }
678 
679     return nullptr;
680 }
681 
findGroupByPath(const QString & groupPath)682 Group* Group::findGroupByPath(const QString& groupPath)
683 {
684     // normalize the groupPath by adding missing front and rear slashes. once.
685     QString normalizedGroupPath;
686 
687     if (groupPath.isEmpty()) {
688         normalizedGroupPath = QString("/"); // root group
689     } else {
690         // clang-format off
691         normalizedGroupPath = (groupPath.startsWith("/") ? "" : "/")
692             + groupPath
693             + (groupPath.endsWith("/") ? "" : "/");
694         // clang-format on
695     }
696     return findGroupByPathRecursive(normalizedGroupPath, "/");
697 }
698 
findGroupByPathRecursive(const QString & groupPath,const QString & basePath)699 Group* Group::findGroupByPathRecursive(const QString& groupPath, const QString& basePath)
700 {
701     // paths must be normalized
702     Q_ASSERT(groupPath.startsWith("/") && groupPath.endsWith("/"));
703     Q_ASSERT(basePath.startsWith("/") && basePath.endsWith("/"));
704 
705     if (groupPath == basePath) {
706         return this;
707     }
708 
709     for (Group* innerGroup : children()) {
710         QString innerBasePath = basePath + innerGroup->name() + "/";
711         Group* group = innerGroup->findGroupByPathRecursive(groupPath, innerBasePath);
712         if (group != nullptr) {
713             return group;
714         }
715     }
716 
717     return nullptr;
718 }
719 
print(bool recursive,bool flatten,int depth)720 QString Group::print(bool recursive, bool flatten, int depth)
721 {
722     QString response;
723     QString prefix;
724 
725     if (flatten) {
726         const QString separator("/");
727         prefix = hierarchy(depth).join(separator);
728         if (!prefix.isEmpty()) {
729             prefix += separator;
730         }
731     } else {
732         prefix = QString("  ").repeated(depth);
733     }
734 
735     if (entries().isEmpty() && children().isEmpty()) {
736         response += prefix + tr("[empty]", "group has no children") + "\n";
737         return response;
738     }
739 
740     for (Entry* entry : entries()) {
741         response += prefix + entry->title() + "\n";
742     }
743 
744     for (Group* innerGroup : children()) {
745         response += prefix + innerGroup->name() + "/\n";
746         if (recursive) {
747             response += innerGroup->print(recursive, flatten, depth + 1);
748         }
749     }
750 
751     return response;
752 }
753 
groupsRecursive(bool includeSelf) const754 QList<const Group*> Group::groupsRecursive(bool includeSelf) const
755 {
756     QList<const Group*> groupList;
757     if (includeSelf) {
758         groupList.append(this);
759     }
760 
761     for (const Group* group : asConst(m_children)) {
762         groupList.append(group->groupsRecursive(true));
763     }
764 
765     return groupList;
766 }
767 
groupsRecursive(bool includeSelf)768 QList<Group*> Group::groupsRecursive(bool includeSelf)
769 {
770     QList<Group*> groupList;
771     if (includeSelf) {
772         groupList.append(this);
773     }
774 
775     for (Group* group : asConst(m_children)) {
776         groupList.append(group->groupsRecursive(true));
777     }
778 
779     return groupList;
780 }
781 
customIconsRecursive() const782 QSet<QUuid> Group::customIconsRecursive() const
783 {
784     QSet<QUuid> result;
785 
786     if (!iconUuid().isNull()) {
787         result.insert(iconUuid());
788     }
789 
790     const QList<Entry*> entryList = entriesRecursive(true);
791     for (Entry* entry : entryList) {
792         if (!entry->iconUuid().isNull()) {
793             result.insert(entry->iconUuid());
794         }
795     }
796 
797     for (Group* group : m_children) {
798         result.unite(group->customIconsRecursive());
799     }
800 
801     return result;
802 }
803 
usernamesRecursive(int topN) const804 QList<QString> Group::usernamesRecursive(int topN) const
805 {
806     // Collect all usernames and sort for easy counting
807     QHash<QString, int> countedUsernames;
808     for (const auto* entry : entriesRecursive()) {
809         const auto username = entry->username();
810         if (!username.isEmpty() && !entry->isAttributeReference(EntryAttributes::UserNameKey)) {
811             countedUsernames.insert(username, ++countedUsernames[username]);
812         }
813     }
814 
815     // Sort username/frequency pairs by frequency and name
816     QList<QPair<QString, int>> sortedUsernames;
817     for (const auto& key : countedUsernames.keys()) {
818         sortedUsernames.append({key, countedUsernames[key]});
819     }
820 
821     auto comparator = [](const QPair<QString, int>& arg1, const QPair<QString, int>& arg2) {
822         if (arg1.second == arg2.second) {
823             return arg1.first < arg2.first;
824         }
825         return arg1.second > arg2.second;
826     };
827 
828     std::sort(sortedUsernames.begin(), sortedUsernames.end(), comparator);
829 
830     // Take first topN usernames if set
831     QList<QString> usernames;
832     int actualUsernames = topN < 0 ? sortedUsernames.size() : std::min(topN, sortedUsernames.size());
833     for (int i = 0; i < actualUsernames; i++) {
834         usernames.append(sortedUsernames[i].first);
835     }
836 
837     return usernames;
838 }
839 
findGroupByUuid(const QUuid & uuid)840 Group* Group::findGroupByUuid(const QUuid& uuid)
841 {
842     if (uuid.isNull()) {
843         return nullptr;
844     }
845 
846     for (Group* group : groupsRecursive(true)) {
847         if (group->uuid() == uuid) {
848             return group;
849         }
850     }
851 
852     return nullptr;
853 }
854 
findChildByName(const QString & name)855 Group* Group::findChildByName(const QString& name)
856 {
857     for (Group* group : asConst(m_children)) {
858         if (group->name() == name) {
859             return group;
860         }
861     }
862 
863     return nullptr;
864 }
865 
866 /**
867  * Creates a duplicate of this group.
868  * Note that you need to copy the custom icons manually when inserting the
869  * new group into another database.
870  */
clone(Entry::CloneFlags entryFlags,Group::CloneFlags groupFlags) const871 Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags) const
872 {
873     Group* clonedGroup = new Group();
874 
875     clonedGroup->setUpdateTimeinfo(false);
876 
877     if (groupFlags & Group::CloneNewUuid) {
878         clonedGroup->setUuid(QUuid::createUuid());
879     } else {
880         clonedGroup->setUuid(this->uuid());
881     }
882 
883     clonedGroup->m_data = m_data;
884     clonedGroup->m_customData->copyDataFrom(m_customData);
885 
886     if (groupFlags & Group::CloneIncludeEntries) {
887         const QList<Entry*> entryList = entries();
888         for (Entry* entry : entryList) {
889             Entry* clonedEntry = entry->clone(entryFlags);
890             clonedEntry->setGroup(clonedGroup);
891         }
892 
893         const QList<Group*> childrenGroups = children();
894         for (Group* groupChild : childrenGroups) {
895             Group* clonedGroupChild = groupChild->clone(entryFlags, groupFlags);
896             clonedGroupChild->setParent(clonedGroup);
897         }
898     }
899 
900     clonedGroup->setUpdateTimeinfo(true);
901     if (groupFlags & Group::CloneResetTimeInfo) {
902 
903         QDateTime now = Clock::currentDateTimeUtc();
904         clonedGroup->m_data.timeInfo.setCreationTime(now);
905         clonedGroup->m_data.timeInfo.setLastModificationTime(now);
906         clonedGroup->m_data.timeInfo.setLastAccessTime(now);
907         clonedGroup->m_data.timeInfo.setLocationChanged(now);
908     }
909 
910     return clonedGroup;
911 }
912 
copyDataFrom(const Group * other)913 void Group::copyDataFrom(const Group* other)
914 {
915     if (set(m_data, other->m_data)) {
916         emit groupDataChanged(this);
917     }
918     m_customData->copyDataFrom(other->m_customData);
919     m_lastTopVisibleEntry = other->m_lastTopVisibleEntry;
920 }
921 
addEntry(Entry * entry)922 void Group::addEntry(Entry* entry)
923 {
924     Q_ASSERT(entry);
925     Q_ASSERT(!m_entries.contains(entry));
926 
927     emit entryAboutToAdd(entry);
928 
929     m_entries << entry;
930     connect(entry, SIGNAL(entryDataChanged(Entry*)), SIGNAL(entryDataChanged(Entry*)));
931     if (m_db) {
932         connect(entry, SIGNAL(entryModified()), m_db, SLOT(markAsModified()));
933     }
934 
935     emit groupModified();
936     emit entryAdded(entry);
937 }
938 
removeEntry(Entry * entry)939 void Group::removeEntry(Entry* entry)
940 {
941     Q_ASSERT_X(m_entries.contains(entry),
942                Q_FUNC_INFO,
943                QString("Group %1 does not contain %2").arg(this->name(), entry->title()).toLatin1());
944 
945     emit entryAboutToRemove(entry);
946 
947     entry->disconnect(this);
948     if (m_db) {
949         entry->disconnect(m_db);
950     }
951     m_entries.removeAll(entry);
952     emit groupModified();
953     emit entryRemoved(entry);
954 }
955 
moveEntryUp(Entry * entry)956 void Group::moveEntryUp(Entry* entry)
957 {
958     int row = m_entries.indexOf(entry);
959     if (row <= 0) {
960         return;
961     }
962 
963     emit entryAboutToMoveUp(row);
964     m_entries.move(row, row - 1);
965     emit entryMovedUp();
966     emit groupNonDataChange();
967 }
968 
moveEntryDown(Entry * entry)969 void Group::moveEntryDown(Entry* entry)
970 {
971     int row = m_entries.indexOf(entry);
972     if (row >= m_entries.size() - 1) {
973         return;
974     }
975 
976     emit entryAboutToMoveDown(row);
977     m_entries.move(row, row + 1);
978     emit entryMovedDown();
979     emit groupNonDataChange();
980 }
981 
connectDatabaseSignalsRecursive(Database * db)982 void Group::connectDatabaseSignalsRecursive(Database* db)
983 {
984     if (m_db) {
985         disconnect(m_db);
986     }
987 
988     for (Entry* entry : asConst(m_entries)) {
989         if (m_db) {
990             entry->disconnect(m_db);
991         }
992         if (db) {
993             connect(entry, SIGNAL(entryModified()), db, SLOT(markAsModified()));
994         }
995     }
996 
997     if (db) {
998         // clang-format off
999         connect(this, SIGNAL(groupDataChanged(Group*)), db, SIGNAL(groupDataChanged(Group*)));
1000         connect(this, SIGNAL(groupAboutToRemove(Group*)), db, SIGNAL(groupAboutToRemove(Group*)));
1001         connect(this, SIGNAL(groupRemoved()), db, SIGNAL(groupRemoved()));
1002         connect(this, SIGNAL(groupAboutToAdd(Group*, int)), db, SIGNAL(groupAboutToAdd(Group*,int)));
1003         connect(this, SIGNAL(groupAdded()), db, SIGNAL(groupAdded()));
1004         connect(this, SIGNAL(aboutToMove(Group*,Group*,int)), db, SIGNAL(groupAboutToMove(Group*,Group*,int)));
1005         connect(this, SIGNAL(groupMoved()), db, SIGNAL(groupMoved()));
1006         connect(this, SIGNAL(groupModified()), db, SLOT(markAsModified()));
1007         connect(this, SIGNAL(groupNonDataChange()), db, SLOT(markNonDataChange()));
1008         // clang-format on
1009     }
1010 
1011     m_db = db;
1012 
1013     for (Group* group : asConst(m_children)) {
1014         group->connectDatabaseSignalsRecursive(db);
1015     }
1016 }
1017 
cleanupParent()1018 void Group::cleanupParent()
1019 {
1020     if (m_parent) {
1021         emit groupAboutToRemove(this);
1022         m_parent->m_children.removeAll(this);
1023         emit groupModified();
1024         emit groupRemoved();
1025     }
1026 }
1027 
recCreateDelObjects()1028 void Group::recCreateDelObjects()
1029 {
1030     if (m_db) {
1031         for (Entry* entry : asConst(m_entries)) {
1032             m_db->addDeletedObject(entry->uuid());
1033         }
1034 
1035         for (Group* group : asConst(m_children)) {
1036             group->recCreateDelObjects();
1037         }
1038         m_db->addDeletedObject(m_uuid);
1039     }
1040 }
1041 
resolveSearchingEnabled() const1042 bool Group::resolveSearchingEnabled() const
1043 {
1044     switch (m_data.searchingEnabled) {
1045     case Inherit:
1046         if (!m_parent) {
1047             return true;
1048         } else {
1049             return m_parent->resolveSearchingEnabled();
1050         }
1051     case Enable:
1052         return true;
1053     case Disable:
1054         return false;
1055     default:
1056         Q_ASSERT(false);
1057         return false;
1058     }
1059 }
1060 
resolveAutoTypeEnabled() const1061 bool Group::resolveAutoTypeEnabled() const
1062 {
1063     switch (m_data.autoTypeEnabled) {
1064     case Inherit:
1065         if (!m_parent) {
1066             return true;
1067         } else {
1068             return m_parent->resolveAutoTypeEnabled();
1069         }
1070     case Enable:
1071         return true;
1072     case Disable:
1073         return false;
1074     default:
1075         Q_ASSERT(false);
1076         return false;
1077     }
1078 }
1079 
locate(const QString & locateTerm,const QString & currentPath) const1080 QStringList Group::locate(const QString& locateTerm, const QString& currentPath) const
1081 {
1082     // TODO: Replace with EntrySearcher
1083     QStringList response;
1084     if (locateTerm.isEmpty()) {
1085         return response;
1086     }
1087 
1088     for (const Entry* entry : asConst(m_entries)) {
1089         QString entryPath = currentPath + entry->title();
1090         if (entryPath.contains(locateTerm, Qt::CaseInsensitive)) {
1091             response << entryPath;
1092         }
1093     }
1094 
1095     for (const Group* group : asConst(m_children)) {
1096         for (const QString& path : group->locate(locateTerm, currentPath + group->name() + QString("/"))) {
1097             response << path;
1098         }
1099     }
1100 
1101     return response;
1102 }
1103 
addEntryWithPath(const QString & entryPath)1104 Entry* Group::addEntryWithPath(const QString& entryPath)
1105 {
1106     if (entryPath.isEmpty() || findEntryByPath(entryPath)) {
1107         return nullptr;
1108     }
1109 
1110     QStringList groups = entryPath.split("/");
1111     QString entryTitle = groups.takeLast();
1112     QString groupPath = groups.join("/");
1113 
1114     Group* group = findGroupByPath(groupPath);
1115     if (!group) {
1116         return nullptr;
1117     }
1118 
1119     auto* entry = new Entry();
1120     entry->setTitle(entryTitle);
1121     entry->setUuid(QUuid::createUuid());
1122     entry->setGroup(group);
1123 
1124     return entry;
1125 }
1126 
applyGroupIconOnCreateTo(Entry * entry)1127 void Group::applyGroupIconOnCreateTo(Entry* entry)
1128 {
1129     Q_ASSERT(entry);
1130 
1131     if (!config()->get(Config::UseGroupIconOnEntryCreation).toBool()) {
1132         return;
1133     }
1134 
1135     if (iconNumber() == Group::DefaultIconNumber && iconUuid().isNull()) {
1136         return;
1137     }
1138 
1139     applyGroupIconTo(entry);
1140 }
1141 
applyGroupIconTo(Entry * entry)1142 void Group::applyGroupIconTo(Entry* entry)
1143 {
1144     Q_ASSERT(entry);
1145 
1146     if (iconUuid().isNull()) {
1147         entry->setIcon(iconNumber());
1148     } else {
1149         entry->setIcon(iconUuid());
1150     }
1151 }
1152 
applyGroupIconTo(Group * other)1153 void Group::applyGroupIconTo(Group* other)
1154 {
1155     Q_ASSERT(other);
1156 
1157     if (iconUuid().isNull()) {
1158         other->setIcon(iconNumber());
1159     } else {
1160         other->setIcon(iconUuid());
1161     }
1162 }
1163 
applyGroupIconToChildGroups()1164 void Group::applyGroupIconToChildGroups()
1165 {
1166     for (Group* recursiveChild : groupsRecursive(false)) {
1167         applyGroupIconTo(recursiveChild);
1168     }
1169 }
1170 
applyGroupIconToChildEntries()1171 void Group::applyGroupIconToChildEntries()
1172 {
1173     for (Entry* recursiveEntry : entriesRecursive(false)) {
1174         applyGroupIconTo(recursiveEntry);
1175     }
1176 }
1177 
sortChildrenRecursively(bool reverse)1178 void Group::sortChildrenRecursively(bool reverse)
1179 {
1180     std::sort(
1181         m_children.begin(), m_children.end(), [reverse](const Group* childGroup1, const Group* childGroup2) -> bool {
1182             QString name1 = childGroup1->name();
1183             QString name2 = childGroup2->name();
1184             return reverse ? name1.compare(name2, Qt::CaseInsensitive) > 0
1185                            : name1.compare(name2, Qt::CaseInsensitive) < 0;
1186         });
1187 
1188     for (auto child : m_children) {
1189         child->sortChildrenRecursively(reverse);
1190     }
1191 
1192     emit groupModified();
1193 }
1194 
operator ==(const Group::GroupData & other) const1195 bool Group::GroupData::operator==(const Group::GroupData& other) const
1196 {
1197     return equals(other, CompareItemDefault);
1198 }
1199 
operator !=(const Group::GroupData & other) const1200 bool Group::GroupData::operator!=(const Group::GroupData& other) const
1201 {
1202     return !(*this == other);
1203 }
1204 
equals(const Group::GroupData & other,CompareItemOptions options) const1205 bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions options) const
1206 {
1207     if (::compare(name, other.name, options) != 0) {
1208         return false;
1209     }
1210     if (::compare(notes, other.notes, options) != 0) {
1211         return false;
1212     }
1213     if (::compare(iconNumber, other.iconNumber) != 0) {
1214         return false;
1215     }
1216     if (::compare(customIcon, other.customIcon) != 0) {
1217         return false;
1218     }
1219     if (!timeInfo.equals(other.timeInfo, options)) {
1220         return false;
1221     }
1222     // TODO HNH: Some properties are configurable - should they be ignored?
1223     if (::compare(isExpanded, other.isExpanded, options) != 0) {
1224         return false;
1225     }
1226     if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) {
1227         return false;
1228     }
1229     if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) {
1230         return false;
1231     }
1232     if (::compare(searchingEnabled, other.searchingEnabled, options) != 0) {
1233         return false;
1234     }
1235     if (::compare(mergeMode, other.mergeMode, options) != 0) {
1236         return false;
1237     }
1238     return true;
1239 }
1240