From 004f2b6801d93116277778611920793b7dbe0afb Mon Sep 17 00:00:00 2001 From: louib Date: Sat, 13 Mar 2021 14:07:49 -0500 Subject: Removing QWidget dependency from src/core. --- src/CMakeLists.txt | 4 +- src/autotype/AutoTypeMatchModel.cpp | 8 +- src/cli/CMakeLists.txt | 2 +- src/core/DatabaseIcons.cpp | 118 --------- src/core/DatabaseIcons.h | 59 ----- src/core/Entry.cpp | 39 +-- src/core/Entry.h | 4 +- src/core/Global.h | 7 - src/core/Group.cpp | 48 +--- src/core/Group.h | 4 +- src/core/Merger.cpp | 3 +- src/core/Metadata.cpp | 73 ++---- src/core/Metadata.h | 16 +- src/core/Tools.cpp | 22 +- src/core/Tools.h | 1 - src/fdosecrets/widgets/AccessControlDialog.cpp | 3 +- src/fdosecrets/widgets/SettingsModels.cpp | 2 +- src/format/HtmlExporter.cpp | 263 -------------------- src/format/HtmlExporter.h | 41 ---- src/format/KdbxXmlReader.cpp | 11 +- src/format/KdbxXmlWriter.cpp | 11 +- src/format/KdbxXmlWriter.h | 3 +- src/format/KeePass1Reader.cpp | 6 +- src/gui/DatabaseIcons.cpp | 119 ++++++++++ src/gui/DatabaseIcons.h | 65 +++++ src/gui/DatabaseTabWidget.cpp | 5 +- src/gui/EditWidgetIcons.cpp | 17 +- src/gui/EntryPreviewWidget.cpp | 4 +- src/gui/HtmlExporter.cpp | 264 +++++++++++++++++++++ src/gui/HtmlExporter.h | 41 ++++ src/gui/IconDownloaderDialog.cpp | 14 +- src/gui/IconModels.cpp | 2 +- src/gui/Icons.cpp | 103 ++++++++ src/gui/Icons.h | 11 + .../DatabaseSettingsWidgetMaintenance.cpp | 3 +- src/gui/entry/EntryModel.cpp | 6 +- src/gui/group/GroupModel.cpp | 6 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 4 +- src/gui/reports/ReportsWidgetHibp.cpp | 4 +- src/keeshare/KeeShare.cpp | 5 +- src/keeshare/KeeShareSettings.cpp | 6 + src/keeshare/ShareExport.cpp | 3 +- src/qrcode/CMakeLists.txt | 2 +- 43 files changed, 719 insertions(+), 713 deletions(-) delete mode 100644 src/core/DatabaseIcons.cpp delete mode 100644 src/core/DatabaseIcons.h delete mode 100644 src/format/HtmlExporter.cpp delete mode 100644 src/format/HtmlExporter.h create mode 100644 src/gui/DatabaseIcons.cpp create mode 100644 src/gui/DatabaseIcons.h create mode 100644 src/gui/HtmlExporter.cpp create mode 100644 src/gui/HtmlExporter.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fff80238f..430515185 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,7 +39,6 @@ set(keepassx_SOURCES core/Config.cpp core/CustomData.cpp core/Database.cpp - core/DatabaseIcons.cpp core/Entry.cpp core/EntryAttachments.cpp core/EntryAttributes.cpp @@ -71,7 +70,6 @@ set(keepassx_SOURCES crypto/kdf/Argon2Kdf.cpp format/CsvExporter.cpp format/CsvParser.cpp - format/HtmlExporter.cpp format/KeePass1Reader.cpp format/KeePass2.cpp format/KeePass2RandomStream.cpp @@ -101,6 +99,7 @@ set(keepassx_SOURCES gui/CategoryListWidget.cpp gui/Clipboard.cpp gui/CloneDialog.cpp + gui/DatabaseIcons.cpp gui/DatabaseOpenWidget.cpp gui/DatabaseTabWidget.cpp gui/DatabaseWidget.cpp @@ -114,6 +113,7 @@ set(keepassx_SOURCES gui/FileDialog.cpp gui/Font.cpp gui/GuiTools.cpp + gui/HtmlExporter.cpp gui/IconModels.cpp gui/KeePass1OpenWidget.cpp gui/KMessageWidget.cpp diff --git a/src/autotype/AutoTypeMatchModel.cpp b/src/autotype/AutoTypeMatchModel.cpp index 128015898..207d09b02 100644 --- a/src/autotype/AutoTypeMatchModel.cpp +++ b/src/autotype/AutoTypeMatchModel.cpp @@ -20,8 +20,12 @@ #include +#include "core/Entry.h" +#include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" +#include "gui/DatabaseIcons.h" +#include "gui/Icons.h" AutoTypeMatchModel::AutoTypeMatchModel(QObject* parent) : QAbstractTableModel(parent) @@ -114,11 +118,11 @@ QVariant AutoTypeMatchModel::data(const QModelIndex& index, int role) const switch (index.column()) { case ParentGroup: if (match.first->group()) { - return match.first->group()->iconPixmap(); + return Icons::groupIconPixmap(match.first->group()); } break; case Title: - return match.first->iconPixmap(); + return Icons::entryIconPixmap(match.first); } } else if (role == Qt::FontRole) { QFont font; diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 470af6283..f0d6cbd46 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -44,7 +44,7 @@ set(cli_SOURCES Show.cpp) add_library(cli STATIC ${cli_SOURCES}) -target_link_libraries(cli Qt5::Core Qt5::Widgets) +target_link_libraries(cli Qt5::Core) find_package(Readline) diff --git a/src/core/DatabaseIcons.cpp b/src/core/DatabaseIcons.cpp deleted file mode 100644 index a284d3842..000000000 --- a/src/core/DatabaseIcons.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2010 Felix Geyer - * - * 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 "DatabaseIcons.h" - -#include "core/Config.h" -#include "core/Global.h" - -#include -#include -#include -#include - -DatabaseIcons* DatabaseIcons::m_instance(nullptr); - -namespace -{ - const QString iconDir = QStringLiteral(":/icons/database/"); - QStringList iconList; - - const QString badgeDir = QStringLiteral(":/icons/badges/"); - QStringList badgeList; -} // namespace - -DatabaseIcons::DatabaseIcons() -{ - iconList = QDir(iconDir).entryList(QDir::NoFilter, QDir::Name); - badgeList = QDir(badgeDir).entryList(QDir::NoFilter, QDir::Name); - - // Set this early and once to ensure consistent icon size until app restart - m_compactMode = config()->get(Config::GUI_CompactMode).toBool(); -} - -DatabaseIcons* DatabaseIcons::instance() -{ - if (!m_instance) { - m_instance = new DatabaseIcons(); - } - - return m_instance; -} - -QPixmap DatabaseIcons::icon(int index, IconSize size) -{ - if (index < 0 || index >= count()) { - qWarning("DatabaseIcons::icon: invalid icon index %d", index); - return {}; - } - - auto cacheKey = QString::number(index); - auto icon = m_iconCache.value(cacheKey); - if (icon.isNull()) { - icon.addFile(iconDir + iconList[index]); - icon.addPixmap(icon.pixmap(64)); - m_iconCache.insert(cacheKey, icon); - } - - return icon.pixmap(iconSize(size)); -} - -QPixmap DatabaseIcons::applyBadge(const QPixmap& basePixmap, Badges badgeIndex) -{ - const auto cacheKey = QStringLiteral("badgedicon-%1-%2").arg(basePixmap.cacheKey()).arg(badgeIndex); - QPixmap pixmap = basePixmap; - if (badgeIndex < 0 || badgeIndex >= badgeList.size()) { - qWarning("DatabaseIcons: Out-of-range badge index given to applyBadge: %d", badgeIndex); - } else if (!QPixmapCache::find(cacheKey, &pixmap)) { - int baseSize = basePixmap.width(); - int badgeSize = - baseSize <= iconSize(IconSize::Default) * basePixmap.devicePixelRatio() ? baseSize * 0.6 : baseSize * 0.5; - QPoint badgePos(baseSize - badgeSize, baseSize - badgeSize); - badgePos /= basePixmap.devicePixelRatio(); - - QImageReader reader(badgeDir + badgeList[badgeIndex]); - reader.setScaledSize({badgeSize, badgeSize}); - auto badge = QPixmap::fromImageReader(&reader); - badge.setDevicePixelRatio(basePixmap.devicePixelRatio()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - painter.drawPixmap(badgePos, badge); - - QPixmapCache::insert(cacheKey, pixmap); - } - - return pixmap; -} - -int DatabaseIcons::count() -{ - return iconList.size(); -} - -int DatabaseIcons::iconSize(IconSize size) -{ - switch (size) { - case Medium: - return m_compactMode ? 26 : 30; - case Large: - return m_compactMode ? 30 : 36; - default: - return m_compactMode ? 16 : 22; - } -} diff --git a/src/core/DatabaseIcons.h b/src/core/DatabaseIcons.h deleted file mode 100644 index 2abb8a485..000000000 --- a/src/core/DatabaseIcons.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2010 Felix Geyer - * - * 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 . - */ - -#ifndef KEEPASSX_DATABASEICONS_H -#define KEEPASSX_DATABASEICONS_H - -#include "core/Global.h" -#include - -class DatabaseIcons -{ -public: - static DatabaseIcons* instance(); - - static constexpr int ExpiredIconIndex = 45; - - enum Badges - { - ShareActive = 0, - ShareInactive, - Expired - }; - - QPixmap icon(int index, IconSize size = IconSize::Default); - QPixmap applyBadge(const QPixmap& basePixmap, Badges badgeIndex); - int count(); - - int iconSize(IconSize size); - -private: - DatabaseIcons(); - - static DatabaseIcons* m_instance; - QHash m_iconCache; - bool m_compactMode; - - Q_DISABLE_COPY(DatabaseIcons) -}; - -inline DatabaseIcons* databaseIcons() -{ - return DatabaseIcons::instance(); -} - -#endif // KEEPASSX_DATABASEICONS_H diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index e386a349d..e7e000c2d 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -19,7 +19,7 @@ #include "Entry.h" #include "core/Config.h" -#include "core/DatabaseIcons.h" +#include "core/Database.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/PasswordHealth.h" @@ -159,40 +159,6 @@ const QString Entry::uuidToHex() const return Tools::uuidToHex(m_uuid); } -QImage Entry::icon() const -{ - if (m_data.customIcon.isNull()) { - return databaseIcons()->icon(m_data.iconNumber).toImage(); - } else { - Q_ASSERT(database()); - - if (database()) { - return database()->metadata()->customIcon(m_data.customIcon); - } else { - return QImage(); - } - } -} - -QPixmap Entry::iconPixmap(IconSize size) const -{ - QPixmap icon(size, size); - if (m_data.customIcon.isNull()) { - icon = databaseIcons()->icon(m_data.iconNumber, size); - } else { - Q_ASSERT(database()); - if (database()) { - icon = database()->metadata()->customIconPixmap(m_data.customIcon, size); - } - } - - if (isExpired()) { - icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired); - } - - return icon; -} - int Entry::iconNumber() const { return m_data.iconNumber; @@ -1217,7 +1183,8 @@ void Entry::setGroup(Group* group) // copy custom icon to the new database if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid()) && !group->database()->metadata()->hasCustomIcon(iconUuid())) { - group->database()->metadata()->addCustomIcon(iconUuid(), icon()); + group->database()->metadata()->addCustomIcon(iconUuid(), + m_group->database()->metadata()->customIcon(iconUuid())); } } } diff --git a/src/core/Entry.h b/src/core/Entry.h index 99c0e6024..1fcd09572 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -19,7 +19,7 @@ #ifndef KEEPASSX_ENTRY_H #define KEEPASSX_ENTRY_H -#include +#include #include #include @@ -80,8 +80,6 @@ public: ~Entry(); const QUuid& uuid() const; const QString uuidToHex() const; - QImage icon() const; - QPixmap iconPixmap(IconSize size = IconSize::Default) const; int iconNumber() const; const QUuid& iconUuid() const; QString foregroundColor() const; diff --git a/src/core/Global.h b/src/core/Global.h index fe51b9b5d..e9a4db739 100644 --- a/src/core/Global.h +++ b/src/core/Global.h @@ -45,13 +45,6 @@ static const auto TRUE_STR = QStringLiteral("true"); static const auto FALSE_STR = QStringLiteral("false"); -enum IconSize -{ - Default, - Medium, - Large -}; - enum class AuthDecision { Undecided, diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 196f230c1..6684d1ab9 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -20,14 +20,16 @@ #include "config-keepassx.h" #include "core/Config.h" -#include "core/DatabaseIcons.h" -#include "core/Metadata.h" -#include "core/Tools.h" #ifdef WITH_XC_KEESHARE #include "keeshare/KeeShare.h" #endif +#include "core/Global.h" +#include "core/Metadata.h" +#include "core/Tools.h" + +#include #include const int Group::DefaultIconNumber = 48; @@ -123,44 +125,6 @@ QString Group::notes() const return m_data.notes; } -QImage Group::icon() const -{ - if (m_data.customIcon.isNull()) { - return databaseIcons()->icon(m_data.iconNumber).toImage(); - } else { - Q_ASSERT(m_db); - if (m_db) { - return m_db->metadata()->customIcon(m_data.customIcon); - } else { - return QImage(); - } - } -} - -QPixmap Group::iconPixmap(IconSize size) const -{ - QPixmap icon(size, size); - if (m_data.customIcon.isNull()) { - icon = databaseIcons()->icon(m_data.iconNumber, size); - } else { - Q_ASSERT(m_db); - if (m_db) { - icon = m_db->metadata()->customIconPixmap(m_data.customIcon, size); - } - } - - if (isExpired()) { - icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired); - } -#ifdef WITH_XC_KEESHARE - else if (KeeShare::isShared(this)) { - icon = KeeShare::indicatorBadge(this, icon); - } -#endif - - return icon; -} - int Group::iconNumber() const { return m_data.iconNumber; @@ -469,7 +433,7 @@ void Group::setParent(Group* parent, int index) // copy custom icon to the new database if (!iconUuid().isNull() && parent->m_db && m_db->metadata()->hasCustomIcon(iconUuid()) && !parent->m_db->metadata()->hasCustomIcon(iconUuid())) { - parent->m_db->metadata()->addCustomIcon(iconUuid(), icon()); + parent->m_db->metadata()->addCustomIcon(iconUuid(), m_db->metadata()->customIcon(iconUuid())); } } if (m_db != parent->m_db) { diff --git a/src/core/Group.h b/src/core/Group.h index c564d1609..c283a89a2 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -19,7 +19,7 @@ #ifndef KEEPASSX_GROUP_H #define KEEPASSX_GROUP_H -#include +#include #include "core/CustomData.h" #include "core/Database.h" @@ -82,8 +82,6 @@ public: const QString uuidToHex() const; QString name() const; QString notes() const; - QImage icon() const; - QPixmap iconPixmap(IconSize size = IconSize::Default) const; int iconNumber() const; const QUuid& iconUuid() const; const TimeInfo& timeInfo() const; diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp index a5f532af2..b7c832951 100644 --- a/src/core/Merger.cpp +++ b/src/core/Merger.cpp @@ -611,8 +611,7 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) for (const auto& iconUuid : sourceMetadata->customIconsOrder()) { if (!targetMetadata->hasCustomIcon(iconUuid)) { - QImage customIcon = sourceMetadata->customIcon(iconUuid); - targetMetadata->addCustomIcon(iconUuid, customIcon); + targetMetadata->addCustomIcon(iconUuid, sourceMetadata->customIcon(iconUuid)); changes << tr("Adding missing icon %1").arg(QString::fromLatin1(iconUuid.toRfc4122().toHex())); } } diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index f99f42499..8bb0b52e4 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2021 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 @@ -17,7 +18,8 @@ #include "Metadata.h" -#include "core/DatabaseIcons.h" +#include "core/Clock.h" +#include "core/Entry.h" #include "core/Group.h" #include @@ -64,7 +66,6 @@ void Metadata::clear() { init(); m_customIcons.clear(); - m_customIconsRaw.clear(); m_customIconsOrder.clear(); m_customIconsHashes.clear(); m_customData->clear(); @@ -175,33 +176,14 @@ bool Metadata::protectNotes() const return m_data.protectNotes; } -QImage Metadata::customIcon(const QUuid& uuid) const +QByteArray Metadata::customIcon(const QUuid& uuid) const { - return m_customIconsRaw.value(uuid); -} - -QPixmap Metadata::customIconPixmap(const QUuid& uuid, IconSize size) const -{ - if (!hasCustomIcon(uuid)) { - return {}; - } - return m_customIcons.value(uuid).pixmap(databaseIcons()->iconSize(size)); -} - -QHash Metadata::customIconsPixmaps(IconSize size) const -{ - QHash result; - - for (const QUuid& uuid : m_customIconsOrder) { - result.insert(uuid, customIconPixmap(uuid, size)); - } - - return result; + return m_customIcons.value(uuid); } bool Metadata::hasCustomIcon(const QUuid& uuid) const { - return m_customIconsRaw.contains(uuid); + return m_customIcons.contains(uuid); } QList Metadata::customIconsOrder() const @@ -357,32 +339,19 @@ void Metadata::setProtectNotes(bool value) set(m_data.protectNotes, value); } -void Metadata::addCustomIcon(const QUuid& uuid, const QImage& image) +void Metadata::addCustomIcon(const QUuid& uuid, const QByteArray& iconData) { Q_ASSERT(!uuid.isNull()); - Q_ASSERT(!m_customIconsRaw.contains(uuid)); + Q_ASSERT(!m_customIcons.contains(uuid)); - m_customIconsRaw[uuid] = image; + m_customIcons[uuid] = iconData; // remove all uuids to prevent duplicates in release mode m_customIconsOrder.removeAll(uuid); m_customIconsOrder.append(uuid); // Associate image hash to uuid - QByteArray hash = hashImage(image); + QByteArray hash = hashIcon(iconData); m_customIconsHashes[hash] = uuid; - Q_ASSERT(m_customIconsRaw.count() == m_customIconsOrder.count()); - - // TODO: This check can go away when we move all QIcon handling outside of core - // On older versions of Qt, loading a QPixmap from QImage outside of a GUI - // environment causes ASAN to fail and crash on nullptr violation - static bool isGui = qApp->inherits("QGuiApplication"); - if (isGui) { - // Generate QIcon with pre-baked resolutions - auto basePixmap = QPixmap::fromImage(image.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - QIcon icon(basePixmap); - m_customIcons.insert(uuid, icon); - } else { - m_customIcons.insert(uuid, QIcon()); - } + Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); emitModified(); } @@ -390,24 +359,23 @@ void Metadata::addCustomIcon(const QUuid& uuid, const QImage& image) void Metadata::removeCustomIcon(const QUuid& uuid) { Q_ASSERT(!uuid.isNull()); - Q_ASSERT(m_customIconsRaw.contains(uuid)); + Q_ASSERT(m_customIcons.contains(uuid)); // Remove hash record only if this is the same uuid - QByteArray hash = hashImage(m_customIconsRaw[uuid]); + QByteArray hash = hashIcon(m_customIcons[uuid]); if (m_customIconsHashes.contains(hash) && m_customIconsHashes[hash] == uuid) { m_customIconsHashes.remove(hash); } m_customIcons.remove(uuid); - m_customIconsRaw.remove(uuid); m_customIconsOrder.removeAll(uuid); - Q_ASSERT(m_customIconsRaw.count() == m_customIconsOrder.count()); + Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); emitModified(); } -QUuid Metadata::findCustomIcon(const QImage& candidate) +QUuid Metadata::findCustomIcon(const QByteArray& candidate) { - QByteArray hash = hashImage(candidate); + QByteArray hash = hashIcon(candidate); return m_customIconsHashes.value(hash, QUuid()); } @@ -422,14 +390,9 @@ void Metadata::copyCustomIcons(const QSet& iconList, const Metadata* othe } } -QByteArray Metadata::hashImage(const QImage& image) +QByteArray Metadata::hashIcon(const QByteArray& iconData) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - auto data = QByteArray(reinterpret_cast(image.bits()), static_cast(image.sizeInBytes())); -#else - auto data = QByteArray(reinterpret_cast(image.bits()), image.byteCount()); -#endif - return QCryptographicHash::hash(data, QCryptographicHash::Md5); + return QCryptographicHash::hash(iconData, QCryptographicHash::Md5); } void Metadata::setRecycleBinEnabled(bool value) diff --git a/src/core/Metadata.h b/src/core/Metadata.h index 51276ec61..1b87ec2ad 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -1,5 +1,6 @@ /* * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2021 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 @@ -19,7 +20,7 @@ #define KEEPASSX_METADATA_H #include -#include +#include #include #include @@ -78,10 +79,8 @@ public: bool protectPassword() const; bool protectUrl() const; bool protectNotes() const; - QImage customIcon(const QUuid& uuid) const; + QByteArray customIcon(const QUuid& uuid) const; bool hasCustomIcon(const QUuid& uuid) const; - QPixmap customIconPixmap(const QUuid& uuid, IconSize size = IconSize::Default) const; - QHash customIconsPixmaps(IconSize size = IconSize::Default) const; QList customIconsOrder() const; bool recycleBinEnabled() const; Group* recycleBin(); @@ -117,10 +116,10 @@ public: void setProtectPassword(bool value); void setProtectUrl(bool value); void setProtectNotes(bool value); - void addCustomIcon(const QUuid& uuid, const QImage& image); + void addCustomIcon(const QUuid& uuid, const QByteArray& iconData); void removeCustomIcon(const QUuid& uuid); void copyCustomIcons(const QSet& iconList, const Metadata* otherMetadata); - QUuid findCustomIcon(const QImage& candidate); + QUuid findCustomIcon(const QByteArray& candidate); void setRecycleBinEnabled(bool value); void setRecycleBin(Group* group); void setRecycleBinChanged(const QDateTime& value); @@ -148,13 +147,12 @@ private: template bool set(P& property, const V& value); template bool set(P& property, const V& value, QDateTime& dateTime); - QByteArray hashImage(const QImage& image); + QByteArray hashIcon(const QByteArray& iconData); MetadataData m_data; - QHash m_customIcons; - QHash m_customIconsRaw; QList m_customIconsOrder; + QHash m_customIcons; QHash m_customIconsHashes; QPointer m_recycleBin; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 7c6d52a59..61c8cb6e2 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -24,9 +24,11 @@ #include "core/Clock.h" +#include #include +#include #include -#include +#include #include #include #include @@ -172,24 +174,6 @@ namespace Tools } } - QString imageReaderFilter() - { - const QList formats = QImageReader::supportedImageFormats(); - QStringList formatsStringList; - - for (const QByteArray& format : formats) { - for (char codePoint : format) { - if (!QChar(codePoint).isLetterOrNumber()) { - continue; - } - } - - formatsStringList.append("*." + QString::fromLatin1(format).toLower()); - } - - return formatsStringList.join(" "); - } - bool isHex(const QByteArray& ba) { for (const uchar c : ba) { diff --git a/src/core/Tools.h b/src/core/Tools.h index cf3b3593d..2c22e7427 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -32,7 +32,6 @@ namespace Tools QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); bool readAllFromDevice(QIODevice* device, QByteArray& data); - QString imageReaderFilter(); bool isHex(const QByteArray& ba); bool isBase64(const QByteArray& ba); void sleep(int ms); diff --git a/src/fdosecrets/widgets/AccessControlDialog.cpp b/src/fdosecrets/widgets/AccessControlDialog.cpp index af741ea0f..2c8fd696a 100644 --- a/src/fdosecrets/widgets/AccessControlDialog.cpp +++ b/src/fdosecrets/widgets/AccessControlDialog.cpp @@ -24,6 +24,7 @@ #include "fdosecrets/widgets/RowButtonHelper.h" #include "core/Entry.h" +#include "gui/Icons.h" #include @@ -250,7 +251,7 @@ QVariant AccessControlDialog::EntryModel::data(const QModelIndex& index, int rol case Qt::DisplayRole: return entry->title(); case Qt::DecorationRole: - return entry->icon(); + return Icons::entryIconPixmap(entry); case Qt::CheckStateRole: return QVariant::fromValue(m_selected.contains(entry) ? Qt::Checked : Qt::Unchecked); default: diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index 6f318b7bb..d68ee4b0c 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -147,7 +147,7 @@ namespace FdoSecrets case Qt::DisplayRole: return group->name(); case Qt::DecorationRole: - return group->iconPixmap(); + return Icons::groupIconPixmap(group); case Qt::FontRole: if (group->isExpired()) { QFont font; diff --git a/src/format/HtmlExporter.cpp b/src/format/HtmlExporter.cpp deleted file mode 100644 index 83a5b7345..000000000 --- a/src/format/HtmlExporter.cpp +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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; -} diff --git a/src/format/HtmlExporter.h b/src/format/HtmlExporter.h deleted file mode 100644 index 3a592e54a..000000000 --- a/src/format/HtmlExporter.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 . - */ - -#ifndef KEEPASSX_HTMLEXPORTER_H -#define KEEPASSX_HTMLEXPORTER_H - -#include -#include - -class Database; -class Group; -class QIODevice; - -class HtmlExporter -{ -public: - bool exportDatabase(const QString& filename, const QSharedPointer& db); - QString errorString() const; - -private: - bool exportDatabase(QIODevice* device, const QSharedPointer& db); - bool writeGroup(QIODevice& device, const Group& group, QString path = QString()); - - QString m_error; -}; - -#endif // KEEPASSX_HTMLEXPORTER_H diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index c802040c0..03a87ad5d 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -17,7 +17,7 @@ #include "KdbxXmlReader.h" #include "KeePass2RandomStream.h" -#include "core/DatabaseIcons.h" +#include "core/Clock.h" #include "core/Endian.h" #include "core/Group.h" #include "core/Tools.h" @@ -346,7 +346,7 @@ void KdbxXmlReader::parseIcon() Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Icon"); QUuid uuid; - QImage icon; + QByteArray iconData; bool uuidSet = false; bool iconSet = false; @@ -355,7 +355,7 @@ void KdbxXmlReader::parseIcon() uuid = readUuid(); uuidSet = !uuid.isNull(); } else if (m_xml.name() == "Data") { - icon.loadFromData(readBinary()); + iconData = readBinary(); iconSet = true; } else { skipCurrentElement(); @@ -367,7 +367,7 @@ void KdbxXmlReader::parseIcon() if (m_meta->hasCustomIcon(uuid)) { uuid = QUuid::createUuid(); } - m_meta->addCustomIcon(uuid, icon); + m_meta->addCustomIcon(uuid, iconData); return; } @@ -509,9 +509,6 @@ Group* KdbxXmlReader::parseGroup() raiseError(tr("Invalid group icon number")); } iconId = 0; - } else if (iconId >= databaseIcons()->count()) { - qWarning("KdbxXmlReader::parseGroup: icon id \"%d\" not supported", iconId); - iconId = databaseIcons()->count() - 1; } group->setIcon(iconId); diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp index 853a64e89..c71aa42b9 100644 --- a/src/format/KdbxXmlWriter.cpp +++ b/src/format/KdbxXmlWriter.cpp @@ -162,19 +162,12 @@ void KdbxXmlWriter::writeCustomIcons() m_xml.writeEndElement(); } -void KdbxXmlWriter::writeIcon(const QUuid& uuid, const QImage& icon) +void KdbxXmlWriter::writeIcon(const QUuid& uuid, const QByteArray& iconData) { m_xml.writeStartElement("Icon"); writeUuid("UUID", uuid); - - QByteArray ba; - QBuffer buffer(&ba); - buffer.open(QIODevice::WriteOnly); - // TODO: check !icon.save() - icon.save(&buffer, "PNG"); - buffer.close(); - writeBinary("Data", ba); + writeBinary("Data", iconData); m_xml.writeEndElement(); } diff --git a/src/format/KdbxXmlWriter.h b/src/format/KdbxXmlWriter.h index c0774fc6c..e8dac5bee 100644 --- a/src/format/KdbxXmlWriter.h +++ b/src/format/KdbxXmlWriter.h @@ -18,6 +18,7 @@ #ifndef KEEPASSX_KDBXXMLWRITER_H #define KEEPASSX_KDBXXMLWRITER_H +#include #include #include "core/Group.h" @@ -46,7 +47,7 @@ private: void writeMetadata(); void writeMemoryProtection(); void writeCustomIcons(); - void writeIcon(const QUuid& uuid, const QImage& icon); + void writeIcon(const QUuid& uuid, const QByteArray& iconData); void writeBinaries(); void writeCustomData(const CustomData* customData); void writeCustomDataItem(const QString& key, const QString& value); diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp index e4415af93..0b7f168a4 100644 --- a/src/format/KeePass1Reader.cpp +++ b/src/format/KeePass1Reader.cpp @@ -857,13 +857,9 @@ bool KeePass1Reader::parseCustomIcons4(const QByteArray& data) if (static_cast(data.size()) < (pos + iconSize)) { return false; } - QImage icon = QImage::fromData(data.mid(pos, iconSize)); + QByteArray icon = data.mid(pos, iconSize); pos += iconSize; - if (icon.width() != 16 || icon.height() != 16) { - icon = icon.scaled(16, 16); - } - QUuid uuid = QUuid::createUuid(); iconUuids.append(uuid); m_db->metadata()->addCustomIcon(uuid, icon); diff --git a/src/gui/DatabaseIcons.cpp b/src/gui/DatabaseIcons.cpp new file mode 100644 index 000000000..f83ce9ac4 --- /dev/null +++ b/src/gui/DatabaseIcons.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 Felix Geyer + * + * 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 "DatabaseIcons.h" + +#include "core/Config.h" +#include "core/Global.h" + +#include +#include +#include +#include + +DatabaseIcons* DatabaseIcons::m_instance(nullptr); + +namespace +{ + const QString iconDir = QStringLiteral(":/icons/database/"); + QStringList iconList; + + const QString badgeDir = QStringLiteral(":/icons/badges/"); + QStringList badgeList; +} // namespace + +DatabaseIcons::DatabaseIcons() +{ + iconList = QDir(iconDir).entryList(QDir::NoFilter, QDir::Name); + badgeList = QDir(badgeDir).entryList(QDir::NoFilter, QDir::Name); + + // Set this early and once to ensure consistent icon size until app restart + m_compactMode = config()->get(Config::GUI_CompactMode).toBool(); +} + +DatabaseIcons* DatabaseIcons::instance() +{ + if (!m_instance) { + m_instance = new DatabaseIcons(); + } + + return m_instance; +} + +QPixmap DatabaseIcons::icon(int index, IconSize size) +{ + if (index < 0 || index >= count()) { + qWarning("DatabaseIcons::icon: invalid icon index %d, using 0 instead", index); + index = 0; + Q_ASSERT_X(false, "DatabaseIcons::icon", "invalid icon index %d"); + } + + auto cacheKey = QString::number(index); + auto icon = m_iconCache.value(cacheKey); + if (icon.isNull()) { + icon.addFile(iconDir + iconList[index]); + icon.addPixmap(icon.pixmap(64)); + m_iconCache.insert(cacheKey, icon); + } + + return icon.pixmap(iconSize(size)); +} + +QPixmap DatabaseIcons::applyBadge(const QPixmap& basePixmap, Badges badgeIndex) +{ + const auto cacheKey = QStringLiteral("badgedicon-%1-%2").arg(basePixmap.cacheKey()).arg(badgeIndex); + QPixmap pixmap = basePixmap; + if (badgeIndex < 0 || badgeIndex >= badgeList.size()) { + qWarning("DatabaseIcons: Out-of-range badge index given to applyBadge: %d", badgeIndex); + } else if (!QPixmapCache::find(cacheKey, &pixmap)) { + int baseSize = basePixmap.width(); + int badgeSize = + baseSize <= iconSize(IconSize::Default) * basePixmap.devicePixelRatio() ? baseSize * 0.6 : baseSize * 0.5; + QPoint badgePos(baseSize - badgeSize, baseSize - badgeSize); + badgePos /= basePixmap.devicePixelRatio(); + + QImageReader reader(badgeDir + badgeList[badgeIndex]); + reader.setScaledSize({badgeSize, badgeSize}); + auto badge = QPixmap::fromImageReader(&reader); + badge.setDevicePixelRatio(basePixmap.devicePixelRatio()); + + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawPixmap(badgePos, badge); + + QPixmapCache::insert(cacheKey, pixmap); + } + + return pixmap; +} + +int DatabaseIcons::count() +{ + return iconList.size(); +} + +int DatabaseIcons::iconSize(IconSize size) +{ + switch (size) { + case Medium: + return m_compactMode ? 26 : 30; + case Large: + return m_compactMode ? 30 : 36; + default: + return m_compactMode ? 16 : 22; + } +} diff --git a/src/gui/DatabaseIcons.h b/src/gui/DatabaseIcons.h new file mode 100644 index 000000000..38d1590c7 --- /dev/null +++ b/src/gui/DatabaseIcons.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 Felix Geyer + * + * 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 . + */ + +#ifndef KEEPASSX_DATABASEICONS_H +#define KEEPASSX_DATABASEICONS_H + +#include + +enum IconSize +{ + Default, + Medium, + Large +}; + +class DatabaseIcons +{ +public: + static DatabaseIcons* instance(); + + static constexpr int ExpiredIconIndex = 45; + + enum Badges + { + ShareActive = 0, + ShareInactive, + Expired + }; + + QPixmap icon(int index, IconSize size = IconSize::Default); + QPixmap applyBadge(const QPixmap& basePixmap, Badges badgeIndex); + int count(); + + int iconSize(IconSize size); + +private: + DatabaseIcons(); + + static DatabaseIcons* m_instance; + QHash m_iconCache; + bool m_compactMode; + + Q_DISABLE_COPY(DatabaseIcons) +}; + +inline DatabaseIcons* databaseIcons() +{ + return DatabaseIcons::instance(); +} + +#endif // KEEPASSX_DATABASEICONS_H diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 9aa58f556..40d42f4e1 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -22,10 +22,13 @@ #include "autotype/AutoType.h" #include "core/Tools.h" #include "format/CsvExporter.h" -#include "format/HtmlExporter.h" +#include "gui/Clipboard.h" +#include "gui/DatabaseOpenDialog.h" +#include "gui/DatabaseWidget.h" #include "gui/DatabaseWidgetStateSync.h" #include "gui/DragTabBar.h" #include "gui/FileDialog.h" +#include "gui/HtmlExporter.h" #include "gui/MessageBox.h" #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index 3158a6c9d..c301728b6 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -25,6 +25,8 @@ #include "core/Tools.h" #include "gui/FileDialog.h" #include "gui/IconModels.h" +#include "gui/Icons.h" +#include "gui/MessageBox.h" #ifdef WITH_XC_NETWORKING #include "gui/IconDownloader.h" #endif @@ -128,7 +130,7 @@ void EditWidgetIcons::load(const QUuid& currentUuid, m_currentUuid = currentUuid; setUrl(url); - m_customIconModel->setIcons(database->metadata()->customIconsPixmaps(IconSize::Default), + m_customIconModel->setIcons(Icons::customIconsPixmaps(database.data(), IconSize::Default), database->metadata()->customIconsOrder()); QUuid iconUuid = iconStruct.uuid; @@ -231,7 +233,7 @@ void EditWidgetIcons::addCustomIconFromFile() return; } - auto filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files")); + auto filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Icons::imageFormatsFilter(), tr("All files")); auto filenames = fileDialog()->getOpenFileNames(this, tr("Select Image(s)"), FileDialog::getLastDir("icons"), filter); if (!filenames.empty()) { @@ -284,16 +286,17 @@ bool EditWidgetIcons::addCustomIcon(const QImage& icon) bool added = false; if (m_db) { // Don't add an icon larger than 128x128, but retain original size if smaller - auto scaledicon = icon; + auto scaledIcon = icon; if (icon.width() > 128 || icon.height() > 128) { - scaledicon = icon.scaled(128, 128); + scaledIcon = icon.scaled(128, 128); } - QUuid uuid = m_db->metadata()->findCustomIcon(scaledicon); + QByteArray serializedIcon = Icons::saveToBytes(scaledIcon); + QUuid uuid = m_db->metadata()->findCustomIcon(serializedIcon); if (uuid.isNull()) { uuid = QUuid::createUuid(); - m_db->metadata()->addCustomIcon(uuid, scaledicon); - m_customIconModel->setIcons(m_db->metadata()->customIconsPixmaps(IconSize::Default), + m_db->metadata()->addCustomIcon(uuid, serializedIcon); + m_customIconModel->setIcons(Icons::customIconsPixmaps(m_db.data(), IconSize::Default), m_db->metadata()->customIconsOrder()); added = true; } diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 8b283d39a..7c3b8ffd2 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -173,7 +173,7 @@ void EntryPreviewWidget::updateEntryHeaderLine() Q_ASSERT(m_currentEntry); const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title()); m_ui->entryTitleLabel->setRawText(hierarchy(m_currentEntry->group(), title)); - m_ui->entryIcon->setPixmap(m_currentEntry->iconPixmap(IconSize::Large)); + m_ui->entryIcon->setPixmap(Icons::entryIconPixmap(m_currentEntry, IconSize::Large)); } void EntryPreviewWidget::updateEntryTotp() @@ -377,7 +377,7 @@ void EntryPreviewWidget::updateGroupHeaderLine() { Q_ASSERT(m_currentGroup); m_ui->groupTitleLabel->setRawText(hierarchy(m_currentGroup, {})); - m_ui->groupIcon->setPixmap(m_currentGroup->iconPixmap(IconSize::Large)); + m_ui->groupIcon->setPixmap(Icons::groupIconPixmap(m_currentGroup, IconSize::Large)); } void EntryPreviewWidget::updateGroupGeneralTab() diff --git a/src/gui/HtmlExporter.cpp b/src/gui/HtmlExporter.cpp new file mode 100644 index 000000000..c79681637 --- /dev/null +++ b/src/gui/HtmlExporter.cpp @@ -0,0 +1,264 @@ +/* + * 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 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(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(""); + + // 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(Icons::entryIconPixmap(entry, 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; +} diff --git a/src/gui/HtmlExporter.h b/src/gui/HtmlExporter.h new file mode 100644 index 000000000..3a592e54a --- /dev/null +++ b/src/gui/HtmlExporter.h @@ -0,0 +1,41 @@ +/* + * 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 . + */ + +#ifndef KEEPASSX_HTMLEXPORTER_H +#define KEEPASSX_HTMLEXPORTER_H + +#include +#include + +class Database; +class Group; +class QIODevice; + +class HtmlExporter +{ +public: + bool exportDatabase(const QString& filename, const QSharedPointer& db); + QString errorString() const; + +private: + bool exportDatabase(QIODevice* device, const QSharedPointer& db); + bool writeGroup(QIODevice& device, const Group& group, QString path = QString()); + + QString m_error; +}; + +#endif // KEEPASSX_HTMLEXPORTER_H diff --git a/src/gui/IconDownloaderDialog.cpp b/src/gui/IconDownloaderDialog.cpp index c2d5c8190..271a916f3 100644 --- a/src/gui/IconDownloaderDialog.cpp +++ b/src/gui/IconDownloaderDialog.cpp @@ -24,7 +24,12 @@ #include "core/Metadata.h" #include "core/Tools.h" #include "gui/IconDownloader.h" +#include "gui/IconModels.h" +#include "gui/Icons.h" #include "osutils/OSUtils.h" +#ifdef Q_OS_MACOS +#include "gui/osutils/macutils/MacUtils.h" +#endif #include @@ -126,15 +131,16 @@ void IconDownloaderDialog::downloadFinished(const QString& url, const QImage& ic if (m_db && !icon.isNull()) { // Don't add an icon larger than 128x128, but retain original size if smaller - auto scaledicon = icon; + auto scaledIcon = icon; if (icon.width() > 128 || icon.height() > 128) { - scaledicon = icon.scaled(128, 128); + scaledIcon = icon.scaled(128, 128); } - QUuid uuid = m_db->metadata()->findCustomIcon(scaledicon); + QByteArray serializedIcon = Icons::saveToBytes(scaledIcon); + QUuid uuid = m_db->metadata()->findCustomIcon(serializedIcon); if (uuid.isNull()) { uuid = QUuid::createUuid(); - m_db->metadata()->addCustomIcon(uuid, scaledicon); + m_db->metadata()->addCustomIcon(uuid, serializedIcon); updateTable(url, tr("Ok")); } else { updateTable(url, tr("Already Exists")); diff --git a/src/gui/IconModels.cpp b/src/gui/IconModels.cpp index 3bdd9a5e6..ab435aedc 100644 --- a/src/gui/IconModels.cpp +++ b/src/gui/IconModels.cpp @@ -19,7 +19,7 @@ #include -#include "core/DatabaseIcons.h" +#include "gui/DatabaseIcons.h" DefaultIconModel::DefaultIconModel(QObject* parent) : QAbstractListModel(parent) diff --git a/src/gui/Icons.cpp b/src/gui/Icons.cpp index cdebdcc75..95e6aadc4 100644 --- a/src/gui/Icons.cpp +++ b/src/gui/Icons.cpp @@ -19,11 +19,20 @@ #include "Icons.h" #include +#include +#include #include +#include "config-keepassx.h" +#include "core/Config.h" +#include "gui/DatabaseIcons.h" #include "gui/MainWindow.h" #include "gui/osutils/OSUtils.h" +#ifdef WITH_XC_KEESHARE +#include "keeshare/KeeShare.h" +#endif + class AdaptiveIconEngine : public QIconEngine { public: @@ -206,3 +215,97 @@ Icons* Icons::instance() return m_instance; } + +QPixmap Icons::customIconPixmap(const Database* db, const QUuid& uuid, IconSize size) +{ + if (!db->metadata()->hasCustomIcon(uuid)) { + return {}; + } + // Generate QIcon with pre-baked resolutions + auto icon = QImage::fromData(db->metadata()->customIcon(uuid)); + auto basePixmap = QPixmap::fromImage(icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + return QIcon(basePixmap).pixmap(databaseIcons()->iconSize(size)); +} + +QHash Icons::customIconsPixmaps(const Database* db, IconSize size) +{ + QHash result; + + for (const QUuid& uuid : db->metadata()->customIconsOrder()) { + result.insert(uuid, Icons::customIconPixmap(db, uuid, size)); + } + + return result; +} + +QPixmap Icons::entryIconPixmap(const Entry* entry, IconSize size) +{ + QPixmap icon(size, size); + if (entry->iconUuid().isNull()) { + icon = databaseIcons()->icon(entry->iconNumber(), size); + } else { + Q_ASSERT(entry->database()); + if (entry->database()) { + icon = Icons::customIconPixmap(entry->database(), entry->iconUuid(), size); + } + } + + if (entry->isExpired()) { + icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired); + } + + return icon; +} + +QPixmap Icons::groupIconPixmap(const Group* group, IconSize size) +{ + QPixmap icon(size, size); + if (group->iconUuid().isNull()) { + icon = databaseIcons()->icon(group->iconNumber(), size); + } else { + Q_ASSERT(group->database()); + if (group->database()) { + icon = Icons::customIconPixmap(group->database(), group->iconUuid(), size); + } + } + + if (group->isExpired()) { + icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired); + } +#ifdef WITH_XC_KEESHARE + else if (KeeShare::isShared(group)) { + icon = KeeShare::indicatorBadge(group, icon); + } +#endif + + return icon; +} + +QString Icons::imageFormatsFilter() +{ + const QList formats = QImageReader::supportedImageFormats(); + QStringList formatsStringList; + + for (const QByteArray& format : formats) { + for (char codePoint : format) { + if (!QChar(codePoint).isLetterOrNumber()) { + continue; + } + } + + formatsStringList.append("*." + QString::fromLatin1(format).toLower()); + } + + return formatsStringList.join(" "); +} + +QByteArray Icons::saveToBytes(const QImage& image) +{ + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + // TODO: check !icon.save() + image.save(&buffer, "PNG"); + buffer.close(); + return ba; +} diff --git a/src/gui/Icons.h b/src/gui/Icons.h index d02fd79c4..01af0b289 100644 --- a/src/gui/Icons.h +++ b/src/gui/Icons.h @@ -21,6 +21,9 @@ #include +#include +#include + class Icons { public: @@ -32,6 +35,14 @@ public: QIcon icon(const QString& name, bool recolor = true, const QColor& overrideColor = QColor::Invalid); QIcon onOffIcon(const QString& name, bool on, bool recolor = true); + static QPixmap customIconPixmap(const Database* db, const QUuid& uuid, IconSize size = IconSize::Default); + static QHash customIconsPixmaps(const Database* db, IconSize size = IconSize::Default); + static QPixmap entryIconPixmap(const Entry* entry, IconSize size = IconSize::Default); + static QPixmap groupIconPixmap(const Group* group, IconSize size = IconSize::Default); + + static QByteArray saveToBytes(const QImage& image); + static QString imageFormatsFilter(); + static Icons* instance(); private: diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp index 12738a987..09650c6b0 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp @@ -21,6 +21,7 @@ #include "core/Group.h" #include "core/Metadata.h" #include "gui/IconModels.h" +#include "gui/Icons.h" #include "gui/MessageBox.h" DatabaseSettingsWidgetMaintenance::DatabaseSettingsWidgetMaintenance(QWidget* parent) @@ -47,7 +48,7 @@ DatabaseSettingsWidgetMaintenance::~DatabaseSettingsWidgetMaintenance() void DatabaseSettingsWidgetMaintenance::populateIcons(QSharedPointer db) { - m_customIconModel->setIcons(db->metadata()->customIconsPixmaps(IconSize::Default), + m_customIconModel->setIcons(Icons::customIconsPixmaps(db.data(), IconSize::Default), db->metadata()->customIconsOrder()); m_ui->deleteButton->setEnabled(false); } diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 7b61da042..313b7ee50 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -21,9 +21,11 @@ #include #include +#include "core/Entry.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/PasswordHealth.h" +#include "gui/DatabaseIcons.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" #ifdef Q_OS_MACOS @@ -277,11 +279,11 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const switch (index.column()) { case ParentGroup: if (entry->group()) { - return entry->group()->iconPixmap(); + return Icons::groupIconPixmap(entry->group()); } break; case Title: - return entry->iconPixmap(); + return Icons::entryIconPixmap(entry); case Paperclip: if (!entry->attachments()->isEmpty()) { return icons()->icon("paperclip"); diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index 9a2b745ea..c21d1f911 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -19,8 +19,12 @@ #include +#include "core/Database.h" #include "core/Group.h" #include "core/Metadata.h" +#include "core/Tools.h" +#include "gui/DatabaseIcons.h" +#include "gui/Icons.h" #include "keeshare/KeeShare.h" GroupModel::GroupModel(Database* db, QObject* parent) @@ -126,7 +130,7 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const #endif return nameTemplate.arg(group->name()); } else if (role == Qt::DecorationRole) { - return group->iconPixmap(); + return Icons::groupIconPixmap(group); } else if (role == Qt::FontRole) { QFont font; if (group->isExpired()) { diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index e6b441a4d..d23829031 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -203,8 +203,8 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt auto row = QList(); row << new QStandardItem(descr); - row << new QStandardItem(entry->iconPixmap(), title); - row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")); + row << new QStandardItem(Icons::entryIconPixmap(entry), title); + row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/")); row << new QStandardItem(QString::number(health->score())); row << new QStandardItem(health->scoreReason()); diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index f84be1a18..86be3d92f 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -163,8 +163,8 @@ void ReportsWidgetHibp::makeHibpTable() } auto row = QList(); - row << new QStandardItem(entry->iconPixmap(), title) - << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")) + row << new QStandardItem(Icons::entryIconPixmap(entry), title) + << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/")) << new QStandardItem(countToText(count)); if (entry->excludeFromReports()) { diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index 4287481d9..7f512c6d9 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -16,8 +16,11 @@ */ #include "KeeShare.h" -#include "core/DatabaseIcons.h" +#include "core/CustomData.h" +#include "core/Database.h" #include "core/Group.h" +#include "core/Metadata.h" +#include "gui/DatabaseIcons.h" #include "keeshare/ShareObserver.h" namespace diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp index c2679feb7..601b05310 100644 --- a/src/keeshare/KeeShareSettings.cpp +++ b/src/keeshare/KeeShareSettings.cpp @@ -17,7 +17,13 @@ #include "KeeShareSettings.h" +#include "core/CustomData.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/Metadata.h" #include "crypto/Random.h" +#include "gui/DatabaseIcons.h" +#include "keeshare/Signature.h" #include #include diff --git a/src/keeshare/ShareExport.cpp b/src/keeshare/ShareExport.cpp index ac1d4ddec..31f7fece3 100644 --- a/src/keeshare/ShareExport.cpp +++ b/src/keeshare/ShareExport.cpp @@ -19,6 +19,7 @@ #include "core/Group.h" #include "core/Metadata.h" #include "format/KeePass2Writer.h" +#include "gui/Icons.h" #include "keeshare/KeeShare.h" #include "keeshare/Signature.h" #include "keys/PasswordKey.h" @@ -81,7 +82,7 @@ namespace targetEntry->setUpdateTimeinfo(updateTimeinfoEntry); const auto iconUuid = targetEntry->iconUuid(); if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) { - targetMetadata->addCustomIcon(iconUuid, sourceEntry->icon()); + targetMetadata->addCustomIcon(iconUuid, sourceEntry->database()->metadata()->customIcon(iconUuid)); } } diff --git a/src/qrcode/CMakeLists.txt b/src/qrcode/CMakeLists.txt index 5d01d07b7..10e368406 100644 --- a/src/qrcode/CMakeLists.txt +++ b/src/qrcode/CMakeLists.txt @@ -21,4 +21,4 @@ find_package(QREncode REQUIRED) add_library(qrcode STATIC ${qrcode_SOURCES}) target_include_directories(qrcode PRIVATE ${QRENCODE_INCLUDE_DIR}) -target_link_libraries(qrcode PUBLIC Qt5::Core Qt5::Widgets Qt5::Svg ${QRENCODE_LIBRARY}) +target_link_libraries(qrcode PUBLIC Qt5::Core Qt5::Svg ${QRENCODE_LIBRARY}) -- cgit v1.2.3