/* * Copyright (C) 2019 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 or (at your option) * version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "HtmlExporter.h" #include #include #include "core/Group.h" #include "core/Metadata.h" namespace { QString PixmapToHTML(const QPixmap& pixmap) { if (pixmap.isNull()) { return ""; } // Based on https://stackoverflow.com/a/6621278 QByteArray a; QBuffer buffer(&a); pixmap.save(&buffer, "PNG"); return QString(""; } QString formatHTML(const QString& value) { return value.toHtmlEscaped().replace(" ", " ").replace('\n', "
"); } QString formatAttribute(const QString& key, const QString& value, const QString& classname, const QString& templt = QString("%1%3")) { const auto& formatted_attribute = templt; if (!value.isEmpty()) { // Format key as well -> Translations into other languages may have non-standard chars return formatted_attribute.arg(formatHTML(key), classname, formatHTML(value)); } return {}; } QString formatAttribute(const Entry& entry, const QString& key, const QString& value, const QString& classname, const QString& templt = QString("%1%3")) { if (value.isEmpty()) return {}; return formatAttribute(key, entry.resolveMultiplePlaceholders(value), classname, templt); } QString formatEntry(const Entry& entry) { // Here we collect the table rows with this entry's data fields QString item; // Output the fixed fields item.append(formatAttribute(entry, QObject::tr("User name"), entry.username(), "username")); item.append(formatAttribute(entry, QObject::tr("Password"), entry.password(), "password")); if (!entry.url().isEmpty()) { constexpr auto maxlen = 100; QString displayedURL(formatHTML(entry.url()).mid(0, maxlen)); if (displayedURL.size() == maxlen) { displayedURL.append("…"); } item.append(formatAttribute(entry, QObject::tr("URL"), entry.url(), "url", R"(%1%4)") .arg(entry.resolveMultiplePlaceholders(displayedURL))); } item.append(formatAttribute(entry, QObject::tr("Notes"), entry.notes(), "notes")); // Now add the attributes (if there are any) const auto* const attr = entry.attributes(); if (attr && !attr->customKeys().isEmpty()) { for (const auto& key : attr->customKeys()) { item.append(formatAttribute(entry, key, attr->value(key), "attr")); } } return item; } } // namespace bool HtmlExporter::exportDatabase(const QString& filename, const QSharedPointer& db) { QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_error = file.errorString(); return false; } return exportDatabase(&file, db); } QString HtmlExporter::errorString() const { return m_error; } bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointer& db) { const auto meta = db->metadata(); if (!meta) { m_error = "Internal error: metadata is NULL"; return false; } const auto header = QString("" "" "" "" + meta->name().toHtmlEscaped() + "" "" "\n" "" "

" + meta->name().toHtmlEscaped() + "

" "

" + meta->description().toHtmlEscaped().replace("\n", "
") + "

" "

" + db->filePath().toHtmlEscaped() + "

"); const auto footer = QString("" ""); if (device->write(header.toUtf8()) == -1) { m_error = device->errorString(); return false; } if (db->rootGroup()) { if (!writeGroup(*device, *db->rootGroup())) { return false; } } if (device->write(footer.toUtf8()) == -1) { m_error = device->errorString(); return false; } return true; } bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path) { // Don't output the recycle bin if (&group == group.database()->metadata()->recycleBin()) { return true; } if (!path.isEmpty()) { path.append(" → "); } path.append(group.name().toHtmlEscaped()); // Output the header for this group (but only if there are // any notes or entries in this group, otherwise we'd get // a header with nothing after it, which looks stupid) const auto& entries = group.entries(); const auto notes = group.notes(); if (!entries.empty() || !notes.isEmpty()) { // Header line auto header = QString("

"); header.append(PixmapToHTML(group.iconPixmap(IconSize::Medium))); header.append(" "); header.append(path); header.append("

\n"); // Group notes if (!notes.isEmpty()) { header.append("

"); header.append(notes.toHtmlEscaped().replace("\n", "
")); header.append("

"); } // Output it if (device.write(header.toUtf8()) == -1) { m_error = device.errorString(); return false; } } // Begin the table for the entries in this group auto table = QString(""); // Output the entries in this group for (const auto entry : entries) { auto formatted_entry = formatEntry(*entry); if (formatted_entry.isEmpty()) continue; // Output it into our table. First the left side with // icon and entry title ... table += ""; table += ""; table += ""; // ... then the right side with the data fields table += ""; table += ""; } // Output the complete table of this group table.append("
" + PixmapToHTML(entry->iconPixmap(IconSize::Medium)) + "

" + entry->title().toHtmlEscaped() + "

" + formatted_entry + "
\n"); if (device.write(table.toUtf8()) == -1) { m_error = device.errorString(); return false; } // Recursively output the child groups const auto& children = group.children(); for (const auto child : children) { if (child && !writeGroup(device, *child, path)) { return false; } } return true; }