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