1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "mimetypefinderjob.h"
9
10#include "global.h"
11#include "job.h" // for buildErrorString
12#include "kiocoredebug.h"
13#include "statjob.h"
14#include "transferjob.h"
15
16#include <KLocalizedString>
17#include <KProtocolManager>
18
19#include <QMimeDatabase>
20#include <QTimer>
21#include <QUrl>
22
23class KIO::MimeTypeFinderJobPrivate
24{
25public:
26 explicit MimeTypeFinderJobPrivate(const QUrl &url, MimeTypeFinderJob *qq)
27 : m_url(url)
28 , q(qq)
29 {
30 q->setCapabilities(KJob::Killable);
31 }
32
33 void statFile();
34 void scanFileWithGet();
35
36 QUrl m_url;
37 KIO::MimeTypeFinderJob *const q;
38 QString m_mimeTypeName;
39 QString m_suggestedFileName;
40 bool m_followRedirections = true;
41 bool m_authPrompts = true;
42};
43
44KIO::MimeTypeFinderJob::MimeTypeFinderJob(const QUrl &url, QObject *parent)
45 : KCompositeJob(parent)
46 , d(new MimeTypeFinderJobPrivate(url, this))
47{
48}
49
50KIO::MimeTypeFinderJob::~MimeTypeFinderJob() = default;
51
52void KIO::MimeTypeFinderJob::start()
53{
54 if (!d->m_url.isValid() || d->m_url.scheme().isEmpty()) {
55 const QString error = !d->m_url.isValid() ? d->m_url.errorString() : d->m_url.toDisplayString();
56 setError(KIO::ERR_MALFORMED_URL);
57 setErrorText(i18n("Malformed URL\n%1", error));
58 emitResult();
59 return;
60 }
61
62 if (!KProtocolManager::supportsListing(url: d->m_url)) {
63 // No support for listing => it can't be a directory (example: http)
64 d->scanFileWithGet();
65 return;
66 }
67
68 // It may be a directory or a file, let's use stat to find out
69 d->statFile();
70}
71
72void KIO::MimeTypeFinderJob::setFollowRedirections(bool b)
73{
74 d->m_followRedirections = b;
75}
76
77void KIO::MimeTypeFinderJob::setSuggestedFileName(const QString &suggestedFileName)
78{
79 d->m_suggestedFileName = suggestedFileName;
80}
81
82QString KIO::MimeTypeFinderJob::suggestedFileName() const
83{
84 return d->m_suggestedFileName;
85}
86
87QString KIO::MimeTypeFinderJob::mimeType() const
88{
89 return d->m_mimeTypeName;
90}
91
92void KIO::MimeTypeFinderJob::setAuthenticationPromptEnabled(bool enable)
93{
94 d->m_authPrompts = enable;
95}
96
97bool KIO::MimeTypeFinderJob::isAuthenticationPromptEnabled() const
98{
99 return d->m_authPrompts;
100}
101
102bool KIO::MimeTypeFinderJob::doKill()
103{
104 // This should really be in KCompositeJob...
105 const QList<KJob *> jobs = subjobs();
106 for (KJob *job : jobs) {
107 job->kill(); // ret val ignored, see below
108 }
109 // Even if for some reason killing a subjob fails,
110 // we can still consider this job as killed.
111 // The stat() or get() subjob has no side effects.
112 return true;
113}
114
115void KIO::MimeTypeFinderJob::slotResult(KJob *job)
116{
117 // We do the error handling elsewhere, just do the bookkeeping here
118 removeSubjob(job);
119}
120
121void KIO::MimeTypeFinderJobPrivate::statFile()
122{
123 Q_ASSERT(m_mimeTypeName.isEmpty());
124
125 constexpr auto statFlags = KIO::StatBasic | KIO::StatResolveSymlink | KIO::StatMimeType;
126
127 KIO::StatJob *job = KIO::stat(url: m_url, side: KIO::StatJob::SourceSide, details: statFlags, flags: KIO::HideProgressInfo);
128 if (!m_authPrompts) {
129 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
130 }
131 q->addSubjob(job);
132 QObject::connect(sender: job, signal: &KJob::result, context: q, slot: [=, this]() {
133 const int errCode = job->error();
134 if (errCode) {
135 // ERR_NO_CONTENT is not an error, but an indication no further
136 // actions need to be taken.
137 if (errCode != KIO::ERR_NO_CONTENT) {
138 q->setError(errCode);
139 // We're a KJob, not a KIO::Job, so build the error string here
140 q->setErrorText(KIO::buildErrorString(errorCode: errCode, errorText: job->errorText()));
141 }
142 q->emitResult();
143 return;
144 }
145 if (m_followRedirections) { // Update our URL in case of a redirection
146 m_url = job->url();
147 }
148
149 const KIO::UDSEntry entry = job->statResult();
150
151 qCDebug(KIO_CORE) << "UDSEntry from StatJob in MimeTypeFinderJob" << entry;
152
153 const QString localPath = entry.stringValue(field: KIO::UDSEntry::UDS_LOCAL_PATH);
154 if (!localPath.isEmpty()) {
155 m_url = QUrl::fromLocalFile(localfile: localPath);
156 }
157
158 // MIME type already known? (e.g. print:/manager)
159 m_mimeTypeName = entry.stringValue(field: KIO::UDSEntry::UDS_MIME_TYPE);
160 if (!m_mimeTypeName.isEmpty()) {
161 q->emitResult();
162 return;
163 }
164
165 if (entry.isDir()) {
166 m_mimeTypeName = QStringLiteral("inode/directory");
167 q->emitResult();
168 } else { // It's a file
169 // Start the timer. Once we get the timer event this
170 // protocol server is back in the pool and we can reuse it.
171 // This gives better performance than starting a new worker
172 QTimer::singleShot(interval: 0, receiver: q, slot: [this] {
173 scanFileWithGet();
174 });
175 }
176 });
177}
178
179static QMimeType fixupMimeType(const QString &mimeType, const QString &fileName)
180{
181 QMimeDatabase db;
182 QMimeType mime = db.mimeTypeForName(nameOrAlias: mimeType);
183 if ((!mime.isValid() || mime.isDefault()) && !fileName.isEmpty()) {
184 mime = db.mimeTypeForFile(fileName, mode: QMimeDatabase::MatchExtension);
185 }
186 return mime;
187}
188
189void KIO::MimeTypeFinderJobPrivate::scanFileWithGet()
190{
191 Q_ASSERT(m_mimeTypeName.isEmpty());
192
193 if (!KProtocolManager::supportsReading(url: m_url)) {
194 qCDebug(KIO_CORE) << "No support for reading from" << m_url.scheme();
195 q->setError(KIO::ERR_CANNOT_READ);
196 // We're a KJob, not a KIO::Job, so build the error string here
197 q->setErrorText(KIO::buildErrorString(errorCode: q->error(), errorText: m_url.toDisplayString()));
198 q->emitResult();
199 return;
200 }
201 // qDebug() << this << "Scanning file" << url;
202
203 KIO::TransferJob *job = KIO::get(url: m_url, reload: KIO::NoReload /*reload*/, flags: KIO::HideProgressInfo);
204 if (!m_authPrompts) {
205 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
206 }
207 q->addSubjob(job);
208 QObject::connect(sender: job, signal: &KJob::result, context: q, slot: [=, this]() {
209 const int errCode = job->error();
210 if (errCode) {
211 // ERR_NO_CONTENT is not an error, but an indication no further
212 // actions need to be taken.
213 if (errCode != KIO::ERR_NO_CONTENT) {
214 q->setError(errCode);
215 q->setErrorText(job->errorString());
216 }
217 q->emitResult();
218 }
219 // if the job succeeded, we certainly hope it emitted mimeTypeFound()...
220 if (m_mimeTypeName.isEmpty()) {
221 qCWarning(KIO_CORE) << "KIO::get didn't emit a mimetype! Please fix the KIO worker for URL" << m_url;
222 q->setError(KIO::ERR_INTERNAL);
223 q->setErrorText(i18n("Unable to determine the type of file for %1", m_url.toDisplayString()));
224 q->emitResult();
225 }
226 });
227 QObject::connect(sender: job, signal: &KIO::TransferJob::mimeTypeFound, context: q, slot: [=, this](KIO::Job *, const QString &mimetype) {
228 if (m_followRedirections) { // Update our URL in case of a redirection
229 m_url = job->url();
230 }
231 if (mimetype.isEmpty()) {
232 qCWarning(KIO_CORE) << "get() didn't emit a MIME type! Probably a KIO worker bug, please check the implementation of" << m_url.scheme();
233 }
234 m_mimeTypeName = mimetype;
235
236 // If the current MIME type is the default MIME type, then attempt to
237 // determine the "real" MIME type from the file name (bug #279675)
238 const QMimeType mime = fixupMimeType(mimeType: m_mimeTypeName, fileName: m_suggestedFileName.isEmpty() ? m_url.fileName() : m_suggestedFileName);
239 if (mime.isValid()) {
240 m_mimeTypeName = mime.name();
241 }
242
243 if (m_suggestedFileName.isEmpty()) {
244 m_suggestedFileName = job->queryMetaData(QStringLiteral("content-disposition-filename"));
245 }
246
247 q->emitResult();
248 });
249}
250
251#include "moc_mimetypefinderjob.cpp"
252

source code of kio/src/core/mimetypefinderjob.cpp