HTML export

This commit is contained in:
antelle 2019-09-08 20:28:02 +02:00
parent 15b170db5d
commit 5703dceb0c
10 changed files with 255 additions and 4 deletions

View File

@ -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;

View File

@ -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',

View File

@ -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 {}."
}

View File

@ -43,6 +43,7 @@ const AppSettingsModel = Backbone.Model.extend({
canImportXml: true,
canRemoveLatest: true,
canExportXml: true,
canExportHtml: true,
dropbox: true,
webdav: true,

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{name}}</title>
<link href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=" rel="icon" type="image/x-icon" />
<style>
body {
font-family: -apple-system, "BlinkMacSystemFont", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
font-size: 14px;
padding: 10px 20px;
}
table {
border-collapse: collapse;
border: 1px solid #ccc;
width: 100%;
}
td {
border: 1px solid #ccc;
padding: 8px 16px;
}
tr:nth-of-type(even) {
background: #fafafa;
}
td:first-of-type {
width: 30%;
}
</style>
</head>
<body>
<h1>{{name}}</h1>
<h2>{{res 'exportFileInfo'}}</h2>
<table>
<tr>
<td>{{res 'exportHtmlName'}}</td>
<td>{{name}}</td>
</tr>
<tr>
<td>{{res 'exportHtmlDate'}}</td>
<td>{{date}}</td>
</tr>
<tr>
<td>{{res 'exportGenerator'}}</td>
<td>KeeWeb v{{appVersion}}</td>
</tr>
</table>
<h2>{{res 'exportEntries'}}</h2>
<p>
{{{content}}}
</p>
<p>
{{#res 'exportDescription'}}<a href="{{appLink}}" rel="noreferrer noopener" target="_blank">KeeWeb</a>{{/res}}
</p>
</body>
</html>

View File

@ -0,0 +1,52 @@
<h2>
{{#if title}}{{title}}{{else}}({{Res 'noTitle'}}){{/if}}
</h2>
<table>
{{#each fields as |field|}}
<tr>
<td>{{field.title}}</td>
<td>
{{#if field.protect}}
<code>{{field.value}}</code>
{{else}}
{{field.value}}
{{/if}}
</td>
</tr>
{{/each}}
{{#if tags}}
<tr>
<td>{{Res 'tags'}}</td>
<td>{{tags}}</td>
</tr>
{{/if}}
<tr>
<td>{{Res 'group'}}</td>
<td>{{path}}</td>
</tr>
{{#if attachments.length}}
<tr>
<td>{{res 'detAttachments'}}</td>
<td>
{{#each attachments as |attachment|}}
<a href="{{{attachment.data}}}" download="{{attachment.name}}">{{attachment.name}}</a>
{{~#unless @last}},&nbsp;{{/unless}}
{{/each}}
</td>
</tr>
{{/if}}
<tr>
<td>{{res 'detCreated'}}</td>
<td>{{created}}</td>
</tr>
<tr>
<td>{{res 'detUpdated'}}</td>
<td>{{modified}}</td>
</tr>
{{#if expires}}
<tr>
<td>{{res 'detExpires'}}</td>
<td>{{expires}}</td>
</tr>
{{/if}}
</table>

View File

@ -41,6 +41,11 @@
<i class="fa fa-code"></i>{{res 'setFileSaveToXml'}}
</div>
{{/if}}
{{#if canExportHtml}}
<div class="settings__file-save-to settings__file-save-to-html">
<i class="fa fa-html5"></i>{{res 'setFileSaveToHtml'}}
</div>
{{/if}}
</div>
{{#if storage}}

View File

@ -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