/* * 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" #include "gui/Icons.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 formatEntry(const Entry& entry) { // Here we collect the table rows with this entry's data fields QString item; // Output the fixed fields const auto& u = entry.username(); if (!u.isEmpty()) { item.append(""); item.append(QObject::tr("User name")); item.append(""); item.append(entry.username().toHtmlEscaped()); item.append(""); } const auto& p = entry.password(); if (!p.isEmpty()) { item.append(""); item.append(QObject::tr("Password")); item.append(""); item.append(entry.password().toHtmlEscaped()); item.append(""); } const auto& r = entry.url(); if (!r.isEmpty()) { 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(""); } // 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(""); item.append(key.toHtmlEscaped()); item.append(""); item.append(attr->value(key).toHtmlEscaped().replace(" ", " ").replace("\n", "
")); item.append(""); } } const auto& n = entry.notes(); if (!n.isEmpty()) { item.append(""); item.append(QObject::tr("Notes")); item.append(""); item.append(entry.notes().toHtmlEscaped().replace("\n", "
")); item.append(""); } return item; } } // namespace bool HtmlExporter::exportDatabase(const QString& filename, const QSharedPointer& db, bool sorted, bool ascending) { QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_error = file.errorString(); return false; } return exportDatabase(&file, db, sorted, ascending); } QString HtmlExporter::errorString() const { return m_error; } bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointer& db, bool sorted, bool ascending) { 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(), QString(), sorted, ascending)) { 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, bool sorted, bool ascending) { // 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 notes = group.notes(); if (!group.entries().empty() || !notes.isEmpty()) { // Header line auto header = QString("

"); header.append(PixmapToHTML(Icons::groupIconPixmap(&group, 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(""); auto entries = group.entries(); if (sorted) { std::sort(entries.begin(), entries.end(), [&](Entry* lhs, Entry* rhs) { int cmp = lhs->title().compare(rhs->title(), Qt::CaseInsensitive); return ascending ? cmp < 0 : cmp > 0; }); } // 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 += ""; auto caption = ""; // ... then the right side with the data fields table += ""; table += ""; } // Output the complete table of this group table.append("
" + PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium)) + "
" + entry->title().toHtmlEscaped() + "
" + caption + formatted_entry + "
\n"); if (device.write(table.toUtf8()) == -1) { m_error = device.errorString(); return false; } auto children = group.children(); if (sorted) { std::sort(children.begin(), children.end(), [&](Group* lhs, Group* rhs) { int cmp = lhs->name().compare(rhs->name(), Qt::CaseInsensitive); return ascending ? cmp < 0 : cmp > 0; }); } // Recursively output the child groups for (const auto* child : children) { if (child && !writeGroup(device, *child, path, sorted, ascending)) { return false; } } return true; }