/* * 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/Database.h" #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(""; } } // 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.iconScaledPixmap())); 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; } } // Output the entries in this group for (const auto entry : entries) { auto item = QString("

"); // Begin formatting this item into HTML item.append(PixmapToHTML(entry->iconScaledPixmap())); item.append(" "); item.append(entry->title().toHtmlEscaped()); item.append("

\n" ""); // Output the fixed fields const auto& u = entry->username(); if (!u.isEmpty()) { item.append(""); } const auto& p = entry->password(); if (!p.isEmpty()) { item.append(""); } const auto& r = entry->url(); if (!r.isEmpty()) { item.append(""); } const auto& n = entry->notes(); if (!n.isEmpty()) { item.append(""); } // 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(""); } } // Done with this entry item.append("
"); item.append(QObject::tr("User name")); item.append(""); item.append(entry->username().toHtmlEscaped()); item.append("
"); item.append(QObject::tr("Password")); item.append(""); item.append(entry->password().toHtmlEscaped()); item.append("
"); item.append(QObject::tr("URL")); item.append(""); // Restrict the length of what we display of the URL - // even from a paper backup, nobody will every type in // more than 100 characters of a URL constexpr auto maxlen = 100; if (r.size() <= maxlen) { item.append(r.toHtmlEscaped()); } else { item.append(r.mid(0, maxlen).toHtmlEscaped()); item.append("…"); } item.append("
"); item.append(QObject::tr("Notes")); item.append(""); item.append(entry->notes().toHtmlEscaped().replace("\n", "
")); item.append("
"); item.append(key.toHtmlEscaped()); item.append(""); item.append(attr->value(key).toHtmlEscaped().replace("\n", "
")); item.append("
\n"); if (device.write(item.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; }