264 lines
7.3 KiB
C++
264 lines
7.3 KiB
C++
#include "dropimporter.hpp"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QMetaObject>
|
|
#include <QMimeDatabase>
|
|
#include <QNetworkReply>
|
|
#include <QRegularExpression>
|
|
|
|
namespace {
|
|
constexpr qint64 kMaxBytes = 100 * 1024 * 1024;
|
|
|
|
bool isHttp(const QUrl& u) {
|
|
return u.scheme() == QLatin1String("http") ||
|
|
u.scheme() == QLatin1String("https");
|
|
}
|
|
|
|
QString firstImgSrc(const QString& html) {
|
|
static const QRegularExpression re(
|
|
R"(<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'])",
|
|
QRegularExpression::CaseInsensitiveOption);
|
|
const auto m = re.match(html);
|
|
if (!m.hasMatch()) return {};
|
|
return m.captured(1).replace(QLatin1String("&"), QLatin1String("&"));
|
|
}
|
|
|
|
bool looksLikeUrl(const QString& text) {
|
|
const QString t = text.trimmed();
|
|
if (t.isEmpty() || t.contains(QRegularExpression(QStringLiteral("\\s"))))
|
|
return false;
|
|
const QUrl u(t);
|
|
return u.isValid() && (isHttp(u) || u.isLocalFile());
|
|
}
|
|
|
|
QString sanitize(QString name) {
|
|
name = QFileInfo(name).fileName();
|
|
name.replace(
|
|
QRegularExpression(QStringLiteral("[\\\\/:*?\"<>|\\x00-\\x1f]")),
|
|
QStringLiteral("_"));
|
|
return name.left(120);
|
|
}
|
|
|
|
QString fileNameFor(QNetworkReply* reply, const QUrl& url) {
|
|
static const QRegularExpression cd(
|
|
QStringLiteral("filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?"),
|
|
QRegularExpression::CaseInsensitiveOption);
|
|
const auto m =
|
|
cd.match(QString::fromUtf8(reply->rawHeader("Content-Disposition")));
|
|
QString name = m.hasMatch()
|
|
? QUrl::fromPercentEncoding(m.captured(1).toUtf8())
|
|
: url.fileName();
|
|
name = sanitize(name);
|
|
return name.isEmpty() ? QStringLiteral("download") : name;
|
|
}
|
|
} // namespace
|
|
|
|
namespace ZShell::Services {
|
|
|
|
DropImporter::DropImporter(QObject* parent) : QObject(parent) {}
|
|
|
|
DropImporter::Decision DropImporter::decide(
|
|
const QList<QUrl>& urls,
|
|
const QString& text,
|
|
const QString& html,
|
|
bool hasRawImage) const {
|
|
for (const QUrl& u : urls)
|
|
if (u.isLocalFile()) return Attach;
|
|
|
|
const QString src = firstImgSrc(html);
|
|
if (hasRawImage ||
|
|
(!src.isEmpty() && (text.trimmed().isEmpty() || looksLikeUrl(text))))
|
|
return Attach;
|
|
|
|
QMimeDatabase db;
|
|
for (const QUrl& u : urls) {
|
|
if (!isHttp(u) || QFileInfo(u.path()).suffix().isEmpty()) continue;
|
|
const QMimeType mt = db.mimeTypeForUrl(u);
|
|
if (!mt.isDefault() && !mt.inherits(QStringLiteral("text/html")))
|
|
return Attach;
|
|
}
|
|
|
|
if (!text.isEmpty() || !urls.isEmpty()) return InsertText;
|
|
return Ignore;
|
|
}
|
|
|
|
int DropImporter::importAttachments(
|
|
const QList<QUrl>& urls, const QString& html, const QByteArray& rawImage) {
|
|
const int id = ++m_nextId;
|
|
m_requests[id].pending = 1;
|
|
|
|
if (!rawImage.isEmpty()) {
|
|
saveBytes(id, rawImage);
|
|
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
|
|
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
|
|
const int comma = src.indexOf(QLatin1Char(','));
|
|
if (comma < 0) {
|
|
failWith(id, tr("Malformed data URI"));
|
|
} else {
|
|
const bool b64 = src.left(comma).contains(
|
|
QLatin1String(";base64"), Qt::CaseInsensitive);
|
|
const QString payload = src.mid(comma + 1);
|
|
saveBytes(
|
|
id,
|
|
b64 ? QByteArray::fromBase64(payload.toLatin1())
|
|
: QByteArray::fromPercentEncoding(payload.toUtf8()));
|
|
}
|
|
} else {
|
|
const QUrl u(src);
|
|
if (u.isLocalFile())
|
|
addLocal(id, u);
|
|
else if (isHttp(u))
|
|
download(id, u);
|
|
else
|
|
failWith(id, tr("Unsupported image source"));
|
|
}
|
|
} else {
|
|
for (const QUrl& u : urls) {
|
|
if (u.isLocalFile())
|
|
addLocal(id, u);
|
|
else if (isHttp(u))
|
|
download(id, u);
|
|
else
|
|
failWith(id, tr("Unsupported URL: %1").arg(u.toString()));
|
|
}
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this, [this, id] { finishUnit(id); }, Qt::QueuedConnection);
|
|
return id;
|
|
}
|
|
|
|
void DropImporter::addLocal(int id, const QUrl& url) {
|
|
const QFileInfo fi(url.toLocalFile());
|
|
if (!fi.exists())
|
|
failWith(id, tr("File not found: %1").arg(fi.filePath()));
|
|
else if (fi.isDir())
|
|
failWith(id, tr("Folders can't be attached: %1").arg(fi.fileName()));
|
|
else if (fi.size() > kMaxBytes)
|
|
failWith(id, tr("File too large: %1").arg(fi.fileName()));
|
|
else
|
|
succeed(id, fi.absoluteFilePath());
|
|
}
|
|
|
|
void DropImporter::saveBytes(int id, const QByteArray& bytes) {
|
|
if (bytes.isEmpty() || bytes.size() > kMaxBytes) {
|
|
failWith(id, tr("Empty or oversized data"));
|
|
return;
|
|
}
|
|
const QString dir = newSubdir();
|
|
if (dir.isEmpty()) {
|
|
failWith(id, tr("No temp directory"));
|
|
return;
|
|
}
|
|
|
|
const QString suffix =
|
|
QMimeDatabase().mimeTypeForData(bytes).preferredSuffix();
|
|
const QString path =
|
|
dir + QStringLiteral("/dropped") +
|
|
(suffix.isEmpty() ? QString() : QLatin1Char('.') + suffix);
|
|
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::WriteOnly) || f.write(bytes) != bytes.size()) {
|
|
failWith(id, tr("Could not write temp file"));
|
|
return;
|
|
}
|
|
f.close();
|
|
succeed(id, path);
|
|
}
|
|
|
|
void DropImporter::download(int id, const QUrl& url) {
|
|
const QString dir = newSubdir();
|
|
if (dir.isEmpty()) {
|
|
failWith(id, tr("No temp directory"));
|
|
return;
|
|
}
|
|
|
|
++m_requests[id].pending;
|
|
|
|
QNetworkRequest req(url);
|
|
req.setAttribute(
|
|
QNetworkRequest::RedirectPolicyAttribute,
|
|
QNetworkRequest::NoLessSafeRedirectPolicy);
|
|
QNetworkReply* reply = m_nam.get(req);
|
|
auto* file = new QFile(dir + QStringLiteral("/download.part"), reply);
|
|
|
|
connect(reply, &QNetworkReply::readyRead, this, [reply, file] {
|
|
if (!file->isOpen() && !file->open(QIODevice::WriteOnly)) {
|
|
reply->abort();
|
|
return;
|
|
}
|
|
file->write(reply->readAll());
|
|
if (file->size() > kMaxBytes) reply->abort();
|
|
});
|
|
|
|
connect(
|
|
reply,
|
|
&QNetworkReply::finished,
|
|
this,
|
|
[this, reply, file, id, dir, url] {
|
|
reply->deleteLater();
|
|
const bool tooBig = file->size() > kMaxBytes;
|
|
const bool wrote = file->isOpen();
|
|
file->close();
|
|
|
|
if (tooBig) {
|
|
failWith(id, tr("Download too large: %1").arg(url.toString()));
|
|
} else if (reply->error() != QNetworkReply::NoError) {
|
|
failWith(
|
|
id,
|
|
tr("Download failed (%1): %2")
|
|
.arg(url.toString(), reply->errorString()));
|
|
} else if (!wrote) {
|
|
failWith(id, tr("Empty response: %1").arg(url.toString()));
|
|
} else {
|
|
const QString name = fileNameFor(reply, url);
|
|
QString finalPath = dir + QLatin1Char('/') + name;
|
|
if (QFile::rename(file->fileName(), finalPath)) {
|
|
if (QFileInfo(name).suffix().isEmpty()) {
|
|
const QString sfx =
|
|
QMimeDatabase()
|
|
.mimeTypeForFile(
|
|
finalPath, QMimeDatabase::MatchContent)
|
|
.preferredSuffix();
|
|
if (!sfx.isEmpty() &&
|
|
QFile::rename(
|
|
finalPath, finalPath + QLatin1Char('.') + sfx))
|
|
finalPath += QLatin1Char('.') + sfx;
|
|
}
|
|
succeed(id, finalPath);
|
|
} else {
|
|
failWith(id, tr("Could not store download"));
|
|
}
|
|
}
|
|
finishUnit(id);
|
|
});
|
|
}
|
|
|
|
void DropImporter::succeed(int id, const QString& path) {
|
|
if (auto it = m_requests.find(id); it != m_requests.end())
|
|
it->files.append(path);
|
|
}
|
|
|
|
void DropImporter::failWith(int id, const QString& message) {
|
|
if (auto it = m_requests.find(id); it != m_requests.end())
|
|
it->errors.append(message);
|
|
}
|
|
|
|
void DropImporter::finishUnit(int id) {
|
|
auto it = m_requests.find(id);
|
|
if (it == m_requests.end() || --it->pending > 0) return;
|
|
const Request r = *it;
|
|
m_requests.erase(it);
|
|
emit importFinished(id, r.files, r.errors);
|
|
}
|
|
|
|
QString DropImporter::newSubdir() {
|
|
if (!m_tempDir.isValid()) return {};
|
|
const QString p = m_tempDir.filePath(QString::number(++m_counter));
|
|
return QDir().mkpath(p) ? p : QString();
|
|
}
|
|
|
|
} // namespace ZShell::Services
|