KeepassX format implementation in pure dart.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

216 lines
6.5 KiB

import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart';
import 'package:xml/xml.dart';
import 'kdbx_object.dart';
final _logger = Logger('kdbx_group');
class KdbxGroup extends KdbxObject {
KdbxGroup.create({
required KdbxReadWriteContext ctx,
required KdbxGroup? parent,
required String? name,
}) : super.create(
ctx,
parent?.file,
KdbxXml.NODE_GROUP,
parent,
) {
this.name.set(name);
icon.set(KdbxIcon.Folder);
expanded.set(true);
}
KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup? parent, XmlElement node)
: super.read(ctx, parent, node) {
node
.findElements(KdbxXml.NODE_GROUP)
.map((el) => KdbxGroup.read(ctx, this, el))
.forEach(_groups.add);
node
.findElements('Entry')
.map((el) => KdbxEntry.read(ctx, this, el))
.forEach(_entries.add);
}
@override
XmlElement toXml() {
final el = super.toXml();
XmlUtils.removeChildrenByName(el, 'Group');
XmlUtils.removeChildrenByName(el, 'Entry');
el.children.addAll(groups.map((g) => g.toXml()));
el.children.addAll(_entries.map((e) => e.toXml()));
return el;
}
/// Returns all groups plus this group itself.
List<KdbxGroup> getAllGroups() => groups
.expand((g) => g.getAllGroups())
.followedBy([this]).toList(growable: false);
/// Returns all entries of this group and all sub groups.
List<KdbxEntry> getAllEntries() =>
getAllGroups().expand((g) => g.entries).toList(growable: false);
/// Returns all groups and entries. (Including the group itself).
Iterable<KdbxObject> getAllGroupsAndEntries() => <KdbxObject>[this]
.followedBy(entries)
.followedBy(groups.expand((g) => g.getAllGroupsAndEntries()));
List<KdbxGroup> get groups => List.unmodifiable(_groups);
final List<KdbxGroup> _groups = [];
List<KdbxEntry> get entries => List.unmodifiable(_entries);
final List<KdbxEntry> _entries = [];
void addEntry(KdbxEntry entry) {
if (entry.parent != this) {
throw StateError(
'Invalid operation. Trying to add entry which is already in another group.');
}
assert(_entries.findByUuid(entry.uuid) == null,
'must not already be in this group.');
modify(() => _entries.add(entry));
}
void addGroup(KdbxGroup group) {
if (group.parent != this) {
throw StateError(
'Invalid operation. Trying to add group which is already in another group.');
}
modify(() => _groups.add(group));
}
/// returns all parents recursively including this group.
List<KdbxGroup> get breadcrumbs => [...?parent?.breadcrumbs, this];
StringNode get name => StringNode(this, 'Name');
StringNode get notes => StringNode(this, 'Notes');
// String get name => text('Name') ?? '';
BooleanNode get expanded => BooleanNode(this, 'IsExpanded');
StringNode get defaultAutoTypeSequence =>
StringNode(this, 'DefaultAutoTypeSequence');
BooleanNode get enableAutoType => BooleanNode(this, 'EnableAutoType');
BooleanNode get enableSearching => BooleanNode(this, 'EnableSearching');
UuidNode get lastTopVisibleEntry => UuidNode(this, 'LastTopVisibleEntry');
@override
void merge(MergeContext mergeContext, KdbxGroup other) {
assertSameUuid(other, 'merge');
if (other.wasModifiedAfter(this)) {
_logger.finest('merge: other group was modified $uuid');
_overwriteFrom(mergeContext, other);
}
_mergeSubObjects<KdbxGroup>(
mergeContext,
_groups,
other._groups,
importToHere: (other) =>
KdbxGroup.create(ctx: ctx, parent: this, name: other.name.get())
..forceSetUuid(other.uuid)
..let((x) => addGroup(x))
.._overwriteFrom(mergeContext, other),
);
_mergeSubObjects<KdbxEntry>(
mergeContext,
_entries,
other._entries,
importToHere: (other) => other.cloneInto(this),
);
mergeContext.markAsMerged(this);
}
void _mergeSubObjects<T extends KdbxObject>(
MergeContext mergeContext, List<T> me, List<T> other,
{required T Function(T obj) importToHere}) {
// possibilities:
// 1. Not changed at all 👍
// 2. Deleted in other
// 3. Deleted in this
// 4. Modified in other
// 5. Modified in this
// 6. Moved in other
// 7. Moved in this
for (final otherObj in other) {
final meObj = me.findByUuid(otherObj.uuid);
if (meObj == null) {
// moved or deleted.
final movedObj = mergeContext.objectIndex[otherObj.uuid];
if (movedObj == null) {
// item was created in the other file. we have to import it
final newMeObject = importToHere(otherObj);
mergeContext.trackChange(newMeObject, debug: '(was created)');
newMeObject.merge(mergeContext, otherObj);
} else {
// item was moved.
if (otherObj.wasMovedAfter(movedObj)) {
// item was moved in the other file, so we have to move it here.
file!.move(movedObj, this);
mergeContext.trackChange(movedObj, debug: 'moved to another group');
} else {
// item was moved in this file, so nothing to do.
}
movedObj.merge(mergeContext, otherObj);
}
} else {
meObj.merge(mergeContext, otherObj);
}
}
}
List<KdbxSubNode> get _overwriteNodes => [
...objectNodes,
name,
notes,
expanded,
defaultAutoTypeSequence,
enableAutoType,
enableSearching,
lastTopVisibleEntry,
];
void _overwriteFrom(MergeContext mergeContext, KdbxGroup other) {
assertSameUuid(other, 'overwrite');
overwriteSubNodesFrom(mergeContext, _overwriteNodes, other._overwriteNodes);
// we should probably check that [lastTopVisibleEntry] is still a
// valid reference?
times.overwriteFrom(other.times);
}
@override
String toString() {
return 'KdbxGroup{uuid=$uuid,name=${name.get()}}';
}
}
extension KdbxGroupInternal on KdbxGroup {
void internalRemoveGroup(KdbxGroup group) {
modify(() {
if (!_groups.remove(group)) {
throw StateError('Unable to remove $group from $this (Not found)');
}
});
}
void internalRemoveEntry(KdbxEntry entry) {
modify(() {
if (!_entries.remove(entry)) {
throw StateError('Unable to remove $entry from $this (Not found)');
}
});
}
}