From 5703dceb0c8785544b1ab00d561920bc066efc7d Mon Sep 17 00:00:00 2001 From: antelle Date: Sun, 8 Sep 2019 20:28:02 +0200 Subject: [PATCH] HTML export --- app/scripts/comp/kdbx-to-html.js | 111 ++++++++++++++++++ app/scripts/const/links.js | 1 + app/scripts/locales/base.json | 9 +- app/scripts/models/app-settings-model.js | 1 + app/scripts/models/file-model.js | 9 ++ .../views/settings/settings-file-view.js | 11 +- app/templates/export/db.hbs | 55 +++++++++ app/templates/export/entry.hbs | 52 ++++++++ app/templates/settings/settings-file.hbs | 5 + release-notes.md | 5 +- 10 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 app/scripts/comp/kdbx-to-html.js create mode 100644 app/templates/export/db.hbs create mode 100644 app/templates/export/entry.hbs diff --git a/app/scripts/comp/kdbx-to-html.js b/app/scripts/comp/kdbx-to-html.js new file mode 100644 index 00000000..b334b571 --- /dev/null +++ b/app/scripts/comp/kdbx-to-html.js @@ -0,0 +1,111 @@ +const Format = require('../util/format'); +const Locale = require('../util/locale'); +const Links = require('../const/links'); +const RuntimeInfo = require('./runtime-info'); +const kdbxweb = require('kdbxweb'); + +const Templates = { + db: require('templates/export/db.hbs'), + entry: require('templates/export/entry.hbs') +}; + +const FieldMapping = [ + { name: 'Password', locStr: 'password', protect: true }, + { name: 'UserName', locStr: 'user' }, + { name: 'URL', locStr: 'website' }, + { name: 'Notes', locStr: 'notes' } +]; + +const KnownFields = { 'Title': true }; +for (const { name } of FieldMapping) { + KnownFields[name] = true; +} + +function walkGroup(db, group, parents) { + parents = [...parents, group]; + if ( + group.uuid.equals(db.meta.recycleBinUuid) || + group.uuid.equals(db.meta.entryTemplatesGroup) + ) { + return ''; + } + const self = group.entries.map(entry => walkEntry(db, entry, parents)).join('\n'); + const children = group.groups.map(childGroup => walkGroup(db, childGroup, parents)).join('\n'); + return self + children; +} + +function walkEntry(db, entry, parents) { + const path = parents.map(group => group.name).join(' / '); + const fields = []; + for (const field of FieldMapping) { + const value = entryField(entry, field.name); + if (value) { + fields.push({ + title: Format.capFirst(Locale[field.locStr]), + value, + protect: field.protect + }); + } + } + for (const fieldName of Object.keys(entry.fields)) { + if (!KnownFields[fieldName]) { + const value = entryField(entry, fieldName); + if (value) { + fields.push({ + title: fieldName, + value, + protect: entry.fields[fieldName].isProtected + }); + } + } + } + const title = entryField(entry, 'Title'); + let expires; + if (entry.times.expires && entry.times.expiryTime) { + expires = Format.dtStr(entry.times.expiryTime); + } + + const attachments = Object.entries(entry.binaries) + .map(([name, data]) => { + if (data && data.ref) { + data = data.value; + } + if (data) { + const base64 = kdbxweb.ByteUtils.bytesToBase64(data); + data = 'data:application/octet-stream;base64,' + base64; + } + return { name, data }; + }) + .filter(att => att.name && att.data); + + return Templates.entry({ + path, + title, + fields, + tags: entry.tags.join(', '), + created: Format.dtStr(entry.times.creationTime), + modified: Format.dtStr(entry.times.lastModTime), + expires, + attachments + }); +} + +function entryField(entry, fieldName) { + const value = entry.fields[fieldName]; + return (value && value.isProtected && value.getText()) || value || ''; +} + +const KdbxToHtml = { + convert(db, options) { + const content = db.groups.map(group => walkGroup(db, group, [])).join('\n'); + return Templates.db({ + name: options.name, + date: Format.dtStr(Date.now()), + appLink: Links.Homepage, + appVersion: RuntimeInfo.version, + content + }); + } +}; + +module.exports = KdbxToHtml; diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index 371a44ff..cf47102e 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -1,4 +1,5 @@ const Links = { + Homepage: 'https://keeweb.info', Repo: 'https://github.com/keeweb/keeweb', Desktop: 'https://github.com/keeweb/keeweb/releases/latest', WebApp: 'https://app.keeweb.info', diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 31b3f675..13fcf5d2 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -434,6 +434,7 @@ "setFileSync": "Sync", "setFileSyncVerb": "Sync", "setFileSaveToXml": "XML", + "setFileSaveToHtml": "HTML", "setFileLastSync": "Last sync", "setFileLastSyncUnknown": "unknown", "setFileSyncInProgress": "sync in progress", @@ -585,5 +586,11 @@ "launcherFileFilter": "KeePass files", "authPopupRequired": "Pop-ups are blocked", - "authPopupRequiredBody": "Please allow pop-ups in your browser or try again." + "authPopupRequiredBody": "Please allow pop-ups in your browser or try again.", + + "exportFileInfo": "File information", + "exportHtmlName": "Name", + "exportHtmlDate": "Export date", + "exportGenerator": "Software", + "exportDescription": "This file is generated with {}." } diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 6639d2e8..44c74103 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -43,6 +43,7 @@ const AppSettingsModel = Backbone.Model.extend({ canImportXml: true, canRemoveLatest: true, canExportXml: true, + canExportHtml: true, dropbox: true, webdav: true, diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 2825b128..92d674ec 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -3,6 +3,7 @@ const GroupCollection = require('../collections/group-collection'); const GroupModel = require('./group-model'); const IconUrl = require('../util/icon-url'); const Logger = require('../util/logger'); +const KdbxToHtml = require('../comp/kdbx-to-html'); const kdbxweb = require('kdbxweb'); const demoFileData = require('demo.kdbx'); @@ -420,6 +421,14 @@ const FileModel = Backbone.Model.extend({ }); }, + getHtml(cb) { + cb( + KdbxToHtml.convert(this.db, { + name: this.get('name') + }) + ); + }, + getKeyFileHash() { const hash = this.db.credentials.keyFileHash; return hash ? kdbxweb.ByteUtils.bytesToBase64(hash.getBinary()) : null; diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index a7283dda..b09c6501 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -25,6 +25,7 @@ const SettingsFileView = Backbone.View.extend({ 'click .settings__file-button-close': 'closeFile', 'click .settings__file-save-to-file': 'saveToFile', 'click .settings__file-save-to-xml': 'saveToXml', + 'click .settings__file-save-to-html': 'saveToHtml', 'click .settings__file-save-to-storage': 'saveToStorage', 'change #settings__file-key-file': 'keyFileChange', 'click #settings__file-file-select-link': 'triggerSelectFile', @@ -107,7 +108,8 @@ const SettingsFileView = Backbone.View.extend({ kdfParameters: this.kdfParametersToUi(this.model.get('kdfParameters')), storageProviders, canBackup, - canExportXml: AppSettingsModel.instance.get('canExportXml') + canExportXml: AppSettingsModel.instance.get('canExportXml'), + canExportHtml: AppSettingsModel.instance.get('canExportHtml') }); if (!this.model.get('created')) { this.$el @@ -256,6 +258,13 @@ const SettingsFileView = Backbone.View.extend({ }); }, + saveToHtml() { + this.model.getHtml(html => { + const blob = new Blob([html], { type: 'text/html' }); + FileSaver.saveAs(blob, this.model.get('name') + '.html'); + }); + }, + saveToStorage(e) { if (this.model.get('syncing') || this.model.get('demo')) { return; diff --git a/app/templates/export/db.hbs b/app/templates/export/db.hbs new file mode 100644 index 00000000..e3257c3c --- /dev/null +++ b/app/templates/export/db.hbs @@ -0,0 +1,55 @@ + + + + + {{name}} + + + + +

{{name}}

+

{{res 'exportFileInfo'}}

+ + + + + + + + + + + + + +
{{res 'exportHtmlName'}}{{name}}
{{res 'exportHtmlDate'}}{{date}}
{{res 'exportGenerator'}}KeeWeb v{{appVersion}}
+

{{res 'exportEntries'}}

+

+ {{{content}}} +

+

+ {{#res 'exportDescription'}}KeeWeb{{/res}} +

+ + diff --git a/app/templates/export/entry.hbs b/app/templates/export/entry.hbs new file mode 100644 index 00000000..ff88ca0c --- /dev/null +++ b/app/templates/export/entry.hbs @@ -0,0 +1,52 @@ +

+ {{#if title}}{{title}}{{else}}({{Res 'noTitle'}}){{/if}} +

+ + {{#each fields as |field|}} + + + + + {{/each}} + {{#if tags}} + + + + + {{/if}} + + + + + {{#if attachments.length}} + + + + + {{/if}} + + + + + + + + + {{#if expires}} + + + + + {{/if}} +
{{field.title}} + {{#if field.protect}} + {{field.value}} + {{else}} + {{field.value}} + {{/if}} +
{{Res 'tags'}}{{tags}}
{{Res 'group'}}{{path}}
{{res 'detAttachments'}} + {{#each attachments as |attachment|}} + {{attachment.name}} + {{~#unless @last}}, {{/unless}} + {{/each}} +
{{res 'detCreated'}}{{created}}
{{res 'detUpdated'}}{{modified}}
{{res 'detExpires'}}{{expires}}
diff --git a/app/templates/settings/settings-file.hbs b/app/templates/settings/settings-file.hbs index 3e9cbc96..f3291cfb 100644 --- a/app/templates/settings/settings-file.hbs +++ b/app/templates/settings/settings-file.hbs @@ -41,6 +41,11 @@ {{res 'setFileSaveToXml'}} {{/if}} + {{#if canExportHtml}} +
+ {{res 'setFileSaveToHtml'}} +
+ {{/if}} {{#if storage}} diff --git a/release-notes.md b/release-notes.md index 96be5ce4..829ebeea 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,9 +1,11 @@ Release notes ------------- ##### v1.10 (TBD) +`+` macOS Dark theme +`+` pretty-printing exported XML files +`+` HTML export `+` config option to disable xml export (canExportXml) `+` xml files can be now opened as regular files -`+` macOS Dark theme `-` fix #1154: relative Destination header in WebDAV MOVE `-` fix #1129: webdav storage error on Unicode filenames `*` donation link changed @@ -11,7 +13,6 @@ Release notes `*` dropped support for browsers without css variables `*` displaying websites as HTTPS if no scheme is provided `+` confirmation for deleting an entry on mobile -`+` pretty-printing exported XML files ##### v1.9.3 (2019-09-07) `-` fixed group settings not being displayed