From a7e7dcc1fe4f945ac8ec33545f9ed259a64afabb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 7 Jul 2018 23:02:53 -0400 Subject: [PATCH] safeincloud xml importer, org import support --- src/abstractions/api.service.ts | 2 +- src/importers/aviraCsvImporter.ts | 4 - src/importers/baseImporter.ts | 15 +++ src/importers/blurCsvImporter.ts | 4 - src/importers/keepassxCsvImporter.ts | 8 +- src/importers/safeInCloudXmlImporter.ts | 121 ++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/importers/safeInCloudXmlImporter.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 56c668b37c..4b61c79c4d 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -171,5 +171,5 @@ export abstract class ApiService { getEventsOrganization: (id: string, start: string, end: string, token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, - start: string, end: string, token: string) => Promise> + start: string, end: string, token: string) => Promise>; } diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index 15f9ddf491..bb10ad3927 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -10,10 +10,6 @@ import { CipherType } from '../enums/cipherType'; export class AviraCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 009a7ccdc2..19b43104de 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -53,6 +53,12 @@ export abstract class BaseImporter { 'ort', 'adresse', ]; + protected parseXml(data: string): Document { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, 'application/xml'); + return doc != null && doc.querySelector('parsererror') == null ? doc : null; + } + protected parseCsv(data: string, header: boolean): any[] { const result = papa.parse(data, { header: header, @@ -210,4 +216,13 @@ export abstract class BaseImporter { result.folderRelationships = new Map(); result.folders = []; } + + protected querySelectorDirectChild(parentEl: Element, query: string) { + const els = this.querySelectorAllDirectChild(parentEl, query); + return els.length === 0 ? null : els[0]; + } + + protected querySelectorAllDirectChild(parentEl: Element, query: string) { + return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl); + } } diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index efe91de808..33620181b5 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -10,10 +10,6 @@ import { CipherType } from '../enums/cipherType'; export class BlurCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 59325efe9d..cec7fa4e68 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -11,10 +11,6 @@ import { CipherType } from '../enums/cipherType'; export class KeePassXCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -66,6 +62,10 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { result.ciphers.push(cipher); }); + if (this.organization) { + this.moveFoldersToCollections(result); + } + result.success = true; return result; } diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts new file mode 100644 index 0000000000..43c8285754 --- /dev/null +++ b/src/importers/safeInCloudXmlImporter.ts @@ -0,0 +1,121 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; +import { FolderView } from '../models/view/folderView'; +import { LoginView } from '../models/view/loginView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class SafeInCloudXmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const db = doc.querySelector('database'); + if (db == null) { + result.errorMessage = 'Missing `database` node.'; + result.success = false; + return result; + } + + const foldersMap = new Map(); + + Array.from(doc.querySelectorAll('database > label')).forEach((labelEl) => { + const name = labelEl.getAttribute('name'); + const id = labelEl.getAttribute('id'); + if (!this.isNullOrWhitespace(name) && !this.isNullOrWhitespace(id)) { + foldersMap.set(id, result.folders.length); + const folder = new FolderView(); + folder.name = name; + result.folders.push(folder); + } + }); + + Array.from(doc.querySelectorAll('database > card')).forEach((cardEl) => { + if (cardEl.getAttribute('template') === 'true') { + return; + } + + const labelIdEl = this.querySelectorDirectChild(cardEl, 'label_id'); + if (labelIdEl != null) { + const labelId = labelIdEl.textContent; + if (!this.isNullOrWhitespace(labelId) && foldersMap.has(labelId)) { + result.folderRelationships.set(result.ciphers.length, foldersMap.get(labelId)); + } + } + + const cipher = new CipherView(); + cipher.favorite = false; + cipher.notes = ''; + cipher.name = this.getValueOrDefault(cardEl.getAttribute('title'), '--'); + cipher.fields = null; + + const cardType = cardEl.getAttribute('type'); + if (cardType === 'note') { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } else { + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach((fieldEl) => { + const text = fieldEl.textContent; + if (this.isNullOrWhitespace(text)) { + return; + } + const name = fieldEl.getAttribute('name'); + const fieldType = this.getValueOrDefault(fieldEl.getAttribute('type'), '').toLowerCase(); + if (fieldType === 'login') { + cipher.login.username = text; + } else if (fieldType === 'password') { + cipher.login.password = text; + } else if (fieldType === 'notes') { + cipher.notes += (text + '\n'); + } else if (fieldType === 'weblogin' || fieldType === 'website') { + cipher.login.uris = this.makeUriArray(text); + } else if (text.length > 200) { + cipher.notes += (name + ': ' + text + '\n'); + } else { + if (cipher.fields == null) { + cipher.fields = []; + } + const field = new FieldView(); + field.name = name; + field.value = text; + field.type = FieldType.Text; + cipher.fields.push(field); + } + }); + } + + Array.from(this.querySelectorAllDirectChild(cardEl, 'notes')).forEach((notesEl) => { + cipher.notes += (notesEl.textContent + '\n'); + }); + + cipher.notes = cipher.notes.trim(); + if (cipher.notes === '') { + cipher.notes = null; + } + + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +}