mirror of https://github.com/keeweb/keeweb.git
kdbx-to-html
This commit is contained in:
parent
38466f934f
commit
5c0756855a
|
@ -725,6 +725,7 @@
|
|||
"exportHtmlDate": "Export date",
|
||||
"exportGenerator": "Software",
|
||||
"exportDescription": "This file is generated with {}.",
|
||||
"exportEntries": "Entries",
|
||||
"importCsvTitle": "Import from CSV",
|
||||
"importCsvRun": "Import",
|
||||
"importIgnoreField": "Ignore",
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
/* eslint-disable import/no-commonjs */
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Links } from 'const/links';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
const Templates = {
|
||||
db: require('templates/export/db.hbs'),
|
||||
entry: require('templates/export/entry.hbs')
|
||||
};
|
||||
|
||||
const FieldMapping = [
|
||||
{ name: 'UserName', locStr: 'user' },
|
||||
{ name: 'Password', locStr: 'password', protect: true },
|
||||
{ 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: StringFormat.capFirst(Locale[field.locStr]),
|
||||
value,
|
||||
protect: field.protect
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [fieldName, fieldValue] of entry.fields) {
|
||||
if (!KnownFields[fieldName]) {
|
||||
const value = entryField(entry, fieldName);
|
||||
if (value) {
|
||||
fields.push({
|
||||
title: fieldName,
|
||||
value,
|
||||
protect: fieldValue.isProtected
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const title = entryField(entry, 'Title');
|
||||
let expires;
|
||||
if (entry.times.expires && entry.times.expiryTime) {
|
||||
expires = DateFormat.dtStr(entry.times.expiryTime);
|
||||
}
|
||||
|
||||
const attachments = [...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: DateFormat.dtStr(entry.times.creationTime),
|
||||
modified: DateFormat.dtStr(entry.times.lastModTime),
|
||||
expires,
|
||||
attachments
|
||||
});
|
||||
}
|
||||
|
||||
function entryField(entry, fieldName) {
|
||||
const value = entry.fields.get(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: DateFormat.dtStr(Date.now()),
|
||||
appLink: Links.Homepage,
|
||||
appVersion: RuntimeInfo.version,
|
||||
contentHtml: content
|
||||
});
|
||||
},
|
||||
|
||||
entryToHtml(db, entry) {
|
||||
return walkEntry(db, entry, []);
|
||||
}
|
||||
};
|
||||
|
||||
export { KdbxToHtml };
|
|
@ -0,0 +1,136 @@
|
|||
import { h } from 'preact';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Links } from 'const/links';
|
||||
import { DateFormat } from 'util/formatting/date-format';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { HtmlRenderer } from 'util/browser/html-renderer';
|
||||
import { ExportFile } from 'views/standalone/export-file';
|
||||
import { ExportEntry, ExportEntryParameters } from 'views/standalone/export-entry';
|
||||
|
||||
const FieldMapping = [
|
||||
{ name: 'UserName', loc: () => Locale.user },
|
||||
{ name: 'Password', loc: () => Locale.password, protect: true },
|
||||
{ name: 'URL', loc: () => Locale.website },
|
||||
{ name: 'Notes', loc: () => Locale.notes }
|
||||
];
|
||||
|
||||
const KnownFields = new Set('Title');
|
||||
for (const { name } of FieldMapping) {
|
||||
KnownFields.add(name);
|
||||
}
|
||||
|
||||
function convertEntry(db: kdbxweb.Kdbx, entry: kdbxweb.KdbxEntry): ExportEntryParameters {
|
||||
const path = getParents(entry).join(' / ');
|
||||
const fields = [];
|
||||
for (const field of FieldMapping) {
|
||||
const value = entryField(entry, field.name);
|
||||
if (value) {
|
||||
fields.push({
|
||||
title: StringFormat.capFirst(field.loc()),
|
||||
value,
|
||||
protect: field.protect
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [fieldName, fieldValue] of entry.fields) {
|
||||
if (!KnownFields.has(fieldName)) {
|
||||
const value = entryField(entry, fieldName);
|
||||
if (value) {
|
||||
fields.push({
|
||||
title: fieldName,
|
||||
value,
|
||||
protect: fieldValue instanceof kdbxweb.ProtectedValue
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const title = entryField(entry, 'Title');
|
||||
let expires;
|
||||
if (entry.times.expires && entry.times.expiryTime) {
|
||||
expires = DateFormat.dtStr(entry.times.expiryTime);
|
||||
}
|
||||
|
||||
const attachments = [...entry.binaries]
|
||||
.map(([name, data]) => {
|
||||
let value;
|
||||
if (kdbxweb.KdbxBinaries.isKdbxBinaryWithHash(data)) {
|
||||
value = data.value;
|
||||
} else {
|
||||
value = data;
|
||||
}
|
||||
if (value instanceof kdbxweb.ProtectedValue) {
|
||||
value = value.getBinary();
|
||||
}
|
||||
let dataHref = '';
|
||||
if (value) {
|
||||
const base64 = kdbxweb.ByteUtils.bytesToBase64(value);
|
||||
dataHref = 'data:application/octet-stream;base64,' + base64;
|
||||
}
|
||||
return { name, dataHref };
|
||||
})
|
||||
.filter((att) => att.name && att.dataHref);
|
||||
|
||||
return {
|
||||
id: entry.uuid.id,
|
||||
path,
|
||||
title,
|
||||
fields,
|
||||
tags: entry.tags,
|
||||
created: entry.times.creationTime ? DateFormat.dtStr(entry.times.creationTime) : '',
|
||||
modified: entry.times.lastModTime ? DateFormat.dtStr(entry.times.lastModTime) : '',
|
||||
expires,
|
||||
attachments
|
||||
};
|
||||
}
|
||||
|
||||
function entryField(entry: kdbxweb.KdbxEntry, fieldName: string): string {
|
||||
const value = entry.fields.get(fieldName);
|
||||
if (value instanceof kdbxweb.ProtectedValue) {
|
||||
return value.getText();
|
||||
}
|
||||
return value || '';
|
||||
}
|
||||
|
||||
function getParents(entry: kdbxweb.KdbxEntry): string[] {
|
||||
const parents = [];
|
||||
const group = entry.parentGroup;
|
||||
while (group?.name) {
|
||||
parents.push(group.name);
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
const KdbxToHtml = {
|
||||
convert(db: kdbxweb.Kdbx, name: string): string {
|
||||
const entries: ExportEntryParameters[] = [];
|
||||
for (const group of db.groups) {
|
||||
if (
|
||||
group.uuid.equals(db.meta.recycleBinUuid) ||
|
||||
group.uuid.equals(db.meta.entryTemplatesGroup)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of group.allEntries()) {
|
||||
entries.push(convertEntry(db, entry));
|
||||
}
|
||||
}
|
||||
return HtmlRenderer.renderToHtml(
|
||||
h(ExportFile, {
|
||||
name,
|
||||
date: DateFormat.dtStr(Date.now()),
|
||||
appVersion: RuntimeInfo.version,
|
||||
appLink: Links.Homepage,
|
||||
entries
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
entryToHtml(db: kdbxweb.Kdbx, entry: kdbxweb.KdbxEntry): string {
|
||||
const params = convertEntry(db, entry);
|
||||
return HtmlRenderer.renderToHtml(h(ExportEntry, params));
|
||||
}
|
||||
};
|
||||
|
||||
export { KdbxToHtml };
|
|
@ -1,7 +1,7 @@
|
|||
const BaseLocale = require('locales/base.json') as Record<string, string>;
|
||||
const BaseLocaleName = 'en-US';
|
||||
|
||||
interface LocWithReplace {
|
||||
export interface LocWithReplace {
|
||||
with(value: string): string;
|
||||
}
|
||||
|
||||
|
@ -756,6 +756,7 @@ export const Locale = {
|
|||
get exportHtmlDate(): string { return get('exportHtmlDate'); },
|
||||
get exportGenerator(): string { return get('exportGenerator'); },
|
||||
exportDescription: withReplace('exportDescription'),
|
||||
get exportEntries(): string { return get('exportEntries'); },
|
||||
get importCsvTitle(): string { return get('importCsvTitle'); },
|
||||
get importCsvRun(): string { return get('importCsvRun'); },
|
||||
get importIgnoreField(): string { return get('importIgnoreField'); },
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { FunctionComponent } from 'preact';
|
||||
import { LocWithReplace } from 'util/locale';
|
||||
|
||||
export const LocalizedWith: FunctionComponent<{ str: LocWithReplace }> = ({ str, children }) => {
|
||||
const [first, ...rest] = str.with('{}').split('{}');
|
||||
return (
|
||||
<>
|
||||
{first}
|
||||
{children}
|
||||
{rest}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
import { FunctionComponent } from 'preact';
|
||||
import { Locale } from 'util/locale';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
|
||||
export interface ExportEntryField {
|
||||
title: string;
|
||||
protect?: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ExportEntryAttachment {
|
||||
name: string;
|
||||
dataHref: string;
|
||||
}
|
||||
|
||||
export interface ExportEntryParameters {
|
||||
id: string;
|
||||
title: string;
|
||||
fields: ExportEntryField[];
|
||||
tags?: string[];
|
||||
path?: string;
|
||||
created: string;
|
||||
modified: string;
|
||||
expires?: string;
|
||||
attachments?: ExportEntryAttachment[];
|
||||
}
|
||||
|
||||
export const ExportEntry: FunctionComponent<ExportEntryParameters> = ({
|
||||
title,
|
||||
fields,
|
||||
tags,
|
||||
path,
|
||||
created,
|
||||
modified,
|
||||
expires,
|
||||
attachments
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h2>{title || StringFormat.capFirst(Locale.noTitle)}</h2>
|
||||
<table>
|
||||
{fields.map((field) => (
|
||||
<tr key={`field-${field.title}`}>
|
||||
<td>{field.title}</td>
|
||||
<td class="field">
|
||||
{field.protect ? <code>{field.value}</code> : field.value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{tags?.length ? (
|
||||
<tr key="tags">
|
||||
<td>{StringFormat.capFirst(Locale.tags)}</td>
|
||||
<td>{tags.join(', ')}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{path ? (
|
||||
<tr key="path">
|
||||
<td>{StringFormat.capFirst(Locale.group)}</td>
|
||||
<td>{path}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{attachments?.length ? (
|
||||
<tr key="attachments">
|
||||
<td>{Locale.detAttachments}</td>
|
||||
<td>
|
||||
{attachments.map((att, ix) => (
|
||||
<span key={att.name}>
|
||||
<a href={att.dataHref} download={att.name}>
|
||||
{att.name}
|
||||
</a>
|
||||
{ix < attachments?.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
<tr key="created">
|
||||
<td>{Locale.detCreated}</td>
|
||||
<td>{created}</td>
|
||||
</tr>
|
||||
<tr key="modified">
|
||||
<td>{Locale.detUpdated}</td>
|
||||
<td>{modified}</td>
|
||||
</tr>
|
||||
{expires ? (
|
||||
<tr key="expires">
|
||||
<td>{StringFormat.capFirst(Locale.detExpires)}</td>
|
||||
<td>{expires}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import { FunctionComponent } from 'preact';
|
||||
import { Locale } from 'util/locale';
|
||||
import { LocalizedWith } from 'views/helpers/localized';
|
||||
import { ExportEntry, ExportEntryParameters } from './export-entry';
|
||||
|
||||
export const ExportFile: FunctionComponent<{
|
||||
name: string;
|
||||
date: string;
|
||||
appVersion: string;
|
||||
appLink: string;
|
||||
entries: ExportEntryParameters[];
|
||||
}> = ({ name, date, appVersion, appLink, entries }) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="UTF-8" />
|
||||
<title>{{ name }}</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="script-src 'none'; img-src data:; style-src 'unsafe-inline';"
|
||||
/>
|
||||
<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%;
|
||||
}
|
||||
td.field {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ name }}</h1>
|
||||
<h2>{Locale.exportFileInfo}</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>{Locale.exportHtmlName}</td>
|
||||
<td>{{ name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{Locale.exportHtmlDate}</td>
|
||||
<td>{{ date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{Locale.exportGenerator}</td>
|
||||
<td>KeeWeb v{{ appVersion }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2>{Locale.exportEntries}</h2>
|
||||
<div>
|
||||
{entries.map((entry) => (
|
||||
<ExportEntry key={entry.id} {...entry} />
|
||||
))}
|
||||
</div>
|
||||
<footer>
|
||||
<LocalizedWith str={Locale.exportDescription}>
|
||||
<a href={appLink} rel="noreferrer noopener" target="_blank">
|
||||
KeeWeb
|
||||
</a>
|
||||
</LocalizedWith>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{name}}</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'none'; img-src data:; style-src 'unsafe-inline';" />
|
||||
<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%;
|
||||
}
|
||||
td.field {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</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>
|
||||
<div>
|
||||
{{{contentHtml}}}
|
||||
</div>
|
||||
<footer>
|
||||
{{#res 'exportDescription'}}<a href="{{appLink}}" rel="noreferrer noopener" target="_blank">KeeWeb</a>{{/res}}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -1,56 +0,0 @@
|
|||
<h2>
|
||||
{{#if title}}{{title}}{{else}}({{Res 'noTitle'}}){{/if}}
|
||||
</h2>
|
||||
<table>
|
||||
{{#each fields as |field|}}
|
||||
<tr>
|
||||
<td>{{field.title}}</td>
|
||||
<td class="field">
|
||||
{{~#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}}
|
||||
{{#if path}}
|
||||
<tr>
|
||||
<td>{{Res 'group'}}</td>
|
||||
<td>{{path}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#if attachments}}
|
||||
{{#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}}, {{/unless}}
|
||||
{{/each}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{/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>
|
Loading…
Reference in New Issue