diff --git a/src/core/attachmentmodel.cpp b/src/core/attachmentmodel.cpp index 25ddaf9..80dfb62 100644 --- a/src/core/attachmentmodel.cpp +++ b/src/core/attachmentmodel.cpp @@ -1,349 +1,349 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com> // SPDX-FileCopyCopyright: 2017 Christian Mollekopf <mollekopf@kolabsys.com> // SPDX-License-Identifier: LGPL-2.0-or-later #include "attachmentmodel.h" #include "mimetreeparser_core_debug.h" #include "objecttreeparser.h" #include <QGpgME/ImportJob> #include <QGpgME/Protocol> #include <KLocalizedString> #include <KMime/Content> #include <QDesktopServices> #include <QDir> #include <QFile> #include <QGuiApplication> #include <QIcon> #include <QMimeDatabase> #include <QMimeType> #include <QRegularExpression> #include <QStandardPaths> -#include <QTemporaryFile> +#include <QTemporaryDir> #include <QUrl> namespace { QString sizeHuman(float size) { QStringList list; list << QStringLiteral("KB") << QStringLiteral("MB") << QStringLiteral("GB") << QStringLiteral("TB"); QStringListIterator i(list); QString unit = QStringLiteral("Bytes"); while (size >= 1024.0 && i.hasNext()) { unit = i.next(); size /= 1024.0; } if (unit == QStringLiteral("Bytes")) { return QString().setNum(size) + QStringLiteral(" ") + unit; } else { return QString().setNum(size, 'f', 2) + QStringLiteral(" ") + unit; } } // SPDX-SnippetBegin // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: GPL-3.0-only #define WINDOWS_DEVICES_PATTERN "(CON|AUX|PRN|NUL|COM[1-9]|LPT[1-9])(\\..*)?" // Naming a file like a device name will break on Windows, even if it is // "com1.txt". Since we are cross-platform, we generally disallow such file // names. const QRegularExpression &windowsDeviceNoSubDirPattern() { static const QRegularExpression rc(QStringLiteral("^" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption); Q_ASSERT(rc.isValid()); return rc; } const QRegularExpression &windowsDeviceSubDirPattern() { static const QRegularExpression rc(QStringLiteral("^.*[/\\\\]" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption); Q_ASSERT(rc.isValid()); return rc; } /* Validate a file base name, check for forbidden characters/strings. */ #define SLASHES "/\\" static const char notAllowedCharsSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' "; static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES; static const char *notAllowedSubStrings[] = {".."}; bool validateFileName(const QString &name, bool allowDirectories) { if (name.isEmpty()) { return false; } // Characters const char *notAllowedChars = allowDirectories ? notAllowedCharsSubDir : notAllowedCharsNoSubDir; for (const char *c = notAllowedChars; *c; c++) { if (name.contains(QLatin1Char(*c))) { return false; } } // Substrings const int notAllowedSubStringCount = sizeof(notAllowedSubStrings) / sizeof(const char *); for (int s = 0; s < notAllowedSubStringCount; s++) { const QLatin1String notAllowedSubString(notAllowedSubStrings[s]); if (name.contains(notAllowedSubString)) { return false; } } // Windows devices bool matchesWinDevice = name.contains(windowsDeviceNoSubDirPattern()); if (!matchesWinDevice && allowDirectories) { matchesWinDevice = name.contains(windowsDeviceSubDirPattern()); } return !matchesWinDevice; } // SPDX-SnippetEnd } class AttachmentModelPrivate { public: AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser); AttachmentModel *q; QMimeDatabase mimeDb; std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser; MimeTreeParser::MessagePart::List mAttachments; }; AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser) : q(q_ptr) , mParser(parser) { mAttachments = mParser->collectAttachmentParts(); } AttachmentModel::AttachmentModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser) : QAbstractTableModel() , d(std::unique_ptr<AttachmentModelPrivate>(new AttachmentModelPrivate(this, parser))) { } AttachmentModel::~AttachmentModel() { } QHash<int, QByteArray> AttachmentModel::roleNames() const { return { {TypeRole, QByteArrayLiteral("type")}, {NameRole, QByteArrayLiteral("name")}, {SizeRole, QByteArrayLiteral("size")}, {IconRole, QByteArrayLiteral("iconName")}, {IsEncryptedRole, QByteArrayLiteral("encrypted")}, {IsSignedRole, QByteArrayLiteral("signed")}, }; } QVariant AttachmentModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case NameColumn: return i18ndc("mimetreeparser", "@title:column", "Name"); case SizeColumn: return i18ndc("mimetreeparser", "@title:column", "Size"); case IsEncryptedColumn: return i18ndc("mimetreeparser", "@title:column", "Encrypted"); case IsSignedColumn: return i18ndc("mimetreeparser", "@title:column", "Signed"); } } return {}; } QVariant AttachmentModel::data(const QModelIndex &index, int role) const { const auto row = index.row(); const auto column = index.column(); const auto part = d->mAttachments.at(row); Q_ASSERT(part); auto node = part->node(); if (!node) { qWarning() << "no content for attachment"; return {}; } const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(part->mimeType())); const auto content = node->encodedContent(); switch (column) { case NameColumn: switch (role) { case TypeRole: return mimetype.name(); case Qt::DisplayRole: case NameRole: return part->filename(); case IconRole: return mimetype.iconName(); case Qt::DecorationRole: return QIcon::fromTheme(mimetype.iconName()); case SizeRole: return sizeHuman(content.size()); case IsEncryptedRole: return part->encryptions().size() > 0; case IsSignedRole: return part->signatures().size() > 0; case AttachmentPartRole: return QVariant::fromValue(part); default: return {}; } case SizeColumn: switch (role) { case Qt::DisplayRole: return sizeHuman(content.size()); default: return {}; } case IsEncryptedColumn: switch (role) { case Qt::CheckStateRole: return part->encryptions().size() > 0 ? Qt::Checked : Qt::Unchecked; default: return {}; } case IsSignedColumn: switch (role) { case Qt::CheckStateRole: return part->signatures().size() > 0 ? Qt::Checked : Qt::Unchecked; default: return {}; } default: return {}; } } QString AttachmentModel::saveAttachmentToPath(const int row, const QString &path, bool readonly) { const auto part = d->mAttachments.at(row); return saveAttachmentToPath(part, path, readonly); } QString AttachmentModel::saveAttachmentToPath(const MimeTreeParser::MessagePart::Ptr &part, const QString &path, bool readonly) { Q_ASSERT(part); auto node = part->node(); auto data = node->decodedContent(); // This is necessary to store messages embedded messages (EncapsulatedRfc822MessagePart) if (data.isEmpty()) { data = node->encodedContent(); } if (part->isText()) { // convert CRLF to LF before writing text attachments to disk data = KMime::CRLFtoLF(data); } QFile f(path); if (!f.open(QIODevice::ReadWrite)) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to write attachment to file:" << path << " Error: " << f.errorString(); Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to save attachment.")); return {}; } f.write(data); if (readonly) { // make file read-only so that nobody gets the impression that he migh edit attached files f.setPermissions(QFileDevice::ReadUser); } f.close(); qCInfo(MIMETREEPARSER_CORE_LOG) << "Wrote attachment to file: " << path; return path; } bool AttachmentModel::openAttachment(const int row) { const auto part = d->mAttachments.at(row); return openAttachment(part); } bool AttachmentModel::openAttachment(const MimeTreeParser::MessagePart::Ptr &message) { - const QString tempDir = QDir::tempPath() + QLatin1Char('/') + qGuiApp->applicationName(); QString fileName = message->filename(); QString errorMessage; - if (message->filename().isEmpty() || validateFileName(fileName, false)) { - QTemporaryFile file; + QTemporaryDir tempDir(QDir::tempPath() + QLatin1Char('/') + qGuiApp->applicationName() + QStringLiteral(".XXXXXX")); + // TODO: We need some cleanup here. Otherwise the files will stay forever on Windows. + tempDir.setAutoRemove(false); + if (message->filename().isEmpty() || !validateFileName(fileName, false)) { const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(message->mimeType())); - file.setFileTemplate(tempDir + QStringLiteral("XXXXXX.") + mimetype.preferredSuffix()); - file.setAutoRemove(false); - if (!file.open()) { - Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to create temporary file.")); - return false; - } - fileName = file.fileName(); + fileName = tempDir.filePath(i18n("attachment") + QLatin1Char('.') + mimetype.preferredSuffix()); } else { - fileName = tempDir + QLatin1Char('/') + message->filename(); + fileName = tempDir.filePath(message->filename()); } const auto filePath = saveAttachmentToPath(message, fileName, true); - if (!QDesktopServices::openUrl(QUrl(QStringLiteral("file://") + filePath))) { + if (filePath.isEmpty()) { + Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to write attachment for opening.")); + return false; + } + + if (!QDesktopServices::openUrl(QUrl::fromLocalFile(filePath))) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment.")); return false; } return true; } bool AttachmentModel::importPublicKey(const int row) { const auto part = d->mAttachments.at(row); return importPublicKey(part); } bool AttachmentModel::importPublicKey(const MimeTreeParser::MessagePart::Ptr &part) { Q_ASSERT(part); const QByteArray certData = part->node()->decodedContent(); QGpgME::ImportJob *importJob = QGpgME::openpgp()->importJob(); connect(importJob, &QGpgME::AbstractImportJob::result, this, [this](const GpgME::ImportResult &result) { if (result.numConsidered() == 0) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "No keys were found in this attachment")); return; } else { QString message = i18ndcp("mimetreeparser", "@info", "one key imported", "%1 keys imported", result.numImported()); if (result.numUnchanged() != 0) { message += QStringLiteral("\n") + i18ndcp("mimetreeparser", "@info", "one key was already imported", "%1 keys were already imported", result.numUnchanged()); } Q_EMIT info(message); } }); GpgME::Error err = importJob->start(certData); return !err; } int AttachmentModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->mAttachments.size(); } return 0; } int AttachmentModel::columnCount(const QModelIndex &parent) const { if (!parent.isValid()) { return ColumnCount; } return 0; }