diff --git a/src/core/global.cpp b/src/core/global.cpp index c1763158..a950aacf 100644 --- a/src/core/global.cpp +++ b/src/core/global.cpp @@ -1,339 +1,339 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "global.h" #include "kioglobal_p.h" #include "faviconscache_p.h" #include #include #include #include #include #include #include #include #include #include #include "kiocoredebug.h" KFormat::BinaryUnitDialect _k_loadBinaryDialect(); Q_GLOBAL_STATIC_WITH_ARGS(KFormat::BinaryUnitDialect, _k_defaultBinaryDialect, (_k_loadBinaryDialect())) KFormat::BinaryUnitDialect _k_loadBinaryDialect() { KConfigGroup mainGroup(KSharedConfig::openConfig(), "Locale"); KFormat::BinaryUnitDialect dialect(KFormat::BinaryUnitDialect(mainGroup.readEntry("BinaryUnitDialect", int(KFormat::DefaultBinaryDialect)))); dialect = static_cast(mainGroup.readEntry("BinaryUnitDialect", int(dialect))); // Error checking if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) { dialect = KFormat::IECBinaryDialect; } return dialect; } KIOCORE_EXPORT QString KIO::convertSize(KIO::filesize_t fileSize) { const KFormat::BinaryUnitDialect dialect = *_k_defaultBinaryDialect(); return KFormat().formatByteSize(fileSize, 1, dialect); } KIOCORE_EXPORT QString KIO::convertSizeFromKiB(KIO::filesize_t kibSize) { return convertSize(kibSize * 1024); } KIOCORE_EXPORT QString KIO::number(KIO::filesize_t size) { char charbuf[256]; sprintf(charbuf, "%lld", size); return QLatin1String(charbuf); } KIOCORE_EXPORT unsigned int KIO::calculateRemainingSeconds(KIO::filesize_t totalSize, KIO::filesize_t processedSize, KIO::filesize_t speed) { if ((speed != 0) && (totalSize != 0)) { return (totalSize - processedSize) / speed; } else { return 0; } } KIOCORE_EXPORT QString KIO::convertSeconds(unsigned int seconds) { unsigned int days = seconds / 86400; unsigned int hours = (seconds - (days * 86400)) / 3600; unsigned int mins = (seconds - (days * 86400) - (hours * 3600)) / 60; seconds = (seconds - (days * 86400) - (hours * 3600) - (mins * 60)); const QTime time(hours, mins, seconds); const QString timeStr(time.toString(QStringLiteral("hh:mm:ss"))); if (days > 0) { return i18np("1 day %2", "%1 days %2", days, timeStr); } else { return timeStr; } } #ifndef KIOCORE_NO_DEPRECATED KIOCORE_EXPORT QTime KIO::calculateRemaining(KIO::filesize_t totalSize, KIO::filesize_t processedSize, KIO::filesize_t speed) { QTime remainingTime; if (speed != 0) { KIO::filesize_t secs; if (totalSize == 0) { secs = 0; } else { secs = (totalSize - processedSize) / speed; } if (secs >= (24 * 60 * 60)) { // Limit to 23:59:59 secs = (24 * 60 * 60) - 1; } int hr = secs / (60 * 60); int mn = (secs - hr * 60 * 60) / 60; int sc = (secs - hr * 60 * 60 - mn * 60); remainingTime.setHMS(hr, mn, sc); } return remainingTime; } #endif KIOCORE_EXPORT QString KIO::itemsSummaryString(uint items, uint files, uint dirs, KIO::filesize_t size, bool showSize) { if (files == 0 && dirs == 0 && items == 0) { return i18np("%1 Item", "%1 Items", 0); } QString summary; const QString foldersText = i18np("1 Folder", "%1 Folders", dirs); const QString filesText = i18np("1 File", "%1 Files", files); if (files > 0 && dirs > 0) { summary = showSize ? i18nc("folders, files (size)", "%1, %2 (%3)", foldersText, filesText, KIO::convertSize(size)) : i18nc("folders, files", "%1, %2", foldersText, filesText); } else if (files > 0) { summary = showSize ? i18nc("files (size)", "%1 (%2)", filesText, KIO::convertSize(size)) : filesText; } else if (dirs > 0) { summary = foldersText; } if (items > dirs + files) { const QString itemsText = i18np("%1 Item", "%1 Items", items); summary = summary.isEmpty() ? itemsText : i18nc("items: folders, files (size)", "%1: %2", itemsText, summary); } return summary; } KIOCORE_EXPORT QString KIO::encodeFileName(const QString &_str) { QString str(_str); str.replace(QLatin1Char('/'), QChar(0x2044)); // "Fraction slash" return str; } KIOCORE_EXPORT QString KIO::decodeFileName(const QString &_str) { // Nothing to decode. "Fraction slash" is fine in filenames. return _str; } /*************************************************************** * * Utility functions * ***************************************************************/ KIO::CacheControl KIO::parseCacheControl(const QString &cacheControl) { QString tmp = cacheControl.toLower(); if (tmp == QLatin1String("cacheonly")) { return KIO::CC_CacheOnly; } if (tmp == QLatin1String("cache")) { return KIO::CC_Cache; } if (tmp == QLatin1String("verify")) { return KIO::CC_Verify; } if (tmp == QLatin1String("refresh")) { return KIO::CC_Refresh; } if (tmp == QLatin1String("reload")) { return KIO::CC_Reload; } qCDebug(KIO_CORE) << "unrecognized Cache control option:" << cacheControl; return KIO::CC_Verify; } QString KIO::getCacheControlString(KIO::CacheControl cacheControl) { if (cacheControl == KIO::CC_CacheOnly) { return QStringLiteral("CacheOnly"); } if (cacheControl == KIO::CC_Cache) { return QStringLiteral("Cache"); } if (cacheControl == KIO::CC_Verify) { return QStringLiteral("Verify"); } if (cacheControl == KIO::CC_Refresh) { return QStringLiteral("Refresh"); } if (cacheControl == KIO::CC_Reload) { return QStringLiteral("Reload"); } qCDebug(KIO_CORE) << "unrecognized Cache control enum value:" << cacheControl; return QString(); } QString KIO::favIconForUrl(const QUrl &url) { if (url.isLocalFile() || !url.scheme().startsWith(QLatin1String("http"))) { return QString(); } return FavIconsCache::instance()->iconForUrl(url); } QString KIO::iconNameForUrl(const QUrl &url) { const QLatin1String unknown("unknown"); if (url.scheme().isEmpty()) { // empty URL or relative URL (e.g. '~') return unknown; } QMimeDatabase db; const QMimeType mt = db.mimeTypeForUrl(url); const QString mimeTypeIcon = mt.iconName(); QString i = mimeTypeIcon; // check whether it's a xdg location (e.g. Pictures folder) if (url.isLocalFile() && mt.inherits(QStringLiteral("inode/directory"))) { i = KIOPrivate::iconForStandardPath(url.toLocalFile()); } // if we don't find an icon, maybe we can use the one for the protocol if (i == unknown || i.isEmpty() || mt.isDefault() // and for the root of the protocol (e.g. trash:/) the protocol icon has priority over the mimetype icon || url.path().length() <= 1) { i = favIconForUrl(url); // maybe there is a favicon? // reflect actual fill state of trash can if (url.scheme() == QLatin1String("trash") && url.path().length() <= 1) { KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); if (trashConfig.group("Status").readEntry("Empty", true)) { i = QStringLiteral("user-trash"); } else { i = QStringLiteral("user-trash-full"); } } if (i.isEmpty()) { i = KProtocolInfo::icon(url.scheme()); } // root of protocol: if we found nothing, revert to mimeTypeIcon (which is usually "folder") if (url.path().length() <= 1 && (i == unknown || i.isEmpty())) { i = mimeTypeIcon; } } return !i.isEmpty() ? i : unknown; } QUrl KIO::upUrl(const QUrl &url) { if (!url.isValid() || url.isRelative()) { return QUrl(); } QUrl u(url); if (url.hasQuery()) { u.setQuery(QString()); return u; } if (url.hasFragment()) { u.setFragment(QString()); } u = u.adjusted(QUrl::StripTrailingSlash); /// don't combine with the line below return u.adjusted(QUrl::RemoveFilename); } QString KIO::suggestName(const QUrl &baseURL, const QString &oldName) { QString basename; // Extract the original file extension from the filename QMimeDatabase db; QString nameSuffix = db.suffixForFileName(oldName); if (oldName.lastIndexOf(QLatin1Char('.')) == 0) { basename = QStringLiteral("."); nameSuffix = oldName; } else if (nameSuffix.isEmpty()) { const int lastDot = oldName.lastIndexOf(QLatin1Char('.')); if (lastDot == -1) { basename = oldName; } else { basename = oldName.left(lastDot); nameSuffix = oldName.mid(lastDot); } } else { nameSuffix.prepend(QLatin1Char('.')); basename = oldName.left(oldName.length() - nameSuffix.length()); } // check if (number) exists from the end of the oldName and increment that number QRegExp numSearch(QStringLiteral("\\(\\d+\\)")); int start = numSearch.lastIndexIn(oldName); if (start != -1) { QString numAsStr = numSearch.cap(0); QString number = QString::number(numAsStr.midRef(1, numAsStr.size() - 2).toInt() + 1); - basename = basename.left(start) + QLatin1Char('(') + number + QLatin1Char(')'); + basename = basename.leftRef(start) + QLatin1Char('(') + number + QLatin1Char(')'); } else { // number does not exist, so just append " (1)" to filename basename += QLatin1String(" (1)"); } const QString suggestedName = basename + nameSuffix; // Check if suggested name already exists bool exists = false; // TODO: network transparency. However, using NetAccess from a modal dialog // could be a problem, no? (given that it uses a modal widget itself....) if (baseURL.isLocalFile()) { exists = QFileInfo::exists(baseURL.toLocalFile() + QLatin1Char('/') + suggestedName); } if (!exists) { return suggestedName; } else { // already exists -> recurse return suggestName(baseURL, suggestedName); } } diff --git a/src/core/kfileitemlistproperties.cpp b/src/core/kfileitemlistproperties.cpp index 2e5fd625..39b36521 100644 --- a/src/core/kfileitemlistproperties.cpp +++ b/src/core/kfileitemlistproperties.cpp @@ -1,218 +1,218 @@ /* This file is part of the KDE project Copyright (C) 2008 by Peter Penz Copyright (C) 2008 by George Goldberg Copyright 2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfileitemlistproperties.h" #include #include #include class KFileItemListPropertiesPrivate : public QSharedData { public: KFileItemListPropertiesPrivate() : m_isDirectory(false), m_isFile(false), m_supportsReading(false), m_supportsDeleting(false), m_supportsWriting(false), m_supportsMoving(false), m_isLocal(true) { } void setItems(const KFileItemList &items); void determineMimeTypeAndGroup() const; KFileItemList m_items; mutable QString m_mimeType; mutable QString m_mimeGroup; bool m_isDirectory : 1; bool m_isFile : 1; bool m_supportsReading : 1; bool m_supportsDeleting : 1; bool m_supportsWriting : 1; bool m_supportsMoving : 1; bool m_isLocal : 1; }; KFileItemListProperties::KFileItemListProperties() : d(new KFileItemListPropertiesPrivate) { } KFileItemListProperties::KFileItemListProperties(const KFileItemList &items) : d(new KFileItemListPropertiesPrivate) { setItems(items); } void KFileItemListProperties::setItems(const KFileItemList &items) { d->setItems(items); } void KFileItemListPropertiesPrivate::setItems(const KFileItemList &items) { const bool initialValue = !items.isEmpty(); m_items = items; m_supportsReading = initialValue; m_supportsDeleting = initialValue; m_supportsWriting = initialValue; m_supportsMoving = initialValue; m_isDirectory = initialValue; m_isFile = initialValue; m_isLocal = true; m_mimeType.clear(); m_mimeGroup.clear(); QFileInfo parentDirInfo; foreach (const KFileItem &item, items) { bool isLocal = false; const QUrl url = item.mostLocalUrl(&isLocal); m_isLocal = m_isLocal && isLocal; m_supportsReading = m_supportsReading && KProtocolManager::supportsReading(url); m_supportsDeleting = m_supportsDeleting && KProtocolManager::supportsDeleting(url); m_supportsWriting = m_supportsWriting && KProtocolManager::supportsWriting(url) && item.isWritable(); m_supportsMoving = m_supportsMoving && KProtocolManager::supportsMoving(url); // For local files we can do better: check if we have write permission in parent directory // TODO: if we knew about the parent KFileItem, we could even do that for remote protocols too #ifndef Q_OS_WIN if (m_isLocal && (m_supportsDeleting || m_supportsMoving)) { const QString directory = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); if (parentDirInfo.filePath() != directory) { parentDirInfo.setFile(directory); } if (!parentDirInfo.isWritable()) { m_supportsDeleting = false; m_supportsMoving = false; } } #else if (m_isLocal && m_supportsDeleting) { if (!QFileInfo(url.toLocalFile()).isWritable()) m_supportsDeleting = false; } #endif if (m_isDirectory && !item.isDir()) { m_isDirectory = false; } if (m_isFile && !item.isFile()) { m_isFile = false; } } } KFileItemListProperties::KFileItemListProperties(const KFileItemListProperties &other) : d(other.d) { } KFileItemListProperties &KFileItemListProperties::operator=(const KFileItemListProperties &other) { d = other.d; return *this; } KFileItemListProperties::~KFileItemListProperties() { } bool KFileItemListProperties::supportsReading() const { return d->m_supportsReading; } bool KFileItemListProperties::supportsDeleting() const { return d->m_supportsDeleting; } bool KFileItemListProperties::supportsWriting() const { return d->m_supportsWriting; } bool KFileItemListProperties::supportsMoving() const { return d->m_supportsMoving && d->m_supportsDeleting; } bool KFileItemListProperties::isLocal() const { return d->m_isLocal; } KFileItemList KFileItemListProperties::items() const { return d->m_items; } QList KFileItemListProperties::urlList() const { return d->m_items.targetUrlList(); } bool KFileItemListProperties::isDirectory() const { return d->m_isDirectory; } bool KFileItemListProperties::isFile() const { return d->m_isFile; } QString KFileItemListProperties::mimeType() const { if (d->m_mimeType.isEmpty()) { d->determineMimeTypeAndGroup(); } return d->m_mimeType; } QString KFileItemListProperties::mimeGroup() const { if (d->m_mimeType.isEmpty()) { d->determineMimeTypeAndGroup(); } return d->m_mimeGroup; } void KFileItemListPropertiesPrivate::determineMimeTypeAndGroup() const { if (!m_items.isEmpty()) { m_mimeType = m_items.first().mimetype(); m_mimeGroup = m_mimeType.left(m_mimeType.indexOf(QLatin1Char('/'))); } foreach (const KFileItem &item, m_items) { const QString itemMimeType = item.mimetype(); // Determine if common mimetype among all items if (m_mimeType != itemMimeType) { m_mimeType.clear(); - if (m_mimeGroup != itemMimeType.left(itemMimeType.indexOf(QLatin1Char('/')))) { + if (m_mimeGroup != itemMimeType.leftRef(itemMimeType.indexOf(QLatin1Char('/')))) { m_mimeGroup.clear(); // mimetype groups are different as well! } } } } diff --git a/src/core/kprotocolmanager.cpp b/src/core/kprotocolmanager.cpp index a0aff9e7..3c1fdb0a 100644 --- a/src/core/kprotocolmanager.cpp +++ b/src/core/kprotocolmanager.cpp @@ -1,1327 +1,1327 @@ /* This file is part of the KDE libraries Copyright (C) 1999 Torben Weis Copyright (C) 2000- Waldo Bastain Copyright (C) 2000- Dawit Alemayehu Copyright (C) 2008 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kprotocolmanager.h" #include "kprotocolinfo_p.h" #include "hostinfo.h" #include #include #include #ifdef Q_OS_WIN #include #undef interface //windows.h defines this, breaks QtDBus since it has parameters named interface #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(QT_NO_NETWORKPROXY) && (defined (Q_OS_WIN32) || defined(Q_OS_MAC)) #include #include #endif #include #include #include #include #include #include "slaveconfig.h" #include "ioslave_defaults.h" #include "http_slave_defaults.h" #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) typedef QPair SubnetPair; /* Domain suffix match. E.g. return true if host is "cuzco.inka.de" and nplist is "inka.de,hadiko.de" or if host is "localhost" and nplist is "localhost". */ static bool revmatch(const char *host, const char *nplist) { if (host == nullptr) { return false; } const char *hptr = host + strlen(host) - 1; const char *nptr = nplist + strlen(nplist) - 1; const char *shptr = hptr; while (nptr >= nplist) { if (*hptr != *nptr) { hptr = shptr; // Try to find another domain or host in the list while (--nptr >= nplist && *nptr != ',' && *nptr != ' '); // Strip out multiple spaces and commas while (--nptr >= nplist && (*nptr == ',' || *nptr == ' ')); } else { if (nptr == nplist || nptr[-1] == ',' || nptr[-1] == ' ') { return true; } if (nptr[-1] == '/' && hptr == host) { // "bugs.kde.org" vs "http://bugs.kde.org", the config UI says URLs are ok return true; } if (hptr == host) { // e.g. revmatch("bugs.kde.org","mybugs.kde.org") return false; } hptr--; nptr--; } } return false; } class KProxyData : public QObject { Q_OBJECT public: KProxyData(const QString &slaveProtocol, const QStringList &proxyAddresses) : protocol(slaveProtocol) , proxyList(proxyAddresses) { } void removeAddress(const QString &address) { proxyList.removeAll(address); } QString protocol; QStringList proxyList; }; class KProtocolManagerPrivate { public: KProtocolManagerPrivate(); ~KProtocolManagerPrivate(); bool shouldIgnoreProxyFor(const QUrl &url); void sync(); KProtocolManager::ProxyType proxyType(); bool useReverseProxy(); QString readNoProxyFor(); QString proxyFor(const QString &protocol); QStringList getSystemProxyFor(const QUrl &url); QMutex mutex; // protects all member vars KSharedConfig::Ptr configPtr; KSharedConfig::Ptr http_config; QString modifiers; QString useragent; QString noProxyFor; QList noProxySubnets; QCache cachedProxyData; QMap protocolForArchiveMimetypes; }; Q_GLOBAL_STATIC(KProtocolManagerPrivate, kProtocolManagerPrivate) static void syncOnExit() { if (kProtocolManagerPrivate.exists()) kProtocolManagerPrivate()->sync(); } KProtocolManagerPrivate::KProtocolManagerPrivate() { // post routine since KConfig::sync() breaks if called too late qAddPostRoutine(syncOnExit); cachedProxyData.setMaxCost(200); // double the max cost. } KProtocolManagerPrivate::~KProtocolManagerPrivate() { } /* * Returns true if url is in the no proxy list. */ bool KProtocolManagerPrivate::shouldIgnoreProxyFor(const QUrl &url) { bool isMatch = false; const KProtocolManager::ProxyType type = proxyType(); const bool useRevProxy = ((type == KProtocolManager::ManualProxy) && useReverseProxy()); const bool useNoProxyList = (type == KProtocolManager::ManualProxy || type == KProtocolManager::EnvVarProxy); // No proxy only applies to ManualProxy and EnvVarProxy types... if (useNoProxyList && noProxyFor.isEmpty()) { QStringList noProxyForList(readNoProxyFor().split(QL1C(','))); QMutableStringListIterator it(noProxyForList); while (it.hasNext()) { SubnetPair subnet = QHostAddress::parseSubnet(it.next()); if (!subnet.first.isNull()) { noProxySubnets << subnet; it.remove(); } } noProxyFor = noProxyForList.join(QLatin1Char(',')); } if (!noProxyFor.isEmpty()) { QString qhost = url.host().toLower(); QByteArray host = qhost.toLatin1(); const QString qno_proxy = noProxyFor.trimmed().toLower(); const QByteArray no_proxy = qno_proxy.toLatin1(); isMatch = revmatch(host.constData(), no_proxy.constData()); // If no match is found and the request url has a port // number, try the combination of "host:port". This allows // users to enter host:port in the No-proxy-For list. if (!isMatch && url.port() > 0) { qhost += QL1C(':') + QString::number(url.port()); host = qhost.toLatin1(); isMatch = revmatch(host.constData(), no_proxy.constData()); } // If the hostname does not contain a dot, check if // is part of noProxy. if (!isMatch && !host.isEmpty() && (strchr(host.constData(), '.') == nullptr)) { isMatch = revmatch("", no_proxy.constData()); } } const QString host(url.host()); if (!noProxySubnets.isEmpty() && !host.isEmpty()) { QHostAddress address(host); // If request url is not IP address, do a DNS lookup of the hostname. // TODO: Perhaps we should make configurable ? if (address.isNull()) { //qDebug() << "Performing DNS lookup for" << host; QHostInfo info = KIO::HostInfo::lookupHost(host, 2000); const QList addresses = info.addresses(); if (!addresses.isEmpty()) { address = addresses.first(); } } if (!address.isNull()) { Q_FOREACH (const SubnetPair &subnet, noProxySubnets) { if (address.isInSubnet(subnet)) { isMatch = true; break; } } } } return (useRevProxy != isMatch); } void KProtocolManagerPrivate::sync() { QMutexLocker lock(&mutex); if (http_config) { http_config->sync(); } if (configPtr) { configPtr->sync(); } } #define PRIVATE_DATA \ KProtocolManagerPrivate *d = kProtocolManagerPrivate() void KProtocolManager::reparseConfiguration() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (d->http_config) { d->http_config->reparseConfiguration(); } if (d->configPtr) { d->configPtr->reparseConfiguration(); } d->cachedProxyData.clear(); d->noProxyFor.clear(); d->modifiers.clear(); d->useragent.clear(); lock.unlock(); // Force the slave config to re-read its config... KIO::SlaveConfig::self()->reset(); } static KSharedConfig::Ptr config() { PRIVATE_DATA; Q_ASSERT(!d->mutex.tryLock()); // the caller must have locked the mutex if (!d->configPtr) { d->configPtr = KSharedConfig::openConfig(QStringLiteral("kioslaverc"), KConfig::NoGlobals); } return d->configPtr; } KProtocolManager::ProxyType KProtocolManagerPrivate::proxyType() { KConfigGroup cg(config(), "Proxy Settings"); return static_cast(cg.readEntry("ProxyType", 0)); } bool KProtocolManagerPrivate::useReverseProxy() { KConfigGroup cg(config(), "Proxy Settings"); return cg.readEntry("ReversedException", false); } QString KProtocolManagerPrivate::readNoProxyFor() { QString noProxy = config()->group("Proxy Settings").readEntry("NoProxyFor"); if (proxyType() == KProtocolManager::EnvVarProxy) { noProxy = QString::fromLocal8Bit(qgetenv(noProxy.toLocal8Bit().constData())); } return noProxy; } QMap KProtocolManager::entryMap(const QString &group) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->entryMap(group); } static KConfigGroup http_config() { PRIVATE_DATA; Q_ASSERT(!d->mutex.tryLock()); // the caller must have locked the mutex if (!d->http_config) { d->http_config = KSharedConfig::openConfig(QStringLiteral("kio_httprc"), KConfig::NoGlobals); } return KConfigGroup(d->http_config, QString()); } /*=============================== TIMEOUT SETTINGS ==========================*/ int KProtocolManager::readTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ReadTimeout", DEFAULT_READ_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::connectTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ConnectTimeout", DEFAULT_CONNECT_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::proxyConnectTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ProxyConnectTimeout", DEFAULT_PROXY_CONNECT_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::responseTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ResponseTimeout", DEFAULT_RESPONSE_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } /*========================== PROXY SETTINGS =================================*/ bool KProtocolManager::useProxy() { return proxyType() != NoProxy; } bool KProtocolManager::useReverseProxy() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->useReverseProxy(); } KProtocolManager::ProxyType KProtocolManager::proxyType() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->proxyType(); } KProtocolManager::ProxyAuthMode KProtocolManager::proxyAuthMode() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), "Proxy Settings"); return static_cast(cg.readEntry("AuthMode", 0)); } /*========================== CACHING =====================================*/ bool KProtocolManager::useCache() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("UseCache", true); } KIO::CacheControl KProtocolManager::cacheControl() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); QString tmp = http_config().readEntry("cache"); if (tmp.isEmpty()) { return DEFAULT_CACHE_CONTROL; } return KIO::parseCacheControl(tmp); } QString KProtocolManager::cacheDir() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readPathEntry("CacheDir", QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_http")); } int KProtocolManager::maxCacheAge() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); } int KProtocolManager::maxCacheSize() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE); } QString KProtocolManager::noProxyFor() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->readNoProxyFor(); } static QString adjustProtocol(const QString &scheme) { if (scheme.compare(QL1S("webdav"), Qt::CaseInsensitive) == 0) { return QStringLiteral("http"); } if (scheme.compare(QL1S("webdavs"), Qt::CaseInsensitive) == 0) { return QStringLiteral("https"); } return scheme.toLower(); } QString KProtocolManager::proxyFor(const QString &protocol) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->proxyFor(protocol); } QString KProtocolManagerPrivate::proxyFor(const QString &protocol) { const QString key = adjustProtocol(protocol) + QL1S("Proxy"); QString proxyStr(config()->group("Proxy Settings").readEntry(key)); const int index = proxyStr.lastIndexOf(QL1C(' ')); if (index > -1) { bool ok = false; const QString portStr(proxyStr.right(proxyStr.length() - index - 1)); portStr.toInt(&ok); if (ok) { - proxyStr = proxyStr.left(index) + QL1C(':') + portStr; + proxyStr = proxyStr.leftRef(index) + QL1C(':') + portStr; } else { proxyStr.clear(); } } return proxyStr; } QString KProtocolManager::proxyForUrl(const QUrl &url) { const QStringList proxies = proxiesForUrl(url); if (proxies.isEmpty()) { return QString(); } return proxies.first(); } QStringList KProtocolManagerPrivate::getSystemProxyFor(const QUrl &url) { QStringList proxies; #if !defined(QT_NO_NETWORKPROXY) && (defined(Q_OS_WIN32) || defined(Q_OS_MAC)) QNetworkProxyQuery query(url); const QList proxyList = QNetworkProxyFactory::systemProxyForQuery(query); Q_FOREACH (const QNetworkProxy &proxy, proxyList) { QUrl url; const QNetworkProxy::ProxyType type = proxy.type(); if (type == QNetworkProxy::NoProxy || type == QNetworkProxy::DefaultProxy) { proxies << QL1S("DIRECT"); continue; } if (type == QNetworkProxy::HttpProxy || type == QNetworkProxy::HttpCachingProxy) { url.setScheme(QL1S("http")); } else if (type == QNetworkProxy::Socks5Proxy) { url.setScheme(QL1S("socks")); } else if (type == QNetworkProxy::FtpCachingProxy) { url.setScheme(QL1S("ftp")); } url.setHost(proxy.hostName()); url.setPort(proxy.port()); url.setUserName(proxy.user()); proxies << url.url(); } #else // On Unix/Linux use system environment variables if any are set. QString proxyVar(proxyFor(url.scheme())); // Check for SOCKS proxy, if not proxy is found for given url. if (!proxyVar.isEmpty()) { const QString proxy(QString::fromLocal8Bit(qgetenv(proxyVar.toLocal8Bit().constData())).trimmed()); if (!proxy.isEmpty()) { proxies << proxy; } } // Add the socks proxy as an alternate proxy if it exists, proxyVar = proxyFor(QStringLiteral("socks")); if (!proxyVar.isEmpty()) { QString proxy = QString::fromLocal8Bit(qgetenv(proxyVar.toLocal8Bit().constData())).trimmed(); // Make sure the scheme of SOCKS proxy is always set to "socks://". const int index = proxy.indexOf(QL1S("://")); const int offset = (index == -1) ? 0 : (index + 3); proxy = QL1S("socks://") + proxy.midRef(offset); if (!proxy.isEmpty()) { proxies << proxy; } } #endif return proxies; } QStringList KProtocolManager::proxiesForUrl(const QUrl &url) { QStringList proxyList; PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (!d->shouldIgnoreProxyFor(url)) { switch (d->proxyType()) { case PACProxy: case WPADProxy: { QUrl u(url); const QString protocol = adjustProtocol(u.scheme()); u.setScheme(protocol); if (protocol.startsWith(QLatin1String("http")) || protocol.startsWith(QLatin1String("ftp"))) { QDBusReply reply = QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/proxyscout"), QStringLiteral("org.kde.KPAC.ProxyScout")) .call(QStringLiteral("proxiesForUrl"), u.toString()); proxyList = reply; } break; } case EnvVarProxy: proxyList = d->getSystemProxyFor(url); break; case ManualProxy: { QString proxy(d->proxyFor(url.scheme())); if (!proxy.isEmpty()) { proxyList << proxy; } // Add the socks proxy as an alternate proxy if it exists, proxy = d->proxyFor(QStringLiteral("socks")); if (!proxy.isEmpty()) { // Make sure the scheme of SOCKS proxy is always set to "socks://". const int index = proxy.indexOf(QL1S("://")); const int offset = (index == -1) ? 0 : (index + 3); proxy = QL1S("socks://") + proxy.midRef(offset); proxyList << proxy; } } break; case NoProxy: break; } } if (proxyList.isEmpty()) { proxyList << QStringLiteral("DIRECT"); } return proxyList; } void KProtocolManager::badProxy(const QString &proxy) { QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/proxyscout")) .asyncCall(QStringLiteral("blackListProxy"), proxy); PRIVATE_DATA; QMutexLocker lock(&d->mutex); const QStringList keys(d->cachedProxyData.keys()); Q_FOREACH (const QString &key, keys) { d->cachedProxyData[key]->removeAddress(proxy); } } QString KProtocolManager::slaveProtocol(const QUrl &url, QString &proxy) { QStringList proxyList; const QString protocol = KProtocolManager::slaveProtocol(url, proxyList); if (!proxyList.isEmpty()) { proxy = proxyList.first(); } return protocol; } // Generates proxy cache key from request given url. static void extractProxyCacheKeyFromUrl(const QUrl &u, QString *key) { if (!key) { return; } *key = u.scheme(); *key += u.host(); if (u.port() > 0) { *key += QString::number(u.port()); } } QString KProtocolManager::slaveProtocol(const QUrl &url, QStringList &proxyList) { #if 0 if (url.hasSubUrl()) { // We don't want the suburl's protocol const QUrl::List list = QUrl::split(url); return slaveProtocol(list.last(), proxyList); } #endif proxyList.clear(); // Do not perform a proxy lookup for any url classified as a ":local" url or // one that does not have a host component or if proxy is disabled. QString protocol(url.scheme()); if (url.host().isEmpty() || KProtocolInfo::protocolClass(protocol) == QL1S(":local") || KProtocolManager::proxyType() == KProtocolManager::NoProxy) { return protocol; } QString proxyCacheKey; extractProxyCacheKeyFromUrl(url, &proxyCacheKey); PRIVATE_DATA; QMutexLocker lock(&d->mutex); // Look for cached proxy information to avoid more work. if (d->cachedProxyData.contains(proxyCacheKey)) { KProxyData *data = d->cachedProxyData.object(proxyCacheKey); proxyList = data->proxyList; return data->protocol; } lock.unlock(); const QStringList proxies = proxiesForUrl(url); const int count = proxies.count(); if (count > 0 && !(count == 1 && proxies.first() == QL1S("DIRECT"))) { Q_FOREACH (const QString &proxy, proxies) { if (proxy == QL1S("DIRECT")) { proxyList << proxy; } else { QUrl u(proxy); if (!u.isEmpty() && u.isValid() && !u.scheme().isEmpty()) { proxyList << proxy; } } } } // The idea behind slave protocols is not applicable to http // and webdav protocols as well as protocols unknown to KDE. if (!proxyList.isEmpty() && !protocol.startsWith(QLatin1String("http")) && !protocol.startsWith(QLatin1String("webdav")) && KProtocolInfo::isKnownProtocol(protocol)) { Q_FOREACH (const QString &proxy, proxyList) { QUrl u(proxy); if (u.isValid() && KProtocolInfo::isKnownProtocol(u.scheme())) { protocol = u.scheme(); break; } } } lock.relock(); // cache the proxy information... d->cachedProxyData.insert(proxyCacheKey, new KProxyData(protocol, proxyList)); return protocol; } /*================================= USER-AGENT SETTINGS =====================*/ QString KProtocolManager::userAgentForHost(const QString &hostname) { const QString sendUserAgent = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), hostname.toLower(), QStringLiteral("SendUserAgent")).toLower(); if (sendUserAgent == QL1S("false")) { return QString(); } const QString useragent = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), hostname.toLower(), QStringLiteral("UserAgent")); // Return the default user-agent if none is specified // for the requested host. if (useragent.isEmpty()) { return defaultUserAgent(); } return useragent; } QString KProtocolManager::defaultUserAgent() { const QString modifiers = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), QString(), QStringLiteral("UserAgentKeys")); return defaultUserAgent(modifiers); } static QString defaultUserAgentFromPreferredService() { QString agentStr; // Check if the default COMPONENT contains a custom default UA string... KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html"), QStringLiteral("KParts/ReadOnlyPart")); if (service && service->showInCurrentDesktop()) agentStr = service->property(QStringLiteral("X-KDE-Default-UserAgent"), QVariant::String).toString(); return agentStr; } // This is not the OS, but the windowing system, e.g. X11 on Unix/Linux. static QString platform() { #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) return QStringLiteral("X11"); #elif defined(Q_OS_MAC) return QStringLiteral("Macintosh"); #elif defined(Q_OS_WIN) return QStringLiteral("Windows"); #else return QStringLiteral("Unknown"); #endif } QString KProtocolManager::defaultUserAgent(const QString &_modifiers) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); QString modifiers = _modifiers.toLower(); if (modifiers.isEmpty()) { modifiers = QStringLiteral(DEFAULT_USER_AGENT_KEYS); } if (d->modifiers == modifiers && !d->useragent.isEmpty()) { return d->useragent; } d->modifiers = modifiers; /* The following code attempts to determine the default user agent string from the 'X-KDE-UA-DEFAULT-STRING' property of the desktop file for the preferred service that was configured to handle the 'text/html' mime type. If the preferred service's desktop file does not specify this property, the long standing default user agent string will be used. The following keyword placeholders are automatically converted when the user agent string is read from the property: %SECURITY% Expands to"N" when SSL is not supported, otherwise it is ignored. %OSNAME% Expands to operating system name, e.g. Linux. %OSVERSION% Expands to operating system version, e.g. 2.6.32 %SYSTYPE% Expands to machine or system type, e.g. i386 %PLATFORM% Expands to windowing system, e.g. X11 on Unix/Linux. %LANGUAGE% Expands to default language in use, e.g. en-US. %APPVERSION% Expands to QCoreApplication applicationName()/applicationVerison(), e.g. Konqueror/4.5.0. If application name and/or application version number are not set, then "KDE" and the runtime KDE version numbers are used respectively. All of the keywords are handled case-insensitively. */ QString systemName, systemVersion, machine, supp; const bool sysInfoFound = getSystemNameVersionAndMachine(systemName, systemVersion, machine); QString agentStr = defaultUserAgentFromPreferredService(); if (agentStr.isEmpty()) { supp += platform(); if (sysInfoFound) { if (modifiers.contains(QL1C('o'))) { supp += QL1S("; ") + systemName; if (modifiers.contains(QL1C('v'))) { supp += QL1C(' ') + systemVersion; } if (modifiers.contains(QL1C('m'))) { supp += QL1C(' ') + machine; } } if (modifiers.contains(QL1C('l'))) { supp += QL1S("; ") + QLocale::languageToString(QLocale().language()); } } // Full format: Mozilla/5.0 (Linux d->useragent = QL1S("Mozilla/5.0 (") + supp + QL1S(") KHTML/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH) + QL1S(" (like Gecko) Konqueror/") + QString::number(KIO_VERSION_MAJOR) + QL1S(" KIO/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR); } else { QString appName = QCoreApplication::applicationName(); if (appName.isEmpty() || appName.startsWith(QLatin1String("kcmshell"), Qt::CaseInsensitive)) { appName = QStringLiteral("KDE"); } QString appVersion = QCoreApplication::applicationVersion(); if (appVersion.isEmpty()) { appVersion += QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH); } appName += QL1C('/') + appVersion; agentStr.replace(QL1S("%appversion%"), appName, Qt::CaseInsensitive); if (!QSslSocket::supportsSsl()) { agentStr.replace(QLatin1String("%security%"), QL1S("N"), Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%security%"), Qt::CaseInsensitive); } if (sysInfoFound) { // Platform (e.g. X11). It is no longer configurable from UI. agentStr.replace(QL1S("%platform%"), platform(), Qt::CaseInsensitive); // Operating system (e.g. Linux) if (modifiers.contains(QL1C('o'))) { agentStr.replace(QL1S("%osname%"), systemName, Qt::CaseInsensitive); // OS version (e.g. 2.6.36) if (modifiers.contains(QL1C('v'))) { agentStr.replace(QL1S("%osversion%"), systemVersion, Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%osversion%"), Qt::CaseInsensitive); } // Machine type (i686, x86-64, etc.) if (modifiers.contains(QL1C('m'))) { agentStr.replace(QL1S("%systype%"), machine, Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%systype%"), Qt::CaseInsensitive); } } else { agentStr.remove(QStringLiteral("%osname%"), Qt::CaseInsensitive); agentStr.remove(QStringLiteral("%osversion%"), Qt::CaseInsensitive); agentStr.remove(QStringLiteral("%systype%"), Qt::CaseInsensitive); } // Language (e.g. en_US) if (modifiers.contains(QL1C('l'))) { agentStr.replace(QL1S("%language%"), QLocale::languageToString(QLocale().language()), Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%language%"), Qt::CaseInsensitive); } // Clean up unnecessary separators that could be left over from the // possible keyword removal above... agentStr.replace(QRegExp(QL1S("[(]\\s*[;]\\s*")), QStringLiteral("(")); agentStr.replace(QRegExp(QL1S("[;]\\s*[;]\\s*")), QStringLiteral("; ")); agentStr.replace(QRegExp(QL1S("\\s*[;]\\s*[)]")), QStringLiteral(")")); } else { agentStr.remove(QStringLiteral("%osname%")); agentStr.remove(QStringLiteral("%osversion%")); agentStr.remove(QStringLiteral("%platform%")); agentStr.remove(QStringLiteral("%systype%")); agentStr.remove(QStringLiteral("%language%")); } d->useragent = agentStr.simplified(); } //qDebug() << "USERAGENT STRING:" << d->useragent; return d->useragent; } QString KProtocolManager::userAgentForApplication(const QString &appName, const QString &appVersion, const QStringList &extraInfo) { QString systemName, systemVersion, machine, info; if (getSystemNameVersionAndMachine(systemName, systemVersion, machine)) { info += systemName + QL1C('/') + systemVersion + QL1S("; "); } info += QL1S("KDE/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH); if (!machine.isEmpty()) { info += QL1S("; ") + machine; } info += QL1S("; ") + extraInfo.join(QStringLiteral("; ")); return (appName + QL1C('/') + appVersion + QStringLiteral(" (") + info + QL1C(')')); } bool KProtocolManager::getSystemNameVersionAndMachine( QString &systemName, QString &systemVersion, QString &machine) { #if defined(Q_OS_WIN) && !defined(_WIN32_WCE) // we do not use unameBuf.sysname information constructed in kdewin32 // because we want to get separate name and version systemName = QStringLiteral("Windows"); OSVERSIONINFOEX versioninfo; ZeroMemory(&versioninfo, sizeof(OSVERSIONINFOEX)); // try calling GetVersionEx using the OSVERSIONINFOEX, if that fails, try using the OSVERSIONINFO versioninfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); bool ok = GetVersionEx((OSVERSIONINFO *) &versioninfo); if (!ok) { versioninfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); ok = GetVersionEx((OSVERSIONINFO *) &versioninfo); } if (ok) { systemVersion = QString::number(versioninfo.dwMajorVersion); systemVersion += QL1C('.'); systemVersion += QString::number(versioninfo.dwMinorVersion); } #else struct utsname unameBuf; if (0 != uname(&unameBuf)) { return false; } systemName = QString::fromUtf8(unameBuf.sysname); systemVersion = QString::fromUtf8(unameBuf.release); machine = QString::fromUtf8(unameBuf.machine); #endif return true; } QString KProtocolManager::acceptLanguagesHeader() { const QLatin1String english("en"); // User's desktop language preference. QStringList languageList = QLocale().uiLanguages(); // Replace possible "C" in the language list with "en", unless "en" is // already pressent. This is to keep user's priorities in order. // If afterwards "en" is still not present, append it. int idx = languageList.indexOf(QStringLiteral("C")); if (idx != -1) { if (languageList.contains(english)) { languageList.removeAt(idx); } else { languageList[idx] = english; } } if (!languageList.contains(english)) { languageList += english; } // Some languages may have web codes different from locale codes, // read them from the config and insert in proper order. KConfig acclangConf(QStringLiteral("accept-languages.codes"), KConfig::NoGlobals); KConfigGroup replacementCodes(&acclangConf, "ReplacementCodes"); QStringList languageListFinal; Q_FOREACH (const QString &lang, languageList) { const QStringList langs = replacementCodes.readEntry(lang, QStringList()); if (langs.isEmpty()) { languageListFinal += lang; } else { languageListFinal += langs; } } // The header is composed of comma separated languages, with an optional // associated priority estimate (q=1..0) defaulting to 1. // As our language tags are already sorted by priority, we'll just decrease // the value evenly int prio = 10; QString header; Q_FOREACH (const QString &lang, languageListFinal) { header += lang; if (prio < 10) { header += QL1S(";q=0.") + QString::number(prio); } // do not add cosmetic whitespace in here : it is less compatible (#220677) header += QL1C(','); if (prio > 1) { --prio; } } header.chop(1); // Some of the languages may have country specifier delimited by // underscore, or modifier delimited by at-sign. // The header should use dashes instead. header.replace(QL1C('_'), QL1C('-')); header.replace(QL1C('@'), QL1C('-')); return header; } /*==================================== OTHERS ===============================*/ bool KProtocolManager::markPartial() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("MarkPartial", true); } int KProtocolManager::minimumKeepSize() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); // 5000 byte } bool KProtocolManager::autoResume() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("AutoResume", false); } bool KProtocolManager::persistentConnections() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("PersistentConnections", true); } bool KProtocolManager::persistentProxyConnection() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("PersistentProxyConnection", false); } QString KProtocolManager::proxyConfigScript() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group("Proxy Settings").readEntry("Proxy Config Script"); } /* =========================== PROTOCOL CAPABILITIES ============== */ static KProtocolInfoPrivate *findProtocol(const QUrl &url) { if (!url.isValid()) { return nullptr; } QString protocol = url.scheme(); if (!KProtocolInfo::proxiedBy(protocol).isEmpty()) { QString dummy; protocol = KProtocolManager::slaveProtocol(url, dummy); } return KProtocolInfoFactory::self()->findProtocol(protocol); } KProtocolInfo::Type KProtocolManager::inputType(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::T_NONE; } return prot->m_inputType; } KProtocolInfo::Type KProtocolManager::outputType(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::T_NONE; } return prot->m_outputType; } bool KProtocolManager::isSourceProtocol(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_isSourceProtocol; } bool KProtocolManager::supportsListing(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsListing; } QStringList KProtocolManager::listing(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return QStringList(); } return prot->m_listing; } bool KProtocolManager::supportsReading(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsReading; } bool KProtocolManager::supportsWriting(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsWriting; } bool KProtocolManager::supportsMakeDir(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsMakeDir; } bool KProtocolManager::supportsDeleting(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsDeleting; } bool KProtocolManager::supportsLinking(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsLinking; } bool KProtocolManager::supportsMoving(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsMoving; } bool KProtocolManager::supportsOpening(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsOpening; } bool KProtocolManager::canCopyFromFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canCopyFromFile; } bool KProtocolManager::canCopyToFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canCopyToFile; } bool KProtocolManager::canRenameFromFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canRenameFromFile; } bool KProtocolManager::canRenameToFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canRenameToFile; } bool KProtocolManager::canDeleteRecursive(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canDeleteRecursive; } KProtocolInfo::FileNameUsedForCopying KProtocolManager::fileNameUsedForCopying(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::FromUrl; } return prot->m_fileNameUsedForCopying; } QString KProtocolManager::defaultMimetype(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return QString(); } return prot->m_defaultMimetype; } QString KProtocolManager::protocolForArchiveMimetype(const QString &mimeType) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (d->protocolForArchiveMimetypes.isEmpty()) { const QList allProtocols = KProtocolInfoFactory::self()->allProtocols(); for (QList::const_iterator it = allProtocols.begin(); it != allProtocols.end(); ++it) { const QStringList archiveMimetypes = (*it)->m_archiveMimeTypes; Q_FOREACH (const QString &mime, archiveMimetypes) { d->protocolForArchiveMimetypes.insert(mime, (*it)->m_name); } } } return d->protocolForArchiveMimetypes.value(mimeType); } QString KProtocolManager::charsetFor(const QUrl &url) { return KIO::SlaveConfig::self()->configData(url.scheme(), url.host(), QStringLiteral("Charset")); } #undef PRIVATE_DATA #include "kprotocolmanager.moc" diff --git a/src/filewidgets/kfilemetapreview.cpp b/src/filewidgets/kfilemetapreview.cpp index 5e986ffa..0fa9389d 100644 --- a/src/filewidgets/kfilemetapreview.cpp +++ b/src/filewidgets/kfilemetapreview.cpp @@ -1,199 +1,199 @@ /* * This file is part of the KDE project. * Copyright (C) 2003 Carsten Pfeiffer * * You can Freely distribute this program under the GNU Library General Public * License. See the file "COPYING" for the exact licensing terms. */ #include "kfilemetapreview_p.h" #include #include #include #include #include #include #include bool KFileMetaPreview::s_tryAudioPreview = true; KFileMetaPreview::KFileMetaPreview(QWidget *parent) : KPreviewWidgetBase(parent), haveAudioPreview(false) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setMargin(0); m_stack = new QStackedWidget(this); layout->addWidget(m_stack); // ### // m_previewProviders.setAutoDelete( true ); initPreviewProviders(); } KFileMetaPreview::~KFileMetaPreview() { } void KFileMetaPreview::initPreviewProviders() { qDeleteAll(m_previewProviders); m_previewProviders.clear(); // hardcoded so far // image previews KImageFilePreview *imagePreview = new KImageFilePreview(m_stack); (void) m_stack->addWidget(imagePreview); m_stack->setCurrentWidget(imagePreview); resize(imagePreview->sizeHint()); const QStringList mimeTypes = imagePreview->supportedMimeTypes(); QStringList::ConstIterator it = mimeTypes.begin(); for (; it != mimeTypes.end(); ++it) { // qDebug(".... %s", (*it).toLatin1().constData()); m_previewProviders.insert(*it, imagePreview); } } KPreviewWidgetBase *KFileMetaPreview::findExistingProvider(const QString &mimeType, const QMimeType &mimeInfo) const { KPreviewWidgetBase *provider = m_previewProviders.value(mimeType); if (provider) { return provider; } if (mimeInfo.isValid()) { // check mime type inheritance const QStringList parentMimeTypes = mimeInfo.allAncestors(); Q_FOREACH (const QString &parentMimeType, parentMimeTypes) { provider = m_previewProviders.value(parentMimeType); if (provider) { return provider; } } } // ### mimetype may be image/* for example, try that const int index = mimeType.indexOf(QLatin1Char('/')); if (index > 0) { - provider = m_previewProviders.value(mimeType.left(index + 1) + QLatin1Char('*')); + provider = m_previewProviders.value(mimeType.leftRef(index + 1) + QLatin1Char('*')); if (provider) { return provider; } } return nullptr; } KPreviewWidgetBase *KFileMetaPreview::previewProviderFor(const QString &mimeType) { QMimeDatabase db; QMimeType mimeInfo = db.mimeTypeForName(mimeType); // qDebug("### looking for: %s", mimeType.toLatin1().constData()); // often the first highlighted item, where we can be sure, there is no plugin // (this "folders reflect icons" is a konq-specific thing, right?) if (mimeInfo.inherits(QStringLiteral("inode/directory"))) { return nullptr; } KPreviewWidgetBase *provider = findExistingProvider(mimeType, mimeInfo); if (provider) { return provider; } //qDebug("#### didn't find anything for: %s", mimeType.toLatin1().constData()); if (s_tryAudioPreview && !mimeType.startsWith(QLatin1String("text/")) && !mimeType.startsWith(QLatin1String("image/"))) { if (!haveAudioPreview) { KPreviewWidgetBase *audioPreview = createAudioPreview(m_stack); if (audioPreview) { haveAudioPreview = true; (void) m_stack->addWidget(audioPreview); const QStringList mimeTypes = audioPreview->supportedMimeTypes(); QStringList::ConstIterator it = mimeTypes.begin(); for (; it != mimeTypes.end(); ++it) { // only add non already handled mimetypes if (m_previewProviders.constFind(*it) == m_previewProviders.constEnd()) { m_previewProviders.insert(*it, audioPreview); } } } } } // with the new mimetypes from the audio-preview, try again provider = findExistingProvider(mimeType, mimeInfo); if (provider) { return provider; } // The logic in this code duplicates the logic in PreviewJob. // But why do we need multiple KPreviewWidgetBase instances anyway? return nullptr; } void KFileMetaPreview::showPreview(const QUrl &url) { QMimeDatabase db; QMimeType mt = db.mimeTypeForUrl(url); KPreviewWidgetBase *provider = previewProviderFor(mt.name()); if (provider) { if (provider != m_stack->currentWidget()) { // stop the previous preview clearPreview(); } m_stack->setEnabled(true); m_stack->setCurrentWidget(provider); provider->showPreview(url); } else { clearPreview(); m_stack->setEnabled(false); } } void KFileMetaPreview::clearPreview() { if (m_stack->currentWidget()) { static_cast(m_stack->currentWidget())->clearPreview(); } } void KFileMetaPreview::addPreviewProvider(const QString &mimeType, KPreviewWidgetBase *provider) { m_previewProviders.insert(mimeType, provider); } void KFileMetaPreview::clearPreviewProviders() { QHash::const_iterator i = m_previewProviders.constBegin(); while (i != m_previewProviders.constEnd()) { m_stack->removeWidget(i.value()); ++i; } qDeleteAll(m_previewProviders); m_previewProviders.clear(); } // static KPreviewWidgetBase *KFileMetaPreview::createAudioPreview(QWidget *parent) { KPluginLoader loader(QStringLiteral("kfileaudiopreview")); KPluginFactory *factory = loader.factory(); if (!factory) { qWarning() << "Couldn't load kfileaudiopreview" << loader.errorString(); s_tryAudioPreview = false; return nullptr; } KPreviewWidgetBase *w = factory->create(parent); if (w) { w->setObjectName(QStringLiteral("kfileaudiopreview")); } return w; } diff --git a/src/filewidgets/kfilepreviewgenerator.cpp b/src/filewidgets/kfilepreviewgenerator.cpp index 67b41d09..f6fc4619 100644 --- a/src/filewidgets/kfilepreviewgenerator.cpp +++ b/src/filewidgets/kfilepreviewgenerator.cpp @@ -1,1282 +1,1281 @@ /******************************************************************************* * Copyright (C) 2008-2009 by Peter Penz * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library 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 * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "kfilepreviewgenerator.h" #include "defaultviewadapter_p.h" #include // from kiowidgets #include // for HAVE_XRENDER #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 && HAVE_XRENDER # include # include # include #endif /** * If the passed item view is an instance of QListView, expensive * layout operations are blocked in the constructor and are unblocked * again in the destructor. * * This helper class is a workaround for the following huge performance * problem when having directories with several 1000 items: * - each change of an icon emits a dataChanged() signal from the model * - QListView iterates through all items on each dataChanged() signal * and invokes QItemDelegate::sizeHint() * - the sizeHint() implementation of KFileItemDelegate is quite complex, * invoking it 1000 times for each icon change might block the UI * * QListView does not invoke QItemDelegate::sizeHint() when the * uniformItemSize property has been set to true, so this property is * set before exchanging a block of icons. */ class KFilePreviewGenerator::LayoutBlocker { public: LayoutBlocker(QAbstractItemView *view) : m_uniformSizes(false), m_view(qobject_cast(view)) { if (m_view != nullptr) { m_uniformSizes = m_view->uniformItemSizes(); m_view->setUniformItemSizes(true); } } ~LayoutBlocker() { if (m_view != nullptr) { m_view->setUniformItemSizes(m_uniformSizes); /* The QListView did the layout with uniform item * sizes, so trigger a relayout with the expected sizes. */ if (!m_uniformSizes) { m_view->setGridSize(m_view->gridSize()); } } } private: bool m_uniformSizes; QListView *m_view; }; /** Helper class for drawing frames for image previews. */ class KFilePreviewGenerator::TileSet { public: enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 }; enum Tile { TopLeftCorner = 0, TopSide, TopRightCorner, LeftSide, RightSide, BottomLeftCorner, BottomSide, BottomRightCorner, NumTiles }; TileSet() { QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied); QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(image.rect(), Qt::transparent); p.fillRect(image.rect().adjusted(3, 3, -3, -3), Qt::black); p.end(); KIO::ImageFilter::shadowBlur(image, 3, Qt::black); QPixmap pixmap = QPixmap::fromImage(image); m_tiles[TopLeftCorner] = pixmap.copy(0, 0, 8, 8); m_tiles[TopSide] = pixmap.copy(8, 0, 8, 8); m_tiles[TopRightCorner] = pixmap.copy(16, 0, 8, 8); m_tiles[LeftSide] = pixmap.copy(0, 8, 8, 8); m_tiles[RightSide] = pixmap.copy(16, 8, 8, 8); m_tiles[BottomLeftCorner] = pixmap.copy(0, 16, 8, 8); m_tiles[BottomSide] = pixmap.copy(8, 16, 8, 8); m_tiles[BottomRightCorner] = pixmap.copy(16, 16, 8, 8); } void paint(QPainter *p, const QRect &r) { p->drawPixmap(r.topLeft(), m_tiles[TopLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.y(), r.width() - 16, 8, m_tiles[TopSide]); } p->drawPixmap(r.right() - 8 + 1, r.y(), m_tiles[TopRightCorner]); if (r.height() - 16 > 0) { p->drawTiledPixmap(r.x(), r.y() + 8, 8, r.height() - 16, m_tiles[LeftSide]); p->drawTiledPixmap(r.right() - 8 + 1, r.y() + 8, 8, r.height() - 16, m_tiles[RightSide]); } p->drawPixmap(r.x(), r.bottom() - 8 + 1, m_tiles[BottomLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.bottom() - 8 + 1, r.width() - 16, 8, m_tiles[BottomSide]); } p->drawPixmap(r.right() - 8 + 1, r.bottom() - 8 + 1, m_tiles[BottomRightCorner]); const QRect contentRect = r.adjusted(LeftMargin + 1, TopMargin + 1, -(RightMargin + 1), -(BottomMargin + 1)); p->fillRect(contentRect, Qt::transparent); } private: QPixmap m_tiles[NumTiles]; }; class Q_DECL_HIDDEN KFilePreviewGenerator::Private { public: Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model); ~Private(); /** * Requests a new icon for the item \a index. * @param sequenceIndex If this is zero, the standard icon is requested, else another one. */ void requestSequenceIcon(const QModelIndex &index, int sequenceIndex); /** * Generates previews for the items \a items asynchronously. */ void updateIcons(const KFileItemList &items); /** * Generates previews for the indices within \a topLeft * and \a bottomRight asynchronously. */ void updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight); /** * Adds the preview \a pixmap for the item \a item to the preview * queue and starts a timer which will dispatch the preview queue * later. */ void addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap); /** * Is invoked when the preview job has been finished and * removes the job from the m_previewJobs list. */ void slotPreviewJobFinished(KJob *job); /** Synchronizes the icon of all items with the clipboard of cut items. */ void updateCutItems(); /** * Reset all icons of the items from m_cutItemsCache and clear * the cache. */ void clearCutItemsCache(); /** * Dispatches the preview queue block by block within * time slices. */ void dispatchIconUpdateQueue(); /** * Pauses all icon updates and invokes KFilePreviewGenerator::resumeIconUpdates() * after a short delay. Is invoked as soon as the user has moved * a scrollbar. */ void pauseIconUpdates(); /** * Resumes the icons updates that have been paused after moving the * scrollbar. The previews for the current visible area are * generated first. */ void resumeIconUpdates(); /** * Starts the resolving of the MIME types from * the m_pendingItems queue. */ void startMimeTypeResolving(); /** * Resolves the MIME type for exactly one item of the * m_pendingItems queue. */ void resolveMimeType(); /** * Returns true, if the item \a item has been cut into * the clipboard. */ bool isCutItem(const KFileItem &item) const; /** * Applies a cut-item effect to all given \a items, if they * are marked as cut in the clipboard. */ void applyCutItemEffect(const KFileItemList &items); /** * Applies a frame around the icon. False is returned if * no frame has been added because the icon is too small. */ bool applyImageFrame(QPixmap &icon); /** * Resizes the icon to \a maxSize if the icon size does not * fit into the maximum size. The aspect ratio of the icon * is kept. */ void limitToSize(QPixmap &icon, const QSize &maxSize); /** * Creates previews by starting new preview jobs for the items * and triggers the preview timer. */ void createPreviews(const KFileItemList &items); /** * Helper method for createPreviews(): Starts a preview job for the given * items. For each returned preview addToPreviewQueue() will get invoked. */ void startPreviewJob(const KFileItemList &items, int width, int height); /** Kills all ongoing preview jobs. */ void killPreviewJobs(); /** * Orders the items \a items in a way that the visible items * are moved to the front of the list. When passing this * list to a preview job, the visible items will get generated * first. */ void orderItems(KFileItemList &items); /** * Helper method for KFilePreviewGenerator::updateIcons(). Adds * recursively all items from the model to the list \a list. */ void addItemsToList(const QModelIndex &index, KFileItemList &list); /** * Updates the icons of files that are constantly changed due to a copy * operation. See m_changedItems and m_changedItemsTimer for details. */ void delayedIconUpdate(); /** * Any items that are removed from the model are also removed from m_changedItems. */ void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); /** Remembers the pixmap for an item specified by an URL. */ struct ItemInfo { QUrl url; QPixmap pixmap; }; /** * During the lifetime of a DataChangeObtainer instance changing * the data of the model won't trigger generating a preview. */ class DataChangeObtainer { public: DataChangeObtainer(KFilePreviewGenerator::Private *generator) : m_gen(generator) { ++m_gen->m_internalDataChange; } ~DataChangeObtainer() { --m_gen->m_internalDataChange; } private: KFilePreviewGenerator::Private *m_gen; }; bool m_previewShown; /** * True, if m_pendingItems and m_dispatchedItems should be * cleared when the preview jobs have been finished. */ bool m_clearItemQueues; /** * True if a selection has been done which should cut items. */ bool m_hasCutSelection; /** * True if the updates of icons has been paused by pauseIconUpdates(). * The value is reset by resumeIconUpdates(). */ bool m_iconUpdatesPaused; /** * If the value is 0, the slot * updateIcons(const QModelIndex&, const QModelIndex&) has * been triggered by an external data change. */ int m_internalDataChange; int m_pendingVisibleIconUpdates; KAbstractViewAdapter *m_viewAdapter; QAbstractItemView *m_itemView; QTimer *m_iconUpdateTimer; QTimer *m_scrollAreaTimer; QList m_previewJobs; QPointer m_dirModel; QAbstractProxyModel *m_proxyModel; /** * Set of all items that already have the 'cut' effect applied, together with the pixmap it was applied to * This is used to make sure that the 'cut' effect is applied max. once for each pixmap * * Referencing the pixmaps here imposes no overhead, as they were also given to KDirModel::setData(), * and thus are held anyway. */ QHash m_cutItemsCache; QList m_previews; QMap m_sequenceIndices; /** * When huge items are copied, it must be prevented that a preview gets generated * for each item size change. m_changedItems keeps track of the changed items and it * is assured that a final preview is only done if an item does not change within * at least 5 seconds. */ QHash m_changedItems; QTimer *m_changedItemsTimer; /** * Contains all items where a preview must be generated, but * where the preview job has not dispatched the items yet. */ KFileItemList m_pendingItems; /** * Contains all items, where a preview has already been * generated by the preview jobs. */ KFileItemList m_dispatchedItems; KFileItemList m_resolvedMimeTypes; QStringList m_enabledPlugins; TileSet *m_tileSet; private: KFilePreviewGenerator *const q; }; KFilePreviewGenerator::Private::Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model) : m_previewShown(true), m_clearItemQueues(true), m_hasCutSelection(false), m_iconUpdatesPaused(false), m_internalDataChange(0), m_pendingVisibleIconUpdates(0), m_viewAdapter(viewAdapter), m_itemView(nullptr), m_iconUpdateTimer(nullptr), m_scrollAreaTimer(nullptr), m_previewJobs(), m_proxyModel(nullptr), m_cutItemsCache(), m_previews(), m_sequenceIndices(), m_changedItems(), m_changedItemsTimer(nullptr), m_pendingItems(), m_dispatchedItems(), m_resolvedMimeTypes(), m_enabledPlugins(), m_tileSet(nullptr), q(parent) { if (!m_viewAdapter->iconSize().isValid()) { m_previewShown = false; } m_proxyModel = qobject_cast(model); m_dirModel = (m_proxyModel == nullptr) ? qobject_cast(model) : qobject_cast(m_proxyModel->sourceModel()); if (!m_dirModel) { // previews can only get generated for directory models m_previewShown = false; } else { KDirModel *dirModel = m_dirModel.data(); connect(dirModel->dirLister(), SIGNAL(newItems(KFileItemList)), q, SLOT(updateIcons(KFileItemList))); connect(dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), q, SLOT(updateIcons(QModelIndex,QModelIndex))); connect(dirModel, SIGNAL(needSequenceIcon(QModelIndex,int)), q, SLOT(requestSequenceIcon(QModelIndex,int))); connect(dirModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), q, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); } QClipboard *clipboard = QApplication::clipboard(); connect(clipboard, SIGNAL(dataChanged()), q, SLOT(updateCutItems())); m_iconUpdateTimer = new QTimer(q); m_iconUpdateTimer->setSingleShot(true); m_iconUpdateTimer->setInterval(200); connect(m_iconUpdateTimer, SIGNAL(timeout()), q, SLOT(dispatchIconUpdateQueue())); // Whenever the scrollbar values have been changed, the pending previews should // be reordered in a way that the previews for the visible items are generated // first. The reordering is done with a small delay, so that during moving the // scrollbars the CPU load is kept low. m_scrollAreaTimer = new QTimer(q); m_scrollAreaTimer->setSingleShot(true); m_scrollAreaTimer->setInterval(200); connect(m_scrollAreaTimer, SIGNAL(timeout()), q, SLOT(resumeIconUpdates())); m_viewAdapter->connect(KAbstractViewAdapter::IconSizeChanged, q, SLOT(updateIcons())); m_viewAdapter->connect(KAbstractViewAdapter::ScrollBarValueChanged, q, SLOT(pauseIconUpdates())); m_changedItemsTimer = new QTimer(q); m_changedItemsTimer->setSingleShot(true); m_changedItemsTimer->setInterval(5000); connect(m_changedItemsTimer, SIGNAL(timeout()), q, SLOT(delayedIconUpdate())); KConfigGroup globalConfig(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), "PreviewSettings"); m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList() << QStringLiteral("directorythumbnail") << QStringLiteral("imagethumbnail") << QStringLiteral("jpegthumbnail")); // Compatibility update: in 4.7, jpegrotatedthumbnail was merged into (or // replaced with?) jpegthumbnail if (m_enabledPlugins.contains(QStringLiteral("jpegrotatedthumbnail"))) { m_enabledPlugins.removeAll(QStringLiteral("jpegrotatedthumbnail")); m_enabledPlugins.append(QStringLiteral("jpegthumbnail")); globalConfig.writeEntry("Plugins", m_enabledPlugins); globalConfig.sync(); } } KFilePreviewGenerator::Private::~Private() { killPreviewJobs(); m_pendingItems.clear(); m_dispatchedItems.clear(); delete m_tileSet; } void KFilePreviewGenerator::Private::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) { if (m_pendingItems.isEmpty() || (sequenceIndex == 0)) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItem item = dirModel->itemForIndex(index); if (sequenceIndex == 0) { m_sequenceIndices.remove(item.url()); } else { m_sequenceIndices.insert(item.url(), sequenceIndex); } ///@todo Update directly, without using m_sequenceIndices updateIcons(KFileItemList() << item); } } void KFilePreviewGenerator::Private::updateIcons(const KFileItemList &items) { if (items.isEmpty()) { return; } applyCutItemEffect(items); KFileItemList orderedItems = items; orderItems(orderedItems); foreach (const KFileItem &item, orderedItems) { m_pendingItems.append(item); } if (m_previewShown) { createPreviews(orderedItems); } else { startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (m_internalDataChange > 0) { // QAbstractItemModel::setData() has been invoked internally by the KFilePreviewGenerator. // The signal dataChanged() is connected with this method, but previews only need // to be generated when an external data change has occurred. return; } // dataChanged emitted for the root dir (e.g. permission changes) if (!topLeft.isValid() || !bottomRight.isValid()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItemList itemList; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { const QModelIndex index = dirModel->index(row, 0); if (!index.isValid()) { continue; } const KFileItem item = dirModel->itemForIndex(index); Q_ASSERT(!item.isNull()); if (m_previewShown) { const QUrl url = item.url(); const bool hasChanged = m_changedItems.contains(url); // O(1) m_changedItems.insert(url, hasChanged); if (!hasChanged) { // only update the icon if it has not been already updated within // the last 5 seconds (the other icons will be updated later with // the help of m_changedItemsTimer) itemList.append(item); } } else { itemList.append(item); } } updateIcons(itemList); m_changedItemsTimer->start(); } void KFilePreviewGenerator::Private::addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap) { KIO::PreviewJob *senderJob = qobject_cast(q->sender()); Q_ASSERT(senderJob != nullptr); if (senderJob != nullptr) { QMap::iterator it = m_sequenceIndices.find(item.url()); if (senderJob->sequenceIndex() && (it == m_sequenceIndices.end() || *it != senderJob->sequenceIndex())) { return; // the sequence index does not match the one we want } if (!senderJob->sequenceIndex() && it != m_sequenceIndices.end()) { return; // the sequence index does not match the one we want } m_sequenceIndices.erase(it); } if (!m_previewShown) { // the preview has been canceled in the meantime return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // check whether the item is part of the directory lister (it is possible // that a preview from an old directory lister is received) bool isOldPreview = true; const QUrl itemParentDir = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); foreach (const QUrl &dir, dirModel->dirLister()->directories()) { if (dir == itemParentDir || dir.path().isEmpty()) { isOldPreview = false; break; } } if (isOldPreview) { return; } QPixmap icon = pixmap; const QString mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); - const QString mimeTypeGroup = mimeType.left(slashIndex); + const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if ((mimeTypeGroup != QLatin1String("image")) || !applyImageFrame(icon)) { limitToSize(icon, m_viewAdapter->iconSize()); } if (m_hasCutSelection && isCutItem(item)) { // apply the disabled effect to the icon for marking it as "cut item" // and apply the icon to the item KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); icon = iconEffect->apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); } KIconLoader::global()->drawOverlays(item.overlays(), icon, KIconLoader::Desktop); // remember the preview and URL, so that it can be applied to the model // in KFilePreviewGenerator::dispatchIconUpdateQueue() ItemInfo preview; preview.url = item.url(); preview.pixmap = icon; m_previews.append(preview); m_pendingItems.removeOne(item); m_dispatchedItems.append(item); } void KFilePreviewGenerator::Private::slotPreviewJobFinished(KJob *job) { const int index = m_previewJobs.indexOf(job); m_previewJobs.removeAt(index); if (m_previewJobs.isEmpty()) { foreach (const KFileItem &item, m_pendingItems) { if (item.isMimeTypeKnown()) { m_resolvedMimeTypes.append(item); } } if (m_clearItemQueues) { m_pendingItems.clear(); m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; QMetaObject::invokeMethod(q, "dispatchIconUpdateQueue", Qt::QueuedConnection); } m_sequenceIndices.clear(); // just to be sure that we don't leak anything } } void KFilePreviewGenerator::Private::updateCutItems() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); clearCutItemsCache(); KFileItemList items; KDirLister *dirLister = dirModel->dirLister(); const QList dirs = dirLister->directories(); foreach (const QUrl &url, dirs) { items << dirLister->itemsForDir(url); } applyCutItemEffect(items); } void KFilePreviewGenerator::Private::clearCutItemsCache() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); KFileItemList previews; // Reset the icons of all items that are stored in the cache // to use their default MIME type icon. foreach (const QUrl &url, m_cutItemsCache.keys()) { const QModelIndex index = dirModel->indexForUrl(url); if (index.isValid()) { dirModel->setData(index, QIcon(), Qt::DecorationRole); if (m_previewShown) { previews.append(dirModel->itemForIndex(index)); } } } m_cutItemsCache.clear(); if (previews.size() > 0) { // assure that the previews gets restored Q_ASSERT(m_previewShown); orderItems(previews); updateIcons(previews); } } void KFilePreviewGenerator::Private::dispatchIconUpdateQueue() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int count = m_previews.count() + m_resolvedMimeTypes.count(); if (count > 0) { LayoutBlocker blocker(m_itemView); DataChangeObtainer obt(this); if (m_previewShown) { // dispatch preview queue foreach (const ItemInfo &preview, m_previews) { const QModelIndex idx = dirModel->indexForUrl(preview.url); if (idx.isValid() && (idx.column() == 0)) { dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); } } m_previews.clear(); } // dispatch mime type queue foreach (const KFileItem &item, m_resolvedMimeTypes) { const QModelIndex idx = dirModel->indexForItem(item); dirModel->itemChanged(idx); } m_resolvedMimeTypes.clear(); m_pendingVisibleIconUpdates -= count; if (m_pendingVisibleIconUpdates < 0) { m_pendingVisibleIconUpdates = 0; } } if (m_pendingVisibleIconUpdates > 0) { // As long as there are pending previews for visible items, poll // the preview queue periodically. If there are no pending previews, // the queue is dispatched in slotPreviewJobFinished(). m_iconUpdateTimer->start(); } } void KFilePreviewGenerator::Private::pauseIconUpdates() { m_iconUpdatesPaused = true; foreach (KJob *job, m_previewJobs) { Q_ASSERT(job != nullptr); job->suspend(); } m_scrollAreaTimer->start(); } void KFilePreviewGenerator::Private::resumeIconUpdates() { m_iconUpdatesPaused = false; // Before creating new preview jobs the m_pendingItems queue must be // cleaned up by removing the already dispatched items. Implementation // note: The order of the m_dispatchedItems queue and the m_pendingItems // queue is usually equal. So even when having a lot of elements the // nested loop is no performance bottle neck, as the inner loop is only // entered once in most cases. foreach (const KFileItem &item, m_dispatchedItems) { KFileItemList::iterator begin = m_pendingItems.begin(); KFileItemList::iterator end = m_pendingItems.end(); for (KFileItemList::iterator it = begin; it != end; ++it) { if ((*it).url() == item.url()) { m_pendingItems.erase(it); break; } } } m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; dispatchIconUpdateQueue(); if (m_previewShown) { KFileItemList orderedItems = m_pendingItems; orderItems(orderedItems); // Kill all suspended preview jobs. Usually when a preview job // has been finished, slotPreviewJobFinished() clears all item queues. // This is not wanted in this case, as a new job is created afterwards // for m_pendingItems. m_clearItemQueues = false; killPreviewJobs(); m_clearItemQueues = true; createPreviews(orderedItems); } else { orderItems(m_pendingItems); startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::startMimeTypeResolving() { resolveMimeType(); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::resolveMimeType() { if (m_pendingItems.isEmpty()) { return; } // resolve at least one MIME type bool resolved = false; do { KFileItem item = m_pendingItems.takeFirst(); if (item.isMimeTypeKnown()) { if (m_pendingVisibleIconUpdates > 0) { // The item is visible and the MIME type already known. // Decrease the update counter for dispatchIconUpdateQueue(): --m_pendingVisibleIconUpdates; } } else { // The MIME type is unknown and must get resolved. The // directory model is not informed yet, as a single update // would be very expensive. Instead the item is remembered in // m_resolvedMimeTypes and will be dispatched later // by dispatchIconUpdateQueue(). item.determineMimeType(); m_resolvedMimeTypes.append(item); resolved = true; } } while (!resolved && !m_pendingItems.isEmpty()); if (m_pendingItems.isEmpty()) { // All MIME types have been resolved now. Assure // that the directory model gets informed about // this, so that an update of the icons is done. dispatchIconUpdateQueue(); } else if (!m_iconUpdatesPaused) { // assure that the MIME type of the next // item will be resolved asynchronously QMetaObject::invokeMethod(q, "resolveMimeType", Qt::QueuedConnection); } } bool KFilePreviewGenerator::Private::isCutItem(const KFileItem &item) const { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); const QList cutUrls = KUrlMimeData::urlsFromMimeData(mimeData); return cutUrls.contains(item.url()); } void KFilePreviewGenerator::Private::applyCutItemEffect(const KFileItemList &items) { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); if (!m_hasCutSelection) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const QSet cutUrls = KUrlMimeData::urlsFromMimeData(mimeData).toSet(); DataChangeObtainer obt(this); KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); foreach (const KFileItem &item, items) { if (cutUrls.contains(item.url())) { const QModelIndex index = dirModel->indexForItem(item); const QVariant value = dirModel->data(index, Qt::DecorationRole); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); const QSize actualSize = icon.actualSize(m_viewAdapter->iconSize()); QPixmap pixmap = icon.pixmap(actualSize); const QHash::const_iterator cacheIt = m_cutItemsCache.constFind(item.url()); if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) { pixmap = iconEffect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); m_cutItemsCache.insert(item.url(), pixmap); } } } } } bool KFilePreviewGenerator::Private::applyImageFrame(QPixmap &icon) { const QSize maxSize = m_viewAdapter->iconSize(); const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha(); if (!applyFrame) { // the maximum size or the image itself is too small for a frame return false; } // resize the icon to the maximum size minus the space required for the frame const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin); limitToSize(icon, size); if (m_tileSet == nullptr) { m_tileSet = new TileSet(); } QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); framedIcon.fill(Qt::transparent); QPainter painter; painter.begin(&framedIcon); painter.setCompositionMode(QPainter::CompositionMode_Source); m_tileSet->paint(&painter, framedIcon.rect()); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); painter.end(); icon = framedIcon; return true; } void KFilePreviewGenerator::Private::limitToSize(QPixmap &icon, const QSize &maxSize) { if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { #pragma message("Cannot use XRender with QPixmap anymore. Find equivalent with Qt API.") #if 0 // HAVE_X11 && HAVE_XRENDER // Assume that the texture size limit is 2048x2048 if ((icon.width() <= 2048) && (icon.height() <= 2048) && icon.x11PictureHandle()) { QSize size = icon.size(); size.scale(maxSize, Qt::KeepAspectRatio); const qreal factor = size.width() / qreal(icon.width()); XTransform xform = {{ { XDoubleToFixed(1 / factor), 0, 0 }, { 0, XDoubleToFixed(1 / factor), 0 }, { 0, 0, XDoubleToFixed(1) } } }; QPixmap pixmap(size); pixmap.fill(Qt::transparent); Display *dpy = QX11Info::display(); XRenderPictureAttributes attr; attr.repeat = RepeatPad; XRenderChangePicture(dpy, icon.x11PictureHandle(), CPRepeat, &attr); XRenderSetPictureFilter(dpy, icon.x11PictureHandle(), FilterBilinear, 0, 0); XRenderSetPictureTransform(dpy, icon.x11PictureHandle(), &xform); XRenderComposite(dpy, PictOpOver, icon.x11PictureHandle(), None, pixmap.x11PictureHandle(), 0, 0, 0, 0, 0, 0, pixmap.width(), pixmap.height()); icon = pixmap; } else { icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::FastTransformation); } #else icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); #endif } } void KFilePreviewGenerator::Private::createPreviews(const KFileItemList &items) { if (items.count() == 0) { return; } const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); // PreviewJob internally caches items always with the size of // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must // do a downscaling anyhow because of the frame, so in this case only the provided // cache sizes are requested. KFileItemList imageItems; KFileItemList otherItems; QString mimeType; - QString mimeTypeGroup; foreach (const KFileItem &item, items) { mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); - mimeTypeGroup = mimeType.left(slashIndex); + const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if (mimeTypeGroup == QLatin1String("image")) { imageItems.append(item); } else { otherItems.append(item); } } const QSize size = m_viewAdapter->iconSize(); startPreviewJob(otherItems, size.width(), size.height()); const int cacheSize = (size.width() > 128) || (size.height() > 128) ? 256 : 128; startPreviewJob(imageItems, cacheSize, cacheSize); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::startPreviewJob(const KFileItemList &items, int width, int height) { if (items.count() > 0) { KIO::PreviewJob *job = KIO::filePreview(items, QSize(width, height), &m_enabledPlugins); // Set the sequence index to the target. We only need to check if items.count() == 1, // because requestSequenceIcon(..) creates exactly such a request. if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) { QMap::iterator it = m_sequenceIndices.find(items[0].url()); if (it != m_sequenceIndices.end()) { job->setSequenceIndex(*it); } } connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), q, SLOT(addToPreviewQueue(KFileItem,QPixmap))); connect(job, SIGNAL(finished(KJob*)), q, SLOT(slotPreviewJobFinished(KJob*))); m_previewJobs.append(job); } } void KFilePreviewGenerator::Private::killPreviewJobs() { foreach (KJob *job, m_previewJobs) { Q_ASSERT(job != nullptr); job->kill(); } m_previewJobs.clear(); m_sequenceIndices.clear(); m_iconUpdateTimer->stop(); m_scrollAreaTimer->stop(); m_changedItemsTimer->stop(); } void KFilePreviewGenerator::Private::orderItems(KFileItemList &items) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Order the items in a way that the preview for the visible items // is generated first, as this improves the feeled performance a lot. const bool hasProxy = (m_proxyModel != nullptr); const int itemCount = items.count(); const QRect visibleArea = m_viewAdapter->visibleArea(); QModelIndex dirIndex; QRect itemRect; int insertPos = 0; for (int i = 0; i < itemCount; ++i) { dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows) if (hasProxy) { const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); itemRect = m_viewAdapter->visualRect(proxyIndex); } else { itemRect = m_viewAdapter->visualRect(dirIndex); } if (itemRect.intersects(visibleArea)) { // The current item is (at least partly) visible. Move it // to the front of the list, so that the preview is // generated earlier. items.insert(insertPos, items.at(i)); items.removeAt(i + 1); ++insertPos; ++m_pendingVisibleIconUpdates; } } } void KFilePreviewGenerator::Private::addItemsToList(const QModelIndex &index, KFileItemList &list) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int rowCount = dirModel->rowCount(index); for (int row = 0; row < rowCount; ++row) { const QModelIndex subIndex = dirModel->index(row, 0, index); KFileItem item = dirModel->itemForIndex(subIndex); list.append(item); if (dirModel->rowCount(subIndex) > 0) { // the model is hierarchical (treeview) addItemsToList(subIndex, list); } } } void KFilePreviewGenerator::Private::delayedIconUpdate() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Precondition: No items have been changed within the last // 5 seconds. This means that items that have been changed constantly // due to a copy operation should be updated now. KFileItemList itemList; QHash::const_iterator it = m_changedItems.constBegin(); while (it != m_changedItems.constEnd()) { const bool hasChanged = it.value(); if (hasChanged) { const QModelIndex index = dirModel->indexForUrl(it.key()); const KFileItem item = dirModel->itemForIndex(index); itemList.append(item); } ++it; } m_changedItems.clear(); updateIcons(itemList); } void KFilePreviewGenerator::Private::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { if (m_changedItems.isEmpty()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } for (int row = start; row <= end; row++) { const QModelIndex index = dirModel->index(row, 0, parent); const KFileItem item = dirModel->itemForIndex(index); if (!item.isNull()) { m_changedItems.remove(item.url()); } if (dirModel->hasChildren(index)) { rowsAboutToBeRemoved(index, 0, dirModel->rowCount(index) - 1); } } } KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent) : QObject(parent), d(new Private(this, new KIO::DefaultViewAdapter(parent, this), parent->model())) { d->m_itemView = parent; } KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model) : QObject(parent), d(new Private(this, parent, model)) { } KFilePreviewGenerator::~KFilePreviewGenerator() { delete d; } void KFilePreviewGenerator::setPreviewShown(bool show) { if (d->m_previewShown == show) { return; } KDirModel *dirModel = d->m_dirModel.data(); if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) { // The view must provide an icon size and a directory model, // otherwise the showing the previews will get ignored return; } d->m_previewShown = show; if (!show) { dirModel->clearAllPreviews(); } updateIcons(); } bool KFilePreviewGenerator::isPreviewShown() const { return d->m_previewShown; } // deprecated (use updateIcons() instead) void KFilePreviewGenerator::updatePreviews() { updateIcons(); } void KFilePreviewGenerator::updateIcons() { d->killPreviewJobs(); d->clearCutItemsCache(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); KFileItemList itemList; d->addItemsToList(QModelIndex(), itemList); d->updateIcons(itemList); } void KFilePreviewGenerator::cancelPreviews() { d->killPreviewJobs(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); updateIcons(); } void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins) { d->m_enabledPlugins = plugins; } QStringList KFilePreviewGenerator::enabledPlugins() const { return d->m_enabledPlugins; } #include "moc_kfilepreviewgenerator.cpp" diff --git a/src/filewidgets/kfilewidget.cpp b/src/filewidgets/kfilewidget.cpp index 5ea33efd..6fae6e77 100644 --- a/src/filewidgets/kfilewidget.cpp +++ b/src/filewidgets/kfilewidget.cpp @@ -1,2885 +1,2885 @@ // -*- c++ -*- /* This file is part of the KDE libraries Copyright (C) 1997, 1998 Richard Moore 1998 Stephan Kulow 1998 Daniel Grana 1999,2000,2001,2002,2003 Carsten Pfeiffer 2003 Clarence Dang 2007 David Faure 2008 Rafael Fernández López This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfilewidget.h" #include "../pathhelpers_p.h" #include "kfileplacesview.h" #include "kfileplacesmodel.h" #include "kfilebookmarkhandler_p.h" #include "kurlcombobox.h" #include "kurlnavigator.h" #include "kfilepreviewgenerator.h" #include "kfilewidgetdocktitlebar_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KFileWidgetPrivate { public: explicit KFileWidgetPrivate(KFileWidget *widget) : q(widget), boxLayout(nullptr), placesDock(nullptr), placesView(nullptr), placesViewSplitter(nullptr), placesViewWidth(-1), labeledCustomWidget(nullptr), bottomCustomWidget(nullptr), autoSelectExtCheckBox(nullptr), operationMode(KFileWidget::Opening), bookmarkHandler(nullptr), toolbar(nullptr), locationEdit(nullptr), ops(nullptr), filterWidget(nullptr), autoSelectExtChecked(false), keepLocation(false), hasView(false), hasDefaultFilter(false), inAccept(false), dummyAdded(false), confirmOverwrite(false), differentHierarchyLevelItemsEntered(false), iconSizeSlider(nullptr), zoomOutAction(nullptr), zoomInAction(nullptr) { } ~KFileWidgetPrivate() { delete bookmarkHandler; // Should be deleted before ops! delete ops; } void updateLocationWhatsThis(); void updateAutoSelectExtension(); void initSpeedbar(); void setPlacesViewSplitterSizes(); void setLafBoxColumnWidth(); void initGUI(); void readViewConfig(); void writeViewConfig(); void setNonExtSelection(); void setLocationText(const QUrl &); void setLocationText(const QList &); void appendExtension(QUrl &url); void updateLocationEditExtension(const QString &); void updateFilter(); QList &parseSelectedUrls(); /** * Parses the string "line" for files. If line doesn't contain any ", the * whole line will be interpreted as one file. If the number of " is odd, * an empty list will be returned. Otherwise, all items enclosed in " " * will be returned as correct urls. */ QList tokenize(const QString &line) const; /** * Reads the recent used files and inserts them into the location combobox */ void readRecentFiles(); /** * Saves the entries from the location combobox. */ void saveRecentFiles(); /** * called when an item is highlighted/selected in multiselection mode. * handles setting the locationEdit. */ void multiSelectionChanged(); /** * Returns the absolute version of the URL specified in locationEdit. */ QUrl getCompleteUrl(const QString &) const; /** * Sets the dummy entry on the history combo box. If the dummy entry * already exists, it is overwritten with this information. */ void setDummyHistoryEntry(const QString &text, const QPixmap &icon = QPixmap(), bool usePreviousPixmapIfNull = true); /** * Removes the dummy entry of the history combo box. */ void removeDummyHistoryEntry(); /** * Asks for overwrite confirmation using a KMessageBox and returns * true if the user accepts. * * @since 4.2 */ bool toOverwrite(const QUrl &); // private slots void _k_slotLocationChanged(const QString &); void _k_urlEntered(const QUrl &); void _k_enterUrl(const QUrl &); void _k_enterUrl(const QString &); void _k_locationAccepted(const QString &); void _k_slotFilterChanged(); void _k_fileHighlighted(const KFileItem &); void _k_fileSelected(const KFileItem &); void _k_slotLoadingFinished(); void _k_fileCompletion(const QString &); void _k_toggleSpeedbar(bool); void _k_toggleBookmarks(bool); void _k_slotAutoSelectExtClicked(); void _k_placesViewSplitterMoved(int, int); void _k_activateUrlNavigator(); void _k_zoomOutIconsSize(); void _k_zoomInIconsSize(); void _k_slotIconSizeSliderMoved(int); void _k_slotIconSizeChanged(int); void _k_slotViewDoubleClicked(const QModelIndex&); void addToRecentDocuments(); QString locationEditCurrentText() const; /** * KIO::NetAccess::mostLocalUrl local replacement. * This method won't show any progress dialogs for stating, since * they are very annoying when stating. */ QUrl mostLocalUrl(const QUrl &url); void setInlinePreviewShown(bool show); KFileWidget * const q; // the last selected url QUrl url; // the selected filenames in multiselection mode -- FIXME QString filenames; // now following all kind of widgets, that I need to rebuild // the geometry management QBoxLayout *boxLayout; QGridLayout *lafBox; QVBoxLayout *vbox; QLabel *locationLabel; QWidget *opsWidget; QWidget *pathSpacer; QLabel *filterLabel; KUrlNavigator *urlNavigator; QPushButton *okButton, *cancelButton; QDockWidget *placesDock; KFilePlacesView *placesView; QSplitter *placesViewSplitter; // caches the places view width. This value will be updated when the splitter // is moved. This allows us to properly set a value when the dialog itself // is resized int placesViewWidth; QWidget *labeledCustomWidget; QWidget *bottomCustomWidget; // Automatically Select Extension stuff QCheckBox *autoSelectExtCheckBox; QString extension; // current extension for this filter QList statJobs; QList urlList; //the list of selected urls KFileWidget::OperationMode operationMode; // The file class used for KRecentDirs QString fileClass; KFileBookmarkHandler *bookmarkHandler; KActionMenu *bookmarkButton; KToolBar *toolbar; KUrlComboBox *locationEdit; KDirOperator *ops; KFileFilterCombo *filterWidget; QTimer filterDelayTimer; KFilePlacesModel *model; // whether or not the _user_ has checked the above box bool autoSelectExtChecked : 1; // indicates if the location edit should be kept or cleared when changing // directories bool keepLocation : 1; // the KDirOperators view is set in KFileWidget::show(), so to avoid // setting it again and again, we have this nice little boolean :) bool hasView : 1; bool hasDefaultFilter : 1; // necessary for the operationMode bool autoDirectoryFollowing : 1; bool inAccept : 1; // true between beginning and end of accept() bool dummyAdded : 1; // if the dummy item has been added. This prevents the combo from having a // blank item added when loaded bool confirmOverwrite : 1; bool differentHierarchyLevelItemsEntered; QSlider *iconSizeSlider; QAction *zoomOutAction; QAction *zoomInAction; // The group which stores app-specific settings. These settings are recent // files and urls. Visual settings (view mode, sorting criteria...) are not // app-specific and are stored in kdeglobals KConfigGroup configGroup; }; Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path static const char autocompletionWhatsThisText[] = I18N_NOOP("While typing in the text area, you may be presented " "with possible matches. " "This feature can be controlled by clicking with the right mouse button " "and selecting a preferred mode from the Text Completion menu."); // returns true if the string contains ":/" sequence, where is at least 2 alpha chars static bool containsProtocolSection(const QString &string) { int len = string.length(); static const char prot[] = ":/"; for (int i = 0; i < len;) { i = string.indexOf(QLatin1String(prot), i); if (i == -1) { return false; } int j = i - 1; for (; j >= 0; j--) { const QChar &ch(string[j]); if (ch.toLatin1() == 0 || !ch.isLetter()) { break; } if (ch.isSpace() && (i - j - 1) >= 2) { return true; } } if (j < 0 && i >= 2) { return true; // at least two letters before ":/" } i += 3; // skip : and / and one char } return false; } // this string-to-url conversion function handles relative paths, full paths and URLs // without the http-prepending that QUrl::fromUserInput does. static QUrl urlFromString(const QString& str) { if (QDir::isAbsolutePath(str)) { return QUrl::fromLocalFile(str); } QUrl url(str); if (url.isRelative()) { url.clear(); url.setPath(str); } return url; } KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent) : QWidget(parent), d(new KFileWidgetPrivate(this)) { QUrl startDir(_startDir); // qDebug() << "startDir" << startDir; QString filename; d->okButton = new QPushButton(this); KGuiItem::assign(d->okButton, KStandardGuiItem::ok()); d->okButton->setDefault(true); d->cancelButton = new QPushButton(this); KGuiItem::assign(d->cancelButton, KStandardGuiItem::cancel()); // The dialog shows them d->okButton->hide(); d->cancelButton->hide(); d->opsWidget = new QWidget(this); QVBoxLayout *opsWidgetLayout = new QVBoxLayout(d->opsWidget); opsWidgetLayout->setMargin(0); opsWidgetLayout->setSpacing(0); //d->toolbar = new KToolBar(this, true); d->toolbar = new KToolBar(d->opsWidget, true); d->toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar")); d->toolbar->setMovable(false); opsWidgetLayout->addWidget(d->toolbar); d->model = new KFilePlacesModel(this); // Resolve this now so that a 'kfiledialog:' URL, if specified, // does not get inserted into the urlNavigator history. d->url = getStartUrl(startDir, d->fileClass, filename); startDir = d->url; // Don't pass startDir to the KUrlNavigator at this stage: as well as // the above, it may also contain a file name which should not get // inserted in that form into the old-style navigation bar history. // Wait until the KIO::stat has been done later. // // The stat cannot be done before this point, bug 172678. d->urlNavigator = new KUrlNavigator(d->model, QUrl(), d->opsWidget); //d->toolbar); d->urlNavigator->setPlacesSelectorVisible(false); opsWidgetLayout->addWidget(d->urlNavigator); QUrl u; KUrlComboBox *pathCombo = d->urlNavigator->editor(); #ifdef Q_OS_WIN #if 0 foreach (const QFileInfo &drive, QFSFileEngine::drives()) { u = QUrl::fromLocalFile(drive.filePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), i18n("Drive: %1", u.toLocalFile())); } #else #pragma message("QT5 PORT") #endif #else u = QUrl::fromLocalFile(QDir::rootPath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); #endif u = QUrl::fromLocalFile(QDir::homePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); QUrl docPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); if (u.adjusted(QUrl::StripTrailingSlash) != docPath.adjusted(QUrl::StripTrailingSlash) && QDir(docPath.toLocalFile()).exists()) { pathCombo->addDefaultUrl(docPath, KIO::pixmapForUrl(docPath, 0, KIconLoader::Small), docPath.toLocalFile()); } u = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); d->ops = new KDirOperator(QUrl(), d->opsWidget); d->ops->setObjectName(QStringLiteral("KFileWidget::ops")); d->ops->setIsSaving(d->operationMode == Saving); opsWidgetLayout->addWidget(d->ops); connect(d->ops, SIGNAL(urlEntered(QUrl)), SLOT(_k_urlEntered(QUrl))); connect(d->ops, SIGNAL(fileHighlighted(KFileItem)), SLOT(_k_fileHighlighted(KFileItem))); connect(d->ops, SIGNAL(fileSelected(KFileItem)), SLOT(_k_fileSelected(KFileItem))); connect(d->ops, SIGNAL(finishedLoading()), SLOT(_k_slotLoadingFinished())); d->ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions); KActionCollection *coll = d->ops->actionCollection(); coll->addAssociatedWidget(this); // add nav items to the toolbar // // NOTE: The order of the button icons here differs from that // found in the file manager and web browser, but has been discussed // and agreed upon on the kde-core-devel mailing list: // // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2 coll->action(QStringLiteral("up"))->setWhatsThis(i18n("Click this button to enter the parent folder.

" "For instance, if the current location is file:/home/konqi clicking this " "button will take you to file:/home.
")); coll->action(QStringLiteral("back"))->setWhatsThis(i18n("Click this button to move backwards one step in the browsing history.")); coll->action(QStringLiteral("forward"))->setWhatsThis(i18n("Click this button to move forward one step in the browsing history.")); coll->action(QStringLiteral("reload"))->setWhatsThis(i18n("Click this button to reload the contents of the current location.")); coll->action(QStringLiteral("mkdir"))->setShortcut(QKeySequence(Qt::Key_F10)); coll->action(QStringLiteral("mkdir"))->setWhatsThis(i18n("Click this button to create a new folder.")); QAction *goToNavigatorAction = coll->addAction(QStringLiteral("gotonavigator"), this, SLOT(_k_activateUrlNavigator())); goToNavigatorAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_L)); KToggleAction *showSidebarAction = new KToggleAction(i18n("Show Places Navigation Panel"), this); coll->addAction(QStringLiteral("toggleSpeedbar"), showSidebarAction); showSidebarAction->setShortcut(QKeySequence(Qt::Key_F9)); connect(showSidebarAction, SIGNAL(toggled(bool)), SLOT(_k_toggleSpeedbar(bool))); KToggleAction *showBookmarksAction = new KToggleAction(i18n("Show Bookmarks"), this); coll->addAction(QStringLiteral("toggleBookmarks"), showBookmarksAction); connect(showBookmarksAction, SIGNAL(toggled(bool)), SLOT(_k_toggleBookmarks(bool))); KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), this); coll->addAction(QStringLiteral("extra menu"), menu); menu->setWhatsThis(i18n("This is the preferences menu for the file dialog. " "Various options can be accessed from this menu including:
    " "
  • how files are sorted in the list
  • " "
  • types of view, including icon and list
  • " "
  • showing of hidden files
  • " "
  • the Places navigation panel
  • " "
  • file previews
  • " "
  • separating folders from files
")); menu->addAction(coll->action(QStringLiteral("sorting menu"))); menu->addAction(coll->action(QStringLiteral("view menu"))); menu->addSeparator(); menu->addAction(coll->action(QStringLiteral("decoration menu"))); menu->addSeparator(); menu->addAction(coll->action(QStringLiteral("show hidden"))); menu->addAction(showSidebarAction); menu->addAction(showBookmarksAction); coll->action(QStringLiteral("inline preview")); menu->addAction(coll->action(QStringLiteral("preview"))); menu->setDelayed(false); connect(menu->menu(), &QMenu::aboutToShow, d->ops, &KDirOperator::updateSelectionDependentActions); d->iconSizeSlider = new QSlider(this); d->iconSizeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); d->iconSizeSlider->setMinimumWidth(40); d->iconSizeSlider->setOrientation(Qt::Horizontal); d->iconSizeSlider->setMinimum(0); d->iconSizeSlider->setMaximum(100); d->iconSizeSlider->installEventFilter(this); connect(d->iconSizeSlider, &QAbstractSlider::valueChanged, d->ops, &KDirOperator::setIconsZoom); connect(d->iconSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(_k_slotIconSizeChanged(int))); connect(d->iconSizeSlider, SIGNAL(sliderMoved(int)), this, SLOT(_k_slotIconSizeSliderMoved(int))); connect(d->ops, &KDirOperator::currentIconSizeChanged, [this](int value) { d->iconSizeSlider->setValue(value); d->zoomOutAction->setDisabled(value <= d->iconSizeSlider->minimum()); d->zoomInAction->setDisabled(value >= d->iconSizeSlider->maximum()); }); d->zoomOutAction = new QAction(QIcon::fromTheme(QStringLiteral("file-zoom-out")), i18n("Zoom out"), this); connect(d->zoomOutAction, SIGNAL(triggered()), SLOT(_k_zoomOutIconsSize())); d->zoomInAction = new QAction(QIcon::fromTheme(QStringLiteral("file-zoom-in")), i18n("Zoom in"), this); connect(d->zoomInAction, SIGNAL(triggered()), SLOT(_k_zoomInIconsSize())); QWidget *midSpacer = new QWidget(this); midSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->toolbar->addAction(coll->action(QStringLiteral("back"))); d->toolbar->addAction(coll->action(QStringLiteral("forward"))); d->toolbar->addAction(coll->action(QStringLiteral("up"))); d->toolbar->addAction(coll->action(QStringLiteral("reload"))); d->toolbar->addSeparator(); d->toolbar->addAction(coll->action(QStringLiteral("inline preview"))); d->toolbar->addWidget(midSpacer); d->toolbar->addAction(d->zoomOutAction); d->toolbar->addWidget(d->iconSizeSlider); d->toolbar->addAction(d->zoomInAction); d->toolbar->addSeparator(); d->toolbar->addAction(coll->action(QStringLiteral("mkdir"))); d->toolbar->addAction(menu); d->toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); d->toolbar->setMovable(false); KUrlCompletion *pathCompletionObj = new KUrlCompletion(KUrlCompletion::DirCompletion); pathCombo->setCompletionObject(pathCompletionObj); pathCombo->setAutoDeleteCompletionObject(true); connect(d->urlNavigator, SIGNAL(urlChanged(QUrl)), this, SLOT(_k_enterUrl(QUrl))); connect(d->urlNavigator, &KUrlNavigator::returnPressed, d->ops, QOverload<>::of(&QWidget::setFocus)); QString whatsThisText; // the Location label/edit d->locationLabel = new QLabel(i18n("&Name:"), this); d->locationEdit = new KUrlComboBox(KUrlComboBox::Files, true, this); d->locationEdit->installEventFilter(this); // Properly let the dialog be resized (to smaller). Otherwise we could have // huge dialogs that can't be resized to smaller (it would be as big as the longest // item in this combo box). (ereslibre) d->locationEdit->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); connect(d->locationEdit, SIGNAL(editTextChanged(QString)), SLOT(_k_slotLocationChanged(QString))); d->updateLocationWhatsThis(); d->locationLabel->setBuddy(d->locationEdit); KUrlCompletion *fileCompletionObj = new KUrlCompletion(KUrlCompletion::FileCompletion); d->locationEdit->setCompletionObject(fileCompletionObj); d->locationEdit->setAutoDeleteCompletionObject(true); connect(fileCompletionObj, SIGNAL(match(QString)), SLOT(_k_fileCompletion(QString))); connect(d->locationEdit, SIGNAL(returnPressed(QString)), this, SLOT(_k_locationAccepted(QString))); // the Filter label/edit whatsThisText = i18n("This is the filter to apply to the file list. " "File names that do not match the filter will not be shown.

" "You may select from one of the preset filters in the " "drop down menu, or you may enter a custom filter " "directly into the text area.

" "Wildcards such as * and ? are allowed.

"); d->filterLabel = new QLabel(i18n("&Filter:"), this); d->filterLabel->setWhatsThis(whatsThisText); d->filterWidget = new KFileFilterCombo(this); // Properly let the dialog be resized (to smaller). Otherwise we could have // huge dialogs that can't be resized to smaller (it would be as big as the longest // item in this combo box). (ereslibre) d->filterWidget->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); d->filterWidget->setWhatsThis(whatsThisText); d->filterLabel->setBuddy(d->filterWidget); connect(d->filterWidget, SIGNAL(filterChanged()), SLOT(_k_slotFilterChanged())); d->filterDelayTimer.setSingleShot(true); d->filterDelayTimer.setInterval(300); connect(d->filterWidget, &QComboBox::editTextChanged, &d->filterDelayTimer, QOverload<>::of(&QTimer::start)); connect(&d->filterDelayTimer, SIGNAL(timeout()), SLOT(_k_slotFilterChanged())); // the Automatically Select Extension checkbox // (the text, visibility etc. is set in updateAutoSelectExtension(), which is called by readConfig()) d->autoSelectExtCheckBox = new QCheckBox(this); const int spacingHint = style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); d->autoSelectExtCheckBox->setStyleSheet(QStringLiteral("QCheckBox { padding-top: %1px; }").arg(spacingHint)); connect(d->autoSelectExtCheckBox, SIGNAL(clicked()), SLOT(_k_slotAutoSelectExtClicked())); d->initGUI(); // activate GM // read our configuration KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group(config, ConfigGroup); readConfig(group); coll->action(QStringLiteral("inline preview"))->setChecked(d->ops->isInlinePreviewShown()); d->iconSizeSlider->setValue(d->ops->iconsZoom()); KFilePreviewGenerator *pg = d->ops->previewGenerator(); if (pg) { coll->action(QStringLiteral("inline preview"))->setChecked(pg->isPreviewShown()); } // getStartUrl() above will have resolved the startDir parameter into // a directory and file name in the two cases: (a) where it is a // special "kfiledialog:" URL, or (b) where it is a plain file name // only without directory or protocol. For any other startDir // specified, it is not possible to resolve whether there is a file name // present just by looking at the URL; the only way to be sure is // to stat it. bool statRes = false; if (filename.isEmpty()) { KIO::StatJob *statJob = KIO::stat(startDir, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); statRes = statJob->exec(); // qDebug() << "stat of" << startDir << "-> statRes" << statRes << "isDir" << statJob->statResult().isDir(); if (!statRes || !statJob->statResult().isDir()) { filename = startDir.fileName(); startDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // qDebug() << "statJob -> startDir" << startDir << "filename" << filename; } } d->ops->setUrl(startDir, true); d->urlNavigator->setLocationUrl(startDir); if (d->placesView) { d->placesView->setUrl(startDir); } // We have a file name either explicitly specified, or have checked that // we could stat it and it is not a directory. Set it. if (!filename.isEmpty()) { QLineEdit *lineEdit = d->locationEdit->lineEdit(); // qDebug() << "selecting filename" << filename; if (statRes) { d->setLocationText(QUrl(filename)); } else { lineEdit->setText(filename); // Preserve this filename when clicking on the view (cf _k_fileHighlighted) lineEdit->setModified(true); } lineEdit->selectAll(); } d->locationEdit->setFocus(); } KFileWidget::~KFileWidget() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->sync(); delete d; } void KFileWidget::setLocationLabel(const QString &text) { d->locationLabel->setText(text); } void KFileWidget::setFilter(const QString &filter) { int pos = filter.indexOf(QLatin1Char('/')); // Check for an un-escaped '/', if found // interpret as a MIME filter. if (pos > 0 && filter[pos - 1] != QLatin1Char('\\')) { QStringList filters = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); setMimeFilter(filters); return; } // Strip the escape characters from // escaped '/' characters. QString copy(filter); for (pos = 0; (pos = copy.indexOf(QStringLiteral("\\/"), pos)) != -1; ++pos) { copy.remove(pos, 1); } d->ops->clearFilter(); d->filterWidget->setFilter(copy); d->ops->setNameFilter(d->filterWidget->currentFilter()); d->ops->updateDir(); d->hasDefaultFilter = false; d->filterWidget->setEditable(true); d->updateAutoSelectExtension(); } QString KFileWidget::currentFilter() const { return d->filterWidget->currentFilter(); } void KFileWidget::setMimeFilter(const QStringList &mimeTypes, const QString &defaultType) { d->filterWidget->setMimeFilter(mimeTypes, defaultType); QStringList types = d->filterWidget->currentFilter().split(QLatin1Char(' '), QString::SkipEmptyParts); //QStringList::split(" ", d->filterWidget->currentFilter()); types.append(QStringLiteral("inode/directory")); d->ops->clearFilter(); d->ops->setMimeFilter(types); d->hasDefaultFilter = !defaultType.isEmpty(); d->filterWidget->setEditable(!d->hasDefaultFilter || d->operationMode != Saving); d->updateAutoSelectExtension(); } void KFileWidget::clearFilter() { d->filterWidget->setFilter(QString()); d->ops->clearFilter(); d->hasDefaultFilter = false; d->filterWidget->setEditable(true); d->updateAutoSelectExtension(); } QString KFileWidget::currentMimeFilter() const { int i = d->filterWidget->currentIndex(); if (d->filterWidget->showsAllTypes() && i == 0) { return QString(); // The "all types" item has no mimetype } return d->filterWidget->filters().at(i); } QMimeType KFileWidget::currentFilterMimeType() { QMimeDatabase db; return db.mimeTypeForName(currentMimeFilter()); } void KFileWidget::setPreviewWidget(KPreviewWidgetBase *w) { d->ops->setPreviewWidget(w); d->ops->clearHistory(); d->hasView = true; } QUrl KFileWidgetPrivate::getCompleteUrl(const QString &_url) const { // qDebug() << "got url " << _url; const QString url = KShell::tildeExpand(_url); QUrl u; if (QDir::isAbsolutePath(url)) { u = QUrl::fromLocalFile(url); } else { QUrl relativeUrlTest(ops->url()); relativeUrlTest.setPath(concatPaths(relativeUrlTest.path(), url)); if (!ops->dirLister()->findByUrl(relativeUrlTest).isNull() || !KProtocolInfo::isKnownProtocol(relativeUrlTest)) { u = relativeUrlTest; } else { u = QUrl(url); // keep it relative } } return u; } QSize KFileWidget::sizeHint() const { int fontSize = fontMetrics().height(); const QSize goodSize(48 * fontSize, 30 * fontSize); const QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); const QSize minSize(screenSize / 2); const QSize maxSize(screenSize * qreal(0.9)); return (goodSize.expandedTo(minSize).boundedTo(maxSize)); } static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url); // Called by KFileDialog void KFileWidget::slotOk() { // qDebug() << "slotOk\n"; const QString locationEditCurrentText(KShell::tildeExpand(d->locationEditCurrentText())); QList locationEditCurrentTextList(d->tokenize(locationEditCurrentText)); KFile::Modes mode = d->ops->mode(); // if there is nothing to do, just return from here if (!locationEditCurrentTextList.count()) { return; } // Make sure that one of the modes was provided if (!((mode & KFile::File) || (mode & KFile::Directory) || (mode & KFile::Files))) { mode |= KFile::File; // qDebug() << "No mode() provided"; } // if we are on file mode, and the list of provided files/folder is greater than one, inform // the user about it if (locationEditCurrentTextList.count() > 1) { if (mode & KFile::File) { KMessageBox::sorry(this, i18n("You can only select one file"), i18n("More than one file provided")); return; } /** * Logic of the next part of code (ends at "end multi relative urls"). * * We allow for instance to be at "/" and insert '"home/foo/bar.txt" "boot/grub/menu.lst"'. * Why we need to support this ? Because we provide tree views, which aren't plain. * * Now, how does this logic work. It will get the first element on the list (with no filename), * following the previous example say "/home/foo" and set it as the top most url. * * After this, it will iterate over the rest of items and check if this URL (topmost url) * contains the url being iterated. * * As you might have guessed it will do "/home/foo" against "/boot/grub" (again stripping * filename), and a false will be returned. Then we upUrl the top most url, resulting in * "/home" against "/boot/grub", what will again return false, so we upUrl again. Now we * have "/" against "/boot/grub", what returns true for us, so we can say that the closest * common ancestor of both is "/". * * This example has been written for 2 urls, but this works for any number of urls. */ if (!d->differentHierarchyLevelItemsEntered) { // avoid infinite recursion. running this int start = 0; QUrl topMostUrl; KIO::StatJob *statJob = nullptr; bool res = false; // we need to check for a valid first url, so in theory we only iterate one time over // this loop. However it can happen that the user did // "home/foo/nonexistantfile" "boot/grub/menu.lst", so we look for a good first // candidate. while (!res && start < locationEditCurrentTextList.count()) { topMostUrl = locationEditCurrentTextList.at(start); statJob = KIO::stat(topMostUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); res = statJob->exec(); start++; } Q_ASSERT(statJob); // if this is not a dir, strip the filename. after this we have an existent and valid // dir (we stated correctly the file). if (!statJob->statResult().isDir()) { topMostUrl = topMostUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } // now the funny part. for the rest of filenames, go and look for the closest ancestor // of all them. for (int i = start; i < locationEditCurrentTextList.count(); ++i) { QUrl currUrl = locationEditCurrentTextList.at(i); KIO::StatJob *statJob = KIO::stat(currUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { // again, we don't care about filenames if (!statJob->statResult().isDir()) { currUrl = currUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } // iterate while this item is contained on the top most url while (!topMostUrl.matches(currUrl, QUrl::StripTrailingSlash) && !topMostUrl.isParentOf(currUrl)) { topMostUrl = KIO::upUrl(topMostUrl); } } } // now recalculate all paths for them being relative in base of the top most url QStringList stringList; for (int i = 0; i < locationEditCurrentTextList.count(); ++i) { Q_ASSERT(topMostUrl.isParentOf(locationEditCurrentTextList[i])); stringList << relativePathOrUrl(topMostUrl, locationEditCurrentTextList[i]); } d->ops->setUrl(topMostUrl, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(QStringLiteral("\"%1\"").arg(stringList.join(QStringLiteral("\" \"")))); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); d->differentHierarchyLevelItemsEntered = true; slotOk(); return; } /** * end multi relative urls */ } else if (locationEditCurrentTextList.count()) { // if we are on file or files mode, and we have an absolute url written by // the user, convert it to relative if (!locationEditCurrentText.isEmpty() && !(mode & KFile::Directory) && (QDir::isAbsolutePath(locationEditCurrentText) || containsProtocolSection(locationEditCurrentText))) { QString fileName; QUrl url = urlFromString(locationEditCurrentText); if (d->operationMode == Opening) { KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { if (!statJob->statResult().isDir()) { fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash } else { if (!url.path().endsWith(QLatin1Char('/'))) { url.setPath(url.path() + QLatin1Char('/')); } } } } else { const QUrl directory = url.adjusted(QUrl::RemoveFilename); //Check if the folder exists KIO::StatJob *statJob = KIO::stat(directory, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { if (statJob->statResult().isDir()) { url = url.adjusted(QUrl::StripTrailingSlash); fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); } } } d->ops->setUrl(url, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(fileName); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); slotOk(); return; } } // restore it d->differentHierarchyLevelItemsEntered = false; // locationEditCurrentTextList contains absolute paths // this is the general loop for the File and Files mode. Obviously we know // that the File mode will iterate only one time here bool directoryMode = (mode & KFile::Directory); bool onlyDirectoryMode = directoryMode && !(mode & KFile::File) && !(mode & KFile::Files); QList::ConstIterator it = locationEditCurrentTextList.constBegin(); bool filesInList = false; while (it != locationEditCurrentTextList.constEnd()) { QUrl url(*it); if (d->operationMode == Saving && !directoryMode) { d->appendExtension(url); } d->url = url; KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), url)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->url.toDisplayString()); KMessageBox::error(this, msg); return; } // if we are on local mode, make sure we haven't got a remote base url if ((mode & KFile::LocalOnly) && !d->mostLocalUrl(d->url).isLocalFile()) { KMessageBox::sorry(this, i18n("You can only select local files"), i18n("Remote files not accepted")); return; } const auto &supportedSchemes = d->model->supportedSchemes(); if (!supportedSchemes.isEmpty() && !supportedSchemes.contains(d->url.scheme())) { KMessageBox::sorry(this, i18np("The selected URL uses an unsupported scheme. " "Please use the following scheme: %2", "The selected URL uses an unsupported scheme. " "Please use one of the following schemes: %2", supportedSchemes.size(), supportedSchemes.join(QLatin1String(", "))), i18n("Unsupported URL scheme")); return; } // if we are given a folder when not on directory mode, let's get into it if (res && !directoryMode && statJob->statResult().isDir()) { // check if we were given more than one folder, in that case we don't know to which one // cd ++it; while (it != locationEditCurrentTextList.constEnd()) { QUrl checkUrl(*it); KIO::StatJob *checkStatJob = KIO::stat(checkUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(checkStatJob, this); bool res = checkStatJob->exec(); if (res && checkStatJob->statResult().isDir()) { KMessageBox::sorry(this, i18n("More than one folder has been selected and this dialog does not accept folders, so it is not possible to decide which one to enter. Please select only one folder to list it."), i18n("More than one folder provided")); return; } else if (res) { filesInList = true; } ++it; } if (filesInList) { KMessageBox::information(this, i18n("At least one folder and one file has been selected. Selected files will be ignored and the selected folder will be listed"), i18n("Files and folders selected")); } d->ops->setUrl(url, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(QString()); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); return; } else if (res && onlyDirectoryMode && !statJob->statResult().isDir()) { // if we are given a file when on directory only mode, reject it return; } else if (!(mode & KFile::ExistingOnly) || res) { // if we don't care about ExistingOnly flag, add the file even if // it doesn't exist. If we care about it, don't add it to the list if (!onlyDirectoryMode || (res && statJob->statResult().isDir())) { d->urlList << url; } filesInList = true; } else { KMessageBox::sorry(this, i18n("The file \"%1\" could not be found", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Cannot open file")); return; // do not emit accepted() if we had ExistingOnly flag and stat failed } if ((d->operationMode == Saving) && d->confirmOverwrite && !d->toOverwrite(url)) { return; } ++it; } // if we have reached this point and we didn't return before, that is because // we want this dialog to be accepted emit accepted(); } void KFileWidget::accept() { d->inAccept = true; // parseSelectedUrls() checks that *lastDirectory() = d->ops->url(); if (!d->fileClass.isEmpty()) { KRecentDirs::add(d->fileClass, d->ops->url().toString()); } // clear the topmost item, we insert it as full path later on as item 1 d->locationEdit->setItemText(0, QString()); const QList list = selectedUrls(); QList::const_iterator it = list.begin(); int atmost = d->locationEdit->maxItems(); //don't add more items than necessary for (; it != list.end() && atmost > 0; ++it) { const QUrl &url = *it; // we strip the last slash (-1) because KUrlComboBox does that as well // when operating in file-mode. If we wouldn't , dupe-finding wouldn't // work. QString file = url.isLocalFile() ? url.toLocalFile() : url.toDisplayString(); // remove dupes for (int i = 1; i < d->locationEdit->count(); i++) { if (d->locationEdit->itemText(i) == file) { d->locationEdit->removeItem(i--); break; } } //FIXME I don't think this works correctly when the KUrlComboBox has some default urls. //KUrlComboBox should provide a function to add an url and rotate the existing ones, keeping //track of maxItems, and we shouldn't be able to insert items as we please. d->locationEdit->insertItem(1, file); atmost--; } d->writeViewConfig(); d->saveRecentFiles(); d->addToRecentDocuments(); if (!(mode() & KFile::Files)) { // single selection emit fileSelected(d->url); } d->ops->close(); } void KFileWidgetPrivate::_k_fileHighlighted(const KFileItem &i) { if ((!i.isNull() && i.isDir()) || (locationEdit->hasFocus() && !locationEdit->currentText().isEmpty())) { // don't disturb return; } const bool modified = locationEdit->lineEdit()->isModified(); if (!(ops->mode() & KFile::Files)) { if (i.isNull()) { if (!modified) { setLocationText(QUrl()); } return; } url = i.url(); if (!locationEdit->hasFocus()) { // don't disturb while editing setLocationText(url); } emit q->fileHighlighted(url); } else { multiSelectionChanged(); emit q->selectionChanged(); } locationEdit->lineEdit()->setModified(false); } void KFileWidgetPrivate::_k_fileSelected(const KFileItem &i) { if (!i.isNull() && i.isDir()) { return; } if (!(ops->mode() & KFile::Files)) { if (i.isNull()) { setLocationText(QUrl()); return; } setLocationText(i.url()); } else { multiSelectionChanged(); emit q->selectionChanged(); } // If we are saving, let another chance to the user before accepting the dialog (or trying to // accept). This way the user can choose a file and add a "_2" for instance to the filename. // Double clicking however will override this, regardless of single/double click mouse setting, // see: _k_slotViewDoubleClicked if (operationMode == KFileWidget::Saving) { locationEdit->setFocus(); } else { q->slotOk(); } } // I know it's slow to always iterate thru the whole filelist // (d->ops->selectedItems()), but what can we do? void KFileWidgetPrivate::multiSelectionChanged() { if (locationEdit->hasFocus() && !locationEdit->currentText().isEmpty()) { // don't disturb return; } const KFileItemList list = ops->selectedItems(); if (list.isEmpty()) { setLocationText(QUrl()); return; } setLocationText(list.urlList()); } void KFileWidgetPrivate::setDummyHistoryEntry(const QString &text, const QPixmap &icon, bool usePreviousPixmapIfNull) { // setCurrentItem() will cause textChanged() being emitted, // so slotLocationChanged() will be called. Make sure we don't clear // the KDirOperator's view-selection in there QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); bool dummyExists = dummyAdded; int cursorPosition = locationEdit->lineEdit()->cursorPosition(); if (dummyAdded) { if (!icon.isNull()) { locationEdit->setItemIcon(0, icon); locationEdit->setItemText(0, text); } else { if (!usePreviousPixmapIfNull) { locationEdit->setItemIcon(0, QPixmap()); } locationEdit->setItemText(0, text); } } else { if (!text.isEmpty()) { if (!icon.isNull()) { locationEdit->insertItem(0, icon, text); } else { if (!usePreviousPixmapIfNull) { locationEdit->insertItem(0, QPixmap(), text); } else { locationEdit->insertItem(0, text); } } dummyAdded = true; dummyExists = true; } } if (dummyExists && !text.isEmpty()) { locationEdit->setCurrentIndex(0); } locationEdit->lineEdit()->setCursorPosition(cursorPosition); QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); } void KFileWidgetPrivate::removeDummyHistoryEntry() { if (!dummyAdded) { return; } // setCurrentItem() will cause textChanged() being emitted, // so slotLocationChanged() will be called. Make sure we don't clear // the KDirOperator's view-selection in there QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); if (locationEdit->count()) { locationEdit->removeItem(0); } locationEdit->setCurrentIndex(-1); dummyAdded = false; QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); } void KFileWidgetPrivate::setLocationText(const QUrl &url) { if (!url.isEmpty()) { QPixmap mimeTypeIcon = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(url), KIconLoader::Small); if (!url.isRelative()) { const QUrl directory = url.adjusted(QUrl::RemoveFilename); if (!directory.path().isEmpty()) { q->setUrl(directory, false); } else { q->setUrl(url, false); } } setDummyHistoryEntry(url.fileName(), mimeTypeIcon); } else { removeDummyHistoryEntry(); } if (operationMode == KFileWidget::Saving) { setNonExtSelection(); } } static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url) { if (baseUrl.isParentOf(url)) { const QString basePath(QDir::cleanPath(baseUrl.path())); QString relPath(QDir::cleanPath(url.path())); relPath.remove(0, basePath.length()); if (relPath.startsWith(QLatin1Char('/'))) { relPath.remove(0, 1); } return relPath; } else { return url.toDisplayString(); } } void KFileWidgetPrivate::setLocationText(const QList &urlList) { const QUrl currUrl = ops->url(); if (urlList.count() > 1) { QString urls; foreach (const QUrl &url, urlList) { urls += QStringLiteral("\"%1\"").arg(relativePathOrUrl(currUrl, url)) + QLatin1Char(' '); } urls = urls.left(urls.size() - 1); setDummyHistoryEntry(urls, QPixmap(), false); } else if (urlList.count() == 1) { const QPixmap mimeTypeIcon = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(urlList[0]), KIconLoader::Small); setDummyHistoryEntry(relativePathOrUrl(currUrl, urlList[0]), mimeTypeIcon); } else { removeDummyHistoryEntry(); } if (operationMode == KFileWidget::Saving) { setNonExtSelection(); } } void KFileWidgetPrivate::updateLocationWhatsThis() { QString whatsThisText; if (operationMode == KFileWidget::Saving) { whatsThisText = QLatin1String("") + i18n("This is the name to save the file as.") + i18n(autocompletionWhatsThisText); } else if (ops->mode() & KFile::Files) { whatsThisText = QLatin1String("") + i18n("This is the list of files to open. More than " "one file can be specified by listing several " "files, separated by spaces.") + i18n(autocompletionWhatsThisText); } else { whatsThisText = QLatin1String("") + i18n("This is the name of the file to open.") + i18n(autocompletionWhatsThisText); } locationLabel->setWhatsThis(whatsThisText); locationEdit->setWhatsThis(whatsThisText); } void KFileWidgetPrivate::initSpeedbar() { if (placesDock) { return; } placesDock = new QDockWidget(i18nc("@title:window", "Places"), q); placesDock->setFeatures(QDockWidget::NoDockWidgetFeatures); placesDock->setTitleBarWidget(new KDEPrivate::KFileWidgetDockTitleBar(placesDock)); placesView = new KFilePlacesView(placesDock); placesView->setModel(model); placesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); placesView->setObjectName(QStringLiteral("url bar")); QObject::connect(placesView, SIGNAL(urlChanged(QUrl)), q, SLOT(_k_enterUrl(QUrl))); // need to set the current url of the urlbar manually (not via urlEntered() // here, because the initial url of KDirOperator might be the same as the // one that will be set later (and then urlEntered() won't be emitted). // TODO: KDE5 ### REMOVE THIS when KDirOperator's initial URL (in the c'tor) is gone. placesView->setUrl(url); placesDock->setWidget(placesView); placesViewSplitter->insertWidget(0, placesDock); // initialize the size of the splitter placesViewWidth = configGroup.readEntry(SpeedbarWidth, placesView->sizeHint().width()); // Needed for when the dialog is shown with the places panel initially hidden setPlacesViewSplitterSizes(); QObject::connect(placesDock, SIGNAL(visibilityChanged(bool)), q, SLOT(_k_toggleSpeedbar(bool))); } void KFileWidgetPrivate::setPlacesViewSplitterSizes() { if (placesViewWidth > 0) { QList sizes = placesViewSplitter->sizes(); sizes[0] = placesViewWidth; sizes[1] = q->width() - placesViewWidth - placesViewSplitter->handleWidth(); placesViewSplitter->setSizes(sizes); } } void KFileWidgetPrivate::setLafBoxColumnWidth() { // In order to perfectly align the filename widget with KDirOperator's icon view // - placesViewWidth needs to account for the size of the splitter handle // - the lafBox grid layout spacing should only affect the label, but not the line edit const int adjustment = placesViewSplitter->handleWidth() - lafBox->horizontalSpacing(); lafBox->setColumnMinimumWidth(0, placesViewWidth + adjustment); } void KFileWidgetPrivate::initGUI() { delete boxLayout; // deletes all sub layouts boxLayout = new QVBoxLayout(q); boxLayout->setMargin(0); // no additional margin to the already existing placesViewSplitter = new QSplitter(q); placesViewSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); placesViewSplitter->setChildrenCollapsible(false); boxLayout->addWidget(placesViewSplitter); QObject::connect(placesViewSplitter, SIGNAL(splitterMoved(int,int)), q, SLOT(_k_placesViewSplitterMoved(int,int))); placesViewSplitter->insertWidget(0, opsWidget); vbox = new QVBoxLayout(); vbox->setMargin(0); boxLayout->addLayout(vbox); lafBox = new QGridLayout(); lafBox->addWidget(locationLabel, 0, 0, Qt::AlignVCenter | Qt::AlignRight); lafBox->addWidget(locationEdit, 0, 1, Qt::AlignVCenter); lafBox->addWidget(okButton, 0, 2, Qt::AlignVCenter); lafBox->addWidget(filterLabel, 1, 0, Qt::AlignVCenter | Qt::AlignRight); lafBox->addWidget(filterWidget, 1, 1, Qt::AlignVCenter); lafBox->addWidget(cancelButton, 1, 2, Qt::AlignVCenter); lafBox->setColumnStretch(1, 4); vbox->addLayout(lafBox); // add the Automatically Select Extension checkbox vbox->addWidget(autoSelectExtCheckBox); q->setTabOrder(ops, autoSelectExtCheckBox); q->setTabOrder(autoSelectExtCheckBox, locationEdit); q->setTabOrder(locationEdit, filterWidget); q->setTabOrder(filterWidget, okButton); q->setTabOrder(okButton, cancelButton); q->setTabOrder(cancelButton, urlNavigator); q->setTabOrder(urlNavigator, ops); } void KFileWidgetPrivate::_k_slotFilterChanged() { // qDebug(); filterDelayTimer.stop(); QString filter = filterWidget->currentFilter(); ops->clearFilter(); if (filter.contains(QLatin1Char('/'))) { QStringList types = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); types.prepend(QStringLiteral("inode/directory")); ops->setMimeFilter(types); } else if (filter.contains(QLatin1Char('*')) || filter.contains(QLatin1Char('?')) || filter.contains(QLatin1Char('['))) { ops->setNameFilter(filter); } else { ops->setNameFilter(QLatin1Char('*') + filter.replace(QLatin1Char(' '), QLatin1Char('*')) + QLatin1Char('*')); } updateAutoSelectExtension(); ops->updateDir(); emit q->filterChanged(filter); } void KFileWidget::setUrl(const QUrl &url, bool clearforward) { // qDebug(); d->ops->setUrl(url, clearforward); } // Protected void KFileWidgetPrivate::_k_urlEntered(const QUrl &url) { // qDebug(); QString filename = locationEditCurrentText(); KUrlComboBox *pathCombo = urlNavigator->editor(); if (pathCombo->count() != 0) { // little hack pathCombo->setUrl(url); } bool blocked = locationEdit->blockSignals(true); if (keepLocation) { QUrl currentUrl = urlFromString(filename); locationEdit->changeUrl(0, QIcon::fromTheme(KIO::iconNameForUrl(currentUrl)), currentUrl); locationEdit->lineEdit()->setModified(true); } locationEdit->blockSignals(blocked); urlNavigator->setLocationUrl(url); // is trigged in ctor before completion object is set KUrlCompletion *completion = dynamic_cast(locationEdit->completionObject()); if (completion) { completion->setDir(url); } if (placesView) { placesView->setUrl(url); } } void KFileWidgetPrivate::_k_locationAccepted(const QString &url) { Q_UNUSED(url); // qDebug(); q->slotOk(); } void KFileWidgetPrivate::_k_enterUrl(const QUrl &url) { // qDebug(); // append '/' if needed: url combo does not add it // tokenize() expects it because it uses QUrl::adjusted(QUrl::RemoveFilename) QUrl u(url); if (!u.path().isEmpty() && !u.path().endsWith(QLatin1Char('/'))) { u.setPath(u.path() + QLatin1Char('/')); } q->setUrl(u); // We need to check window()->focusWidget() instead of locationEdit->hasFocus // because when the window is showing up locationEdit // may still not have focus but it'll be the one that will have focus when the window // gets it and we don't want to steal its focus either if (q->window()->focusWidget() != locationEdit) { ops->setFocus(); } } void KFileWidgetPrivate::_k_enterUrl(const QString &url) { // qDebug(); _k_enterUrl(urlFromString(KUrlCompletion::replacedPath(url, true, true))); } bool KFileWidgetPrivate::toOverwrite(const QUrl &url) { // qDebug(); KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (res) { int ret = KMessageBox::warningContinueCancel(q, i18n("The file \"%1\" already exists. Do you wish to overwrite it?", url.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (ret != KMessageBox::Continue) { return false; } return true; } return true; } #ifndef KIOFILEWIDGETS_NO_DEPRECATED void KFileWidget::setSelection(const QString &url) { // qDebug() << "setSelection " << url; if (url.isEmpty()) { return; } QUrl u = d->getCompleteUrl(url); if (!u.isValid()) { // if it still is qWarning() << url << " is not a correct argument for setSelection!"; return; } setSelectedUrl(urlFromString(url)); } #endif void KFileWidget::setSelectedUrl(const QUrl &url) { // Honor protocols that do not support directory listing if (!url.isRelative() && !KProtocolManager::supportsListing(url)) { return; } d->setLocationText(url); } void KFileWidgetPrivate::_k_slotLoadingFinished() { const QString currentText = locationEdit->currentText(); if (currentText.isEmpty()) { return; } ops->blockSignals(true); QUrl u(ops->url()); if (currentText.startsWith(QLatin1Char('/'))) u.setPath(currentText); else u.setPath(concatPaths(ops->url().path(), currentText)); ops->setCurrentItem(u); ops->blockSignals(false); } void KFileWidgetPrivate::_k_fileCompletion(const QString &match) { // qDebug(); if (match.isEmpty() || locationEdit->currentText().contains(QLatin1Char('"'))) { return; } const QUrl url = urlFromString(match); const QPixmap pix = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(url), KIconLoader::Small); setDummyHistoryEntry(locationEdit->currentText(), pix, !locationEdit->currentText().isEmpty()); } void KFileWidgetPrivate::_k_slotLocationChanged(const QString &text) { // qDebug(); locationEdit->lineEdit()->setModified(true); if (text.isEmpty() && ops->view()) { ops->view()->clearSelection(); } if (text.isEmpty()) { removeDummyHistoryEntry(); } else { setDummyHistoryEntry(text); } if (!locationEdit->lineEdit()->text().isEmpty()) { const QList urlList(tokenize(text)); ops->setCurrentItems(urlList); } updateFilter(); } QUrl KFileWidget::selectedUrl() const { // qDebug(); if (d->inAccept) { return d->url; } else { return QUrl(); } } QList KFileWidget::selectedUrls() const { // qDebug(); QList list; if (d->inAccept) { if (d->ops->mode() & KFile::Files) { list = d->parseSelectedUrls(); } else { list.append(d->url); } } return list; } QList &KFileWidgetPrivate::parseSelectedUrls() { // qDebug(); if (filenames.isEmpty()) { return urlList; } urlList.clear(); if (filenames.contains(QLatin1Char('/'))) { // assume _one_ absolute filename QUrl u; if (containsProtocolSection(filenames)) { u = QUrl(filenames); } else { u.setPath(filenames); } if (u.isValid()) { urlList.append(u); } else KMessageBox::error(q, i18n("The chosen filenames do not\n" "appear to be valid."), i18n("Invalid Filenames")); } else { urlList = tokenize(filenames); } filenames.clear(); // indicate that we parsed that one return urlList; } // FIXME: current implementation drawback: a filename can't contain quotes QList KFileWidgetPrivate::tokenize(const QString &line) const { // qDebug(); QList urls; QUrl u(ops->url()); if (!u.path().endsWith(QLatin1Char('/'))) { u.setPath(u.path() + QLatin1Char('/')); } QString name; const int count = line.count(QLatin1Char('"')); if (count == 0) { // no " " -> assume one single file if (!QDir::isAbsolutePath(line)) { u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + line); if (u.isValid()) { urls.append(u); } } else { urls << QUrl::fromLocalFile(line); } return urls; } int start = 0; int index1 = -1, index2 = -1; while (true) { index1 = line.indexOf(QLatin1Char('"'), start); index2 = line.indexOf(QLatin1Char('"'), index1 + 1); if (index1 < 0 || index2 < 0) { break; } // get everything between the " " name = line.mid(index1 + 1, index2 - index1 - 1); // since we use setPath we need to do this under a temporary url QUrl _u(u); QUrl currUrl(name); if (!QDir::isAbsolutePath(currUrl.url())) { _u = _u.adjusted(QUrl::RemoveFilename); _u.setPath(_u.path() + name); } else { // we allow to insert various absolute paths like: // "/home/foo/bar.txt" "/boot/grub/menu.lst" _u = currUrl; } if (_u.isValid()) { urls.append(_u); } start = index2 + 1; } return urls; } QString KFileWidget::selectedFile() const { // qDebug(); if (d->inAccept) { const QUrl url = d->mostLocalUrl(d->url); if (url.isLocalFile()) { return url.toLocalFile(); } else { KMessageBox::sorry(const_cast(this), i18n("You can only select local files."), i18n("Remote Files Not Accepted")); } } return QString(); } QStringList KFileWidget::selectedFiles() const { // qDebug(); QStringList list; if (d->inAccept) { if (d->ops->mode() & KFile::Files) { const QList urls = d->parseSelectedUrls(); QList::const_iterator it = urls.begin(); while (it != urls.end()) { QUrl url = d->mostLocalUrl(*it); if (url.isLocalFile()) { list.append(url.toLocalFile()); } ++it; } } else { // single-selection mode if (d->url.isLocalFile()) { list.append(d->url.toLocalFile()); } } } return list; } QUrl KFileWidget::baseUrl() const { return d->ops->url(); } void KFileWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); if (d->placesDock) { // we don't want our places dock actually changing size when we resize // and qt doesn't make it easy to enforce such a thing with QSplitter d->setPlacesViewSplitterSizes(); } } void KFileWidget::showEvent(QShowEvent *event) { if (!d->hasView) { // delayed view-creation Q_ASSERT(d); Q_ASSERT(d->ops); d->ops->setView(KFile::Default); d->ops->view()->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum)); d->hasView = true; connect(d->ops->view(), SIGNAL(doubleClicked(QModelIndex)), this, SLOT(_k_slotViewDoubleClicked(QModelIndex))); } d->ops->clearHistory(); QWidget::showEvent(event); } bool KFileWidget::eventFilter(QObject *watched, QEvent *event) { const bool res = QWidget::eventFilter(watched, event); QKeyEvent *keyEvent = dynamic_cast(event); if (watched == d->iconSizeSlider && keyEvent) { if (keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Right || keyEvent->key() == Qt::Key_Down) { d->_k_slotIconSizeSliderMoved(d->iconSizeSlider->value()); } } else if (watched == d->locationEdit && event->type() == QEvent::KeyPress) { if (keyEvent->modifiers() & Qt::AltModifier) { switch (keyEvent->key()) { case Qt::Key_Up: d->ops->actionCollection()->action(QStringLiteral("up"))->trigger(); break; case Qt::Key_Left: d->ops->actionCollection()->action(QStringLiteral("back"))->trigger(); break; case Qt::Key_Right: d->ops->actionCollection()->action(QStringLiteral("forward"))->trigger(); break; default: break; } } } return res; } void KFileWidget::setMode(KFile::Modes m) { // qDebug(); d->ops->setMode(m); if (d->ops->dirOnlyMode()) { d->filterWidget->setDefaultFilter(i18n("*|All Folders")); } else { d->filterWidget->setDefaultFilter(i18n("*|All Files")); } d->updateAutoSelectExtension(); } KFile::Modes KFileWidget::mode() const { return d->ops->mode(); } void KFileWidgetPrivate::readViewConfig() { ops->setViewConfig(configGroup); ops->readConfig(configGroup); KUrlComboBox *combo = urlNavigator->editor(); autoDirectoryFollowing = configGroup.readEntry(AutoDirectoryFollowing, DefaultDirectoryFollowing); KCompletion::CompletionMode cm = (KCompletion::CompletionMode) configGroup.readEntry(PathComboCompletionMode, static_cast(KCompletion::CompletionPopup)); if (cm != KCompletion::CompletionPopup) { combo->setCompletionMode(cm); } cm = (KCompletion::CompletionMode) configGroup.readEntry(LocationComboCompletionMode, static_cast(KCompletion::CompletionPopup)); if (cm != KCompletion::CompletionPopup) { locationEdit->setCompletionMode(cm); } // show or don't show the speedbar _k_toggleSpeedbar(configGroup.readEntry(ShowSpeedbar, true)); // show or don't show the bookmarks _k_toggleBookmarks(configGroup.readEntry(ShowBookmarks, false)); // does the user want Automatically Select Extension? autoSelectExtChecked = configGroup.readEntry(AutoSelectExtChecked, DefaultAutoSelectExtChecked); updateAutoSelectExtension(); // should the URL navigator use the breadcrumb navigation? urlNavigator->setUrlEditable(!configGroup.readEntry(BreadcrumbNavigation, true)); // should the URL navigator show the full path? urlNavigator->setShowFullPath(configGroup.readEntry(ShowFullPath, false)); int w1 = q->minimumSize().width(); int w2 = toolbar->sizeHint().width(); if (w1 < w2) { q->setMinimumWidth(w2); } } void KFileWidgetPrivate::writeViewConfig() { // these settings are global settings; ALL instances of the file dialog // should reflect them. // There is no way to tell KFileOperator::writeConfig() to write to // kdeglobals so we write settings to a temporary config group then copy // them all to kdeglobals KConfig tmp(QString(), KConfig::SimpleConfig); KConfigGroup tmpGroup(&tmp, ConfigGroup); KUrlComboBox *pathCombo = urlNavigator->editor(); //saveDialogSize( tmpGroup, KConfigGroup::Persistent | KConfigGroup::Global ); tmpGroup.writeEntry(PathComboCompletionMode, static_cast(pathCombo->completionMode())); tmpGroup.writeEntry(LocationComboCompletionMode, static_cast(locationEdit->completionMode())); const bool showSpeedbar = placesDock && !placesDock->isHidden(); tmpGroup.writeEntry(ShowSpeedbar, showSpeedbar); if (placesViewWidth > 0) { tmpGroup.writeEntry(SpeedbarWidth, placesViewWidth); } tmpGroup.writeEntry(ShowBookmarks, bookmarkHandler != nullptr); tmpGroup.writeEntry(AutoSelectExtChecked, autoSelectExtChecked); tmpGroup.writeEntry(BreadcrumbNavigation, !urlNavigator->isUrlEditable()); tmpGroup.writeEntry(ShowFullPath, urlNavigator->showFullPath()); ops->writeConfig(tmpGroup); // Copy saved settings to kdeglobals tmpGroup.copyTo(&configGroup, KConfigGroup::Persistent | KConfigGroup::Global); } void KFileWidgetPrivate::readRecentFiles() { // qDebug(); QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); locationEdit->setMaxItems(configGroup.readEntry(RecentFilesNumber, DefaultRecentURLsNumber)); locationEdit->setUrls(configGroup.readPathEntry(RecentFiles, QStringList()), KUrlComboBox::RemoveBottom); locationEdit->setCurrentIndex(-1); QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); KUrlComboBox *combo = urlNavigator->editor(); combo->setUrls(configGroup.readPathEntry(RecentURLs, QStringList()), KUrlComboBox::RemoveTop); combo->setMaxItems(configGroup.readEntry(RecentURLsNumber, DefaultRecentURLsNumber)); combo->setUrl(ops->url()); // since we delayed this moment, initialize the directory of the completion object to // our current directory (that was very probably set on the constructor) KUrlCompletion *completion = dynamic_cast(locationEdit->completionObject()); if (completion) { completion->setDir(ops->url()); } } void KFileWidgetPrivate::saveRecentFiles() { // qDebug(); configGroup.writePathEntry(RecentFiles, locationEdit->urls()); KUrlComboBox *pathCombo = urlNavigator->editor(); configGroup.writePathEntry(RecentURLs, pathCombo->urls()); } QPushButton *KFileWidget::okButton() const { return d->okButton; } QPushButton *KFileWidget::cancelButton() const { return d->cancelButton; } // Called by KFileDialog void KFileWidget::slotCancel() { d->writeViewConfig(); d->ops->close(); } void KFileWidget::setKeepLocation(bool keep) { d->keepLocation = keep; } bool KFileWidget::keepsLocation() const { return d->keepLocation; } void KFileWidget::setOperationMode(OperationMode mode) { // qDebug(); d->operationMode = mode; d->keepLocation = (mode == Saving); d->filterWidget->setEditable(!d->hasDefaultFilter || mode != Saving); if (mode == Opening) { // don't use KStandardGuiItem::open() here which has trailing ellipsis! d->okButton->setText(i18n("&Open")); d->okButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); // hide the new folder actions...usability team says they shouldn't be in open file dialog actionCollection()->removeAction(actionCollection()->action(QStringLiteral("mkdir"))); } else if (mode == Saving) { KGuiItem::assign(d->okButton, KStandardGuiItem::save()); d->setNonExtSelection(); } else { KGuiItem::assign(d->okButton, KStandardGuiItem::ok()); } d->updateLocationWhatsThis(); d->updateAutoSelectExtension(); if (d->ops) { d->ops->setIsSaving(mode == Saving); } } KFileWidget::OperationMode KFileWidget::operationMode() const { return d->operationMode; } void KFileWidgetPrivate::_k_slotAutoSelectExtClicked() { // qDebug() << "slotAutoSelectExtClicked(): " // << autoSelectExtCheckBox->isChecked() << endl; // whether the _user_ wants it on/off autoSelectExtChecked = autoSelectExtCheckBox->isChecked(); // update the current filename's extension updateLocationEditExtension(extension /* extension hasn't changed */); } void KFileWidgetPrivate::_k_placesViewSplitterMoved(int pos, int index) { // qDebug(); // we need to record the size of the splitter when the splitter changes size // so we can keep the places box the right size! if (placesDock && index == 1) { placesViewWidth = pos; // qDebug() << "setting lafBox minwidth to" << placesViewWidth; setLafBoxColumnWidth(); } } void KFileWidgetPrivate::_k_activateUrlNavigator() { // qDebug(); urlNavigator->setUrlEditable(!urlNavigator->isUrlEditable()); if (urlNavigator->isUrlEditable()) { urlNavigator->setFocus(); urlNavigator->editor()->lineEdit()->selectAll(); } } void KFileWidgetPrivate::_k_zoomOutIconsSize() { const int currValue = ops->iconsZoom(); const int futValue = qMax(0, currValue - 10); iconSizeSlider->setValue(futValue); _k_slotIconSizeSliderMoved(futValue); } void KFileWidgetPrivate::_k_zoomInIconsSize() { const int currValue = ops->iconsZoom(); const int futValue = qMin(100, currValue + 10); iconSizeSlider->setValue(futValue); _k_slotIconSizeSliderMoved(futValue); } void KFileWidgetPrivate::_k_slotIconSizeChanged(int _value) { int maxSize = KIconLoader::SizeEnormous - KIconLoader::SizeSmall; int value = (maxSize * _value / 100) + KIconLoader::SizeSmall; switch (value) { case KIconLoader::SizeSmall: case KIconLoader::SizeSmallMedium: case KIconLoader::SizeMedium: case KIconLoader::SizeLarge: case KIconLoader::SizeHuge: case KIconLoader::SizeEnormous: iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels (standard size)", value)); break; default: iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels", value)); break; } } void KFileWidgetPrivate::_k_slotIconSizeSliderMoved(int _value) { // Force this to be called in case this slot is called first on the // slider move. _k_slotIconSizeChanged(_value); QPoint global(iconSizeSlider->rect().topLeft()); global.ry() += iconSizeSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), iconSizeSlider->mapToGlobal(global)); QApplication::sendEvent(iconSizeSlider, &toolTipEvent); } void KFileWidgetPrivate::_k_slotViewDoubleClicked(const QModelIndex &index) { // double clicking to save should only work on files if (operationMode == KFileWidget::Saving && index.isValid() && ops->selectedItems().constFirst().isFile()) { q->slotOk(); } } static QString getExtensionFromPatternList(const QStringList &patternList) { // qDebug(); QString ret; // qDebug() << "\tgetExtension " << patternList; QStringList::ConstIterator patternListEnd = patternList.end(); for (QStringList::ConstIterator it = patternList.begin(); it != patternListEnd; ++it) { // qDebug() << "\t\ttry: \'" << (*it) << "\'"; // is this pattern like "*.BMP" rather than useless things like: // // README // *. // *.* // *.JP*G // *.JP? if ((*it).startsWith(QLatin1String("*.")) && (*it).length() > 2 && (*it).indexOf(QLatin1Char('*'), 2) < 0 && (*it).indexOf(QLatin1Char('?'), 2) < 0) { ret = (*it).mid(1); break; } } return ret; } static QString stripUndisplayable(const QString &string) { QString ret = string; ret.remove(QLatin1Char(':')); ret = KLocalizedString::removeAcceleratorMarker(ret); return ret; } //QString KFileWidget::currentFilterExtension() //{ // return d->extension; //} void KFileWidgetPrivate::updateAutoSelectExtension() { if (!autoSelectExtCheckBox) { return; } QMimeDatabase db; // // Figure out an extension for the Automatically Select Extension thing // (some Windows users apparently don't know what to do when confronted // with a text file called "COPYING" but do know what to do with // COPYING.txt ...) // // qDebug() << "Figure out an extension: "; QString lastExtension = extension; extension.clear(); // Automatically Select Extension is only valid if the user is _saving_ a _file_ if ((operationMode == KFileWidget::Saving) && (ops->mode() & KFile::File)) { // // Get an extension from the filter // QString filter = filterWidget->currentFilter(); if (!filter.isEmpty()) { // if the currently selected filename already has an extension which // is also included in the currently allowed extensions, keep it // otherwise use the default extension QString currentExtension = db.suffixForFileName(locationEditCurrentText()); if (currentExtension.isEmpty()) { currentExtension = locationEditCurrentText().section(QLatin1Char('.'), -1, -1); } // qDebug() << "filter:" << filter << "locationEdit:" << locationEditCurrentText() << "currentExtension:" << currentExtension; QString defaultExtension; QStringList extensionList; // e.g. "*.cpp" if (filter.indexOf(QLatin1Char('/')) < 0) { extensionList = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); defaultExtension = getExtensionFromPatternList(extensionList); } // e.g. "text/html" else { QMimeType mime = db.mimeTypeForName(filter); if (mime.isValid()) { extensionList = mime.globPatterns(); defaultExtension = mime.preferredSuffix(); if (!defaultExtension.isEmpty()) { defaultExtension.prepend(QLatin1Char('.')); } } } if ((!currentExtension.isEmpty() && extensionList.contains(QLatin1String("*.") + currentExtension)) || filter == QStringLiteral("application/octet-stream")) { extension = QLatin1Char('.') + currentExtension; } else { extension = defaultExtension; } // qDebug() << "List:" << extensionList << "auto-selected extension:" << extension; } // // GUI: checkbox // QString whatsThisExtension; if (!extension.isEmpty()) { // remember: sync any changes to the string with below autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension (%1)", extension)); whatsThisExtension = i18n("the extension %1", extension); autoSelectExtCheckBox->setEnabled(true); autoSelectExtCheckBox->setChecked(autoSelectExtChecked); } else { // remember: sync any changes to the string with above autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension")); whatsThisExtension = i18n("a suitable extension"); autoSelectExtCheckBox->setChecked(false); autoSelectExtCheckBox->setEnabled(false); } const QString locationLabelText = stripUndisplayable(locationLabel->text()); autoSelectExtCheckBox->setWhatsThis(QLatin1String("") + i18n( "This option enables some convenient features for " "saving files with extensions:
" "
    " "
  1. Any extension specified in the %1 text " "area will be updated if you change the file type " "to save in.
    " "
  2. " "
  3. If no extension is specified in the %2 " "text area when you click " "Save, %3 will be added to the end of the " "filename (if the filename does not already exist). " "This extension is based on the file type that you " "have chosen to save in.
    " "
    " "If you do not want KDE to supply an extension for the " "filename, you can either turn this option off or you " "can suppress it by adding a period (.) to the end of " "the filename (the period will be automatically " "removed)." "
  4. " "
" "If unsure, keep this option enabled as it makes your " "files more manageable." , locationLabelText, locationLabelText, whatsThisExtension) + QLatin1String("
") ); autoSelectExtCheckBox->show(); // update the current filename's extension updateLocationEditExtension(lastExtension); } // Automatically Select Extension not valid else { autoSelectExtCheckBox->setChecked(false); autoSelectExtCheckBox->hide(); } } // Updates the extension of the filename specified in d->locationEdit if the // Automatically Select Extension feature is enabled. // (this prevents you from accidently saving "file.kwd" as RTF, for example) void KFileWidgetPrivate::updateLocationEditExtension(const QString &lastExtension) { if (!autoSelectExtCheckBox->isChecked() || extension.isEmpty()) { return; } QString urlStr = locationEditCurrentText(); if (urlStr.isEmpty()) { return; } QUrl url = getCompleteUrl(urlStr); // qDebug() << "updateLocationEditExtension (" << url << ")"; const int fileNameOffset = urlStr.lastIndexOf(QLatin1Char('/')) + 1; QString fileName = urlStr.mid(fileNameOffset); const int dot = fileName.lastIndexOf(QLatin1Char('.')); const int len = fileName.length(); if (dot > 0 && // has an extension already and it's not a hidden file // like ".hidden" (but we do accept ".hidden.ext") dot != len - 1 // and not deliberately suppressing extension ) { // exists? KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool result = statJob->exec(); if (result) { // qDebug() << "\tfile exists"; if (statJob->statResult().isDir()) { // qDebug() << "\tisDir - won't alter extension"; return; } // --- fall through --- } // // try to get rid of the current extension // // catch "double extensions" like ".tar.gz" if (lastExtension.length() && fileName.endsWith(lastExtension)) { fileName.chop(lastExtension.length()); } else if (extension.length() && fileName.endsWith(extension)) { fileName.chop(extension.length()); } // can only handle "single extensions" else { fileName.truncate(dot); } // add extension - const QString newText = urlStr.left(fileNameOffset) + fileName + extension; + const QString newText = urlStr.leftRef(fileNameOffset) + fileName + extension; if (newText != locationEditCurrentText()) { - locationEdit->setItemText(locationEdit->currentIndex(), urlStr.left(fileNameOffset) + fileName + extension); + locationEdit->setItemText(locationEdit->currentIndex(), newText); locationEdit->lineEdit()->setModified(true); } } } // Updates the filter if the extension of the filename specified in d->locationEdit is changed // (this prevents you from accidently saving "file.kwd" as RTF, for example) void KFileWidgetPrivate::updateFilter() { // qDebug(); if ((operationMode == KFileWidget::Saving) && (ops->mode() & KFile::File)) { QString urlStr = locationEditCurrentText(); if (urlStr.isEmpty()) { return; } if (filterWidget->isMimeFilter()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(urlStr, QMimeDatabase::MatchExtension); if (mime.isValid() && !mime.isDefault()) { if (filterWidget->currentFilter() != mime.name() && filterWidget->filters().indexOf(mime.name()) != -1) { filterWidget->setCurrentFilter(mime.name()); } } } else { QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename foreach (const QString &filter, filterWidget->filters()) { QStringList patterns = filter.left(filter.indexOf(QLatin1Char('|'))).split(QLatin1Char(' '), QString::SkipEmptyParts); // '*.foo *.bar|Foo type' -> '*.foo', '*.bar' foreach (const QString &p, patterns) { QRegExp rx(p); rx.setPatternSyntax(QRegExp::Wildcard); if (rx.exactMatch(filename)) { if (p != QLatin1String("*")) { // never match the catch-all filter filterWidget->setCurrentFilter(filter); } return; // do not repeat, could match a later filter } } } } } } // applies only to a file that doesn't already exist void KFileWidgetPrivate::appendExtension(QUrl &url) { // qDebug(); if (!autoSelectExtCheckBox->isChecked() || extension.isEmpty()) { return; } QString fileName = url.fileName(); if (fileName.isEmpty()) { return; } // qDebug() << "appendExtension(" << url << ")"; const int len = fileName.length(); const int dot = fileName.lastIndexOf(QLatin1Char('.')); const bool suppressExtension = (dot == len - 1); const bool unspecifiedExtension = (dot <= 0); // don't KIO::Stat if unnecessary if (!(suppressExtension || unspecifiedExtension)) { return; } // exists? KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (res) { // qDebug() << "\tfile exists - won't append extension"; return; } // suppress automatically append extension? if (suppressExtension) { // // Strip trailing dot // This allows lazy people to have autoSelectExtCheckBox->isChecked // but don't want a file extension to be appended // e.g. "README." will make a file called "README" // // If you really want a name like "README.", then type "README.." // and the trailing dot will be removed (or just stop being lazy and // turn off this feature so that you can type "README.") // // qDebug() << "\tstrip trailing dot"; QString path = url.path(); path.chop(1); url.setPath(path); } // evilmatically append extension :) if the user hasn't specified one else if (unspecifiedExtension) { // qDebug() << "\tappending extension \'" << extension << "\'..."; url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash url.setPath(url.path() + fileName + extension); // qDebug() << "\tsaving as \'" << url << "\'"; } } // adds the selected files/urls to 'recent documents' void KFileWidgetPrivate::addToRecentDocuments() { int m = ops->mode(); int atmost = KRecentDocument::maximumItems(); //don't add more than we need. KRecentDocument::add() is pretty slow if (m & KFile::LocalOnly) { const QStringList files = q->selectedFiles(); QStringList::ConstIterator it = files.begin(); for (; it != files.end() && atmost > 0; ++it) { KRecentDocument::add(QUrl::fromLocalFile(*it)); atmost--; } } else { // urls const QList urls = q->selectedUrls(); QList::ConstIterator it = urls.begin(); for (; it != urls.end() && atmost > 0; ++it) { if ((*it).isValid()) { KRecentDocument::add(*it); atmost--; } } } } KUrlComboBox *KFileWidget::locationEdit() const { return d->locationEdit; } KFileFilterCombo *KFileWidget::filterWidget() const { return d->filterWidget; } KActionCollection *KFileWidget::actionCollection() const { return d->ops->actionCollection(); } void KFileWidgetPrivate::_k_toggleSpeedbar(bool show) { if (show) { initSpeedbar(); placesDock->show(); setLafBoxColumnWidth(); // check to see if they have a home item defined, if not show the home button QUrl homeURL; homeURL.setPath(QDir::homePath()); KFilePlacesModel *model = static_cast(placesView->model()); for (int rowIndex = 0; rowIndex < model->rowCount(); rowIndex++) { QModelIndex index = model->index(rowIndex, 0); QUrl url = model->url(index); if (homeURL.matches(url, QUrl::StripTrailingSlash)) { toolbar->removeAction(ops->actionCollection()->action(QStringLiteral("home"))); break; } } } else { if (q->sender() == placesDock && placesDock && placesDock->isVisibleTo(q)) { // we didn't *really* go away! the dialog was simply hidden or // we changed virtual desktops or ... return; } if (placesDock) { placesDock->hide(); } QAction *homeAction = ops->actionCollection()->action(QStringLiteral("home")); QAction *reloadAction = ops->actionCollection()->action(QStringLiteral("reload")); if (!toolbar->actions().contains(homeAction)) { toolbar->insertAction(reloadAction, homeAction); } // reset the lafbox to not follow the width of the splitter lafBox->setColumnMinimumWidth(0, 0); } static_cast(q->actionCollection()->action(QStringLiteral("toggleSpeedbar")))->setChecked(show); // if we don't show the places panel, at least show the places menu urlNavigator->setPlacesSelectorVisible(!show); } void KFileWidgetPrivate::_k_toggleBookmarks(bool show) { if (show) { if (bookmarkHandler) { return; } bookmarkHandler = new KFileBookmarkHandler(q); q->connect(bookmarkHandler, SIGNAL(openUrl(QString)), SLOT(_k_enterUrl(QString))); bookmarkButton = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), q); bookmarkButton->setDelayed(false); q->actionCollection()->addAction(QStringLiteral("bookmark"), bookmarkButton); bookmarkButton->setMenu(bookmarkHandler->menu()); bookmarkButton->setWhatsThis(i18n("This button allows you to bookmark specific locations. " "Click on this button to open the bookmark menu where you may add, " "edit or select a bookmark.

" "These bookmarks are specific to the file dialog, but otherwise operate " "like bookmarks elsewhere in KDE.
")); toolbar->addAction(bookmarkButton); } else if (bookmarkHandler) { delete bookmarkHandler; bookmarkHandler = nullptr; delete bookmarkButton; bookmarkButton = nullptr; } static_cast(q->actionCollection()->action(QStringLiteral("toggleBookmarks")))->setChecked(show); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass) { QString fileName; // result discarded return getStartUrl(startDir, recentDirClass, fileName); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName) { recentDirClass.clear(); fileName.clear(); QUrl ret; bool useDefaultStartDir = startDir.isEmpty(); if (!useDefaultStartDir) { if (startDir.scheme() == QLatin1String("kfiledialog")) { // The startDir URL with this protocol may be in the format: // directory() fileName() // 1. kfiledialog:///keyword "/" keyword // 2. kfiledialog:///keyword?global "/" keyword // 3. kfiledialog:///keyword/ "/" keyword // 4. kfiledialog:///keyword/?global "/" keyword // 5. kfiledialog:///keyword/filename /keyword filename // 6. kfiledialog:///keyword/filename?global /keyword filename QString keyword; QString urlDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString urlFile = startDir.fileName(); if (urlDir == QLatin1String("/")) { // '1'..'4' above keyword = urlFile; fileName.clear(); } else { // '5' or '6' above keyword = urlDir.mid(1); fileName = urlFile; } if (startDir.query() == QLatin1String("global")) { recentDirClass = QStringLiteral("::%1").arg(keyword); } else { recentDirClass = QStringLiteral(":%1").arg(keyword); } ret = QUrl::fromLocalFile(KRecentDirs::dir(recentDirClass)); } else { // not special "kfiledialog" URL // "foo.png" only gives us a file name, the default start dir will be used. // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same // (and is the reason why we don't just use QUrl::isRelative()). // In all other cases (startDir contains a directory path, or has no // fileName for us anyway, such as smb://), startDir is indeed a dir url. if (!startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) { // can use start directory ret = startDir; // will be checked by stat later // If we won't be able to list it (e.g. http), then use default if (!KProtocolManager::supportsListing(ret)) { useDefaultStartDir = true; fileName = startDir.fileName(); } } else { // file name only fileName = startDir.fileName(); useDefaultStartDir = true; } } } if (useDefaultStartDir) { if (lastDirectory()->isEmpty()) { *lastDirectory() = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); const QUrl home(QUrl::fromLocalFile(QDir::homePath())); // if there is no docpath set (== home dir), we prefer the current // directory over it. We also prefer the homedir when our CWD is // different from our homedirectory or when the document dir // does not exist if (lastDirectory()->adjusted(QUrl::StripTrailingSlash) == home.adjusted(QUrl::StripTrailingSlash) || QDir::currentPath() != QDir::homePath() || !QDir(lastDirectory()->toLocalFile()).exists()) { *lastDirectory() = QUrl::fromLocalFile(QDir::currentPath()); } } ret = *lastDirectory(); } // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName; return ret; } void KFileWidget::setStartDir(const QUrl &directory) { if (directory.isValid()) { *lastDirectory() = directory; } } void KFileWidgetPrivate::setNonExtSelection() { // Enhanced rename: Don't highlight the file extension. QString filename = locationEditCurrentText(); QMimeDatabase db; QString extension = db.suffixForFileName(filename); if (!extension.isEmpty()) { locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1); } else { int lastDot = filename.lastIndexOf(QLatin1Char('.')); if (lastDot > 0) { locationEdit->lineEdit()->setSelection(0, lastDot); } else { locationEdit->lineEdit()->selectAll(); } } } KToolBar *KFileWidget::toolBar() const { return d->toolbar; } void KFileWidget::setCustomWidget(QWidget *widget) { delete d->bottomCustomWidget; d->bottomCustomWidget = widget; // add it to the dialog, below the filter list box. // Change the parent so that this widget is a child of the main widget d->bottomCustomWidget->setParent(this); d->vbox->addWidget(d->bottomCustomWidget); //d->vbox->addSpacing(3); // can't do this every time... // FIXME: This should adjust the tab orders so that the custom widget // comes after the Cancel button. The code appears to do this, but the result // somehow screws up the tab order of the file path combo box. Not a major // problem, but ideally the tab order with a custom widget should be // the same as the order without one. setTabOrder(d->cancelButton, d->bottomCustomWidget); setTabOrder(d->bottomCustomWidget, d->urlNavigator); } void KFileWidget::setCustomWidget(const QString &text, QWidget *widget) { delete d->labeledCustomWidget; d->labeledCustomWidget = widget; QLabel *label = new QLabel(text, this); label->setAlignment(Qt::AlignRight); d->lafBox->addWidget(label, 2, 0, Qt::AlignVCenter); d->lafBox->addWidget(widget, 2, 1, Qt::AlignVCenter); } KDirOperator *KFileWidget::dirOperator() { return d->ops; } void KFileWidget::readConfig(KConfigGroup &group) { d->configGroup = group; d->readViewConfig(); d->readRecentFiles(); } QString KFileWidgetPrivate::locationEditCurrentText() const { return QDir::fromNativeSeparators(locationEdit->currentText()); } QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url) { if (url.isLocalFile()) { return url; } KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (!res) { return url; } const QString path = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); if (!path.isEmpty()) { QUrl newUrl; newUrl.setPath(path); return newUrl; } return url; } void KFileWidgetPrivate::setInlinePreviewShown(bool show) { ops->setInlinePreviewShown(show); } void KFileWidget::setConfirmOverwrite(bool enable) { d->confirmOverwrite = enable; } void KFileWidget::setInlinePreviewShown(bool show) { d->setInlinePreviewShown(show); } QSize KFileWidget::dialogSizeHint() const { int fontSize = fontMetrics().height(); QSize goodSize(48 * fontSize, 30 * fontSize); QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); QSize minSize(screenSize / 2); QSize maxSize(screenSize * qreal(0.9)); return (goodSize.expandedTo(minSize).boundedTo(maxSize)); } void KFileWidget::setViewMode(KFile::FileView mode) { d->ops->setView(mode); d->hasView = true; } void KFileWidget::setSupportedSchemes(const QStringList &schemes) { d->model->setSupportedSchemes(schemes); d->ops->setSupportedSchemes(schemes); d->urlNavigator->setCustomProtocols(schemes); } QStringList KFileWidget::supportedSchemes() const { return d->model->supportedSchemes(); } #include "moc_kfilewidget.cpp" diff --git a/src/filewidgets/knewfilemenu.cpp b/src/filewidgets/knewfilemenu.cpp index c6b1564f..31e9d9f3 100644 --- a/src/filewidgets/knewfilemenu.cpp +++ b/src/filewidgets/knewfilemenu.cpp @@ -1,1317 +1,1317 @@ /* This file is part of the KDE project Copyright (C) 1998-2009 David Faure 2003 Sven Leiber This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 or at your option version 3. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "knewfilemenu.h" #include "../pathhelpers_p.h" #include "knameandurlinputdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #endif static QString expandTilde(const QString &name, bool isfile = false) { if (!name.isEmpty() && (!isfile || name[0] == QLatin1Char('\\'))) { const QString expandedName = KShell::tildeExpand(name); // When a tilde mark cannot be properly expanded, the above call // returns an empty string... if (!expandedName.isEmpty()) { return expandedName; } } return name; } // Singleton, with data shared by all KNewFileMenu instances class KNewFileMenuSingleton { public: KNewFileMenuSingleton() : dirWatch(nullptr), filesParsed(false), templatesList(nullptr), templatesVersion(0) { } ~KNewFileMenuSingleton() { delete dirWatch; delete templatesList; } /** * Opens the desktop files and completes the Entry list * Input: the entry list. Output: the entry list ;-) */ void parseFiles(); /** * For entryType * LINKTOTEMPLATE: a desktop file that points to a file or dir to copy * TEMPLATE: a real file to copy as is (the KDE-1.x solution) * SEPARATOR: to put a separator in the menu * 0 means: not parsed, i.e. we don't know */ enum EntryType { Unknown, LinkToTemplate = 1, Template, Separator }; KDirWatch *dirWatch; struct Entry { QString text; QString filePath; // empty for Separator QString templatePath; // same as filePath for Template QString icon; EntryType entryType; QString comment; QString mimeType; }; // NOTE: only filePath is known before we call parseFiles /** * List of all template files. It is important that they are in * the same order as the 'New' menu. */ typedef QList EntryList; /** * Set back to false each time new templates are found, * and to true on the first call to parseFiles */ bool filesParsed; EntryList *templatesList; /** * Is increased when templatesList has been updated and * menu needs to be re-filled. Menus have their own version and compare it * to templatesVersion before showing up */ int templatesVersion; }; void KNewFileMenuSingleton::parseFiles() { //qDebug(); filesParsed = true; QMutableListIterator templIter(*templatesList); while (templIter.hasNext()) { KNewFileMenuSingleton::Entry &templ = templIter.next(); const QString filePath = templ.filePath; if (!filePath.isEmpty()) { QString text; QString templatePath; // If a desktop file, then read the name from it. // Otherwise (or if no name in it?) use file name if (KDesktopFile::isDesktopFile(filePath)) { KDesktopFile desktopFile(filePath); if (desktopFile.noDisplay()) { templIter.remove(); continue; } text = desktopFile.readName(); templ.icon = desktopFile.readIcon(); templ.comment = desktopFile.readComment(); QString type = desktopFile.readType(); if (type == QLatin1String("Link")) { templatePath = desktopFile.desktopGroup().readPathEntry("URL", QString()); if (templatePath[0] != QLatin1Char('/') && !templatePath.startsWith(QLatin1String("__"))) { if (templatePath.startsWith(QLatin1String("file:/"))) { templatePath = QUrl(templatePath).toLocalFile(); } else { // A relative path, then (that's the default in the files we ship) - QString linkDir = filePath.left(filePath.lastIndexOf(QLatin1Char('/')) + 1 /*keep / */); + const QStringRef linkDir = filePath.leftRef(filePath.lastIndexOf(QLatin1Char('/')) + 1 /*keep / */); //qDebug() << "linkDir=" << linkDir; templatePath = linkDir + templatePath; } } } if (templatePath.isEmpty()) { // No URL key, this is an old-style template templ.entryType = KNewFileMenuSingleton::Template; templ.templatePath = templ.filePath; // we'll copy the file } else { templ.entryType = KNewFileMenuSingleton::LinkToTemplate; templ.templatePath = templatePath; } } if (text.isEmpty()) { text = QUrl(filePath).fileName(); if (text.endsWith(QLatin1String(".desktop"))) { text.chop(8); } } templ.text = text; /*// qDebug() << "Updating entry with text=" << text << "entryType=" << templ.entryType << "templatePath=" << templ.templatePath;*/ } else { templ.entryType = KNewFileMenuSingleton::Separator; } } } Q_GLOBAL_STATIC(KNewFileMenuSingleton, kNewMenuGlobals) class KNewFileMenuCopyData { public: KNewFileMenuCopyData() { m_isSymlink = false; } QString chosenFileName() const { return m_chosenFileName; } // If empty, no copy is performed. QString sourceFileToCopy() const { return m_src; } QString tempFileToDelete() const { return m_tempFileToDelete; } bool m_isSymlink; QString m_chosenFileName; QString m_src; QString m_tempFileToDelete; QString m_templatePath; }; class KNewFileMenuPrivate { public: explicit KNewFileMenuPrivate(KNewFileMenu *qq) : m_menuItemsVersion(0), m_modal(true), m_viewShowsHiddenFiles(false), q(qq) {} bool checkSourceExists(const QString &src); /** * Asks user whether to create a hidden directory with a dialog */ void confirmCreatingHiddenDir(const QString &name); /** * The strategy used for other desktop files than Type=Link. Example: Application, Device. */ void executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry); /** * The strategy used for "real files or directories" (the common case) */ void executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry); /** * Actually performs file handling. Reads in m_copyData for needed data, that has been collected by execute*() before */ void executeStrategy(); /** * The strategy used when creating a symlink */ void executeSymLink(const KNewFileMenuSingleton::Entry &entry); /** * The strategy used for "url" desktop files */ void executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry); /** * Fills the menu from the templates list. */ void fillMenu(); /** * Tries to map a local URL for the given URL. */ QUrl mostLocalUrl(const QUrl &url); /** * Just clears the string buffer d->m_text, but I need a slot for this to occur */ void _k_slotAbortDialog(); /** * Called when New->* is clicked */ void _k_slotActionTriggered(QAction *action); /** * Callback function that reads in directory name from dialog and processes it */ void _k_slotCreateDirectory(bool writeHiddenDir = false); /** * Callback function that reads in directory name from dialog and processes it. This will wirte * a hidden directory without further questions */ void _k_slotCreateHiddenDirectory(); /** * Fills the templates list. */ void _k_slotFillTemplates(); /** * Called when accepting the KPropertiesDialog (for "other desktop files") */ void _k_slotOtherDesktopFile(); /** * Called when closing the KPropertiesDialog is closed (whichever way, accepted and rejected) */ void _k_slotOtherDesktopFileClosed(); /** * Callback in KNewFileMenu for the RealFile Dialog. Handles dialog input and gives over * to executeStrategy() */ void _k_slotRealFileOrDir(); /** * Dialogs use this slot to write the changed string into KNewFile menu when the user * changes touches them */ void _k_slotTextChanged(const QString &text); /** * Callback in KNewFileMenu for the Symlink Dialog. Handles dialog input and gives over * to executeStrategy() */ void _k_slotSymLink(); /** * Callback in KNewFileMenu for the Url/Desktop Dialog. Handles dialog input and gives over * to executeStrategy() */ void _k_slotUrlDesktopFile(); KActionCollection *m_actionCollection; QDialog *m_fileDialog; KActionMenu *m_menuDev; int m_menuItemsVersion; bool m_modal; QAction *m_newDirAction; /** * The action group that our actions belong to */ QActionGroup *m_newMenuGroup; QWidget *m_parentWidget; /** * When the user pressed the right mouse button over an URL a popup menu * is displayed. The URL belonging to this popup menu is stored here. */ QList m_popupFiles; QStringList m_supportedMimeTypes; QString m_tempFileToDelete; // set when a tempfile was created for a Type=URL desktop file QString m_text; bool m_viewShowsHiddenFiles; KNewFileMenu * const q; KNewFileMenuCopyData m_copyData; }; bool KNewFileMenuPrivate::checkSourceExists(const QString &src) { if (!QFile::exists(src)) { qWarning() << src << "doesn't exist"; QDialog *dialog = new QDialog(m_parentWidget); dialog->setWindowTitle(i18n("Sorry")); dialog->setObjectName(QStringLiteral("sorry")); dialog->setModal(q->isModal()); dialog->setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok); KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Warning, i18n("The template file %1 does not exist.", src), QStringList(), QString(), nullptr, KMessageBox::NoExec, QString()); dialog->show(); return false; } return true; } void KNewFileMenuPrivate::confirmCreatingHiddenDir(const QString &name) { if (!KMessageBox::shouldBeShownContinue(QStringLiteral("confirm_create_hidden_dir"))) { _k_slotCreateHiddenDirectory(); return; } KGuiItem continueGuiItem(KStandardGuiItem::cont()); continueGuiItem.setText(i18nc("@action:button", "Create directory")); KGuiItem cancelGuiItem(KStandardGuiItem::cancel()); cancelGuiItem.setText(i18nc("@action:button", "Enter a Different Name")); cancelGuiItem.setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); QDialog *confirmDialog = new QDialog(m_parentWidget); confirmDialog->setWindowTitle(i18n("Create hidden directory?")); confirmDialog->setModal(m_modal); confirmDialog->setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(confirmDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), continueGuiItem); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), cancelGuiItem); KMessageBox::createKMessageBox(confirmDialog, buttonBox, QMessageBox::Warning, i18n("The name \"%1\" starts with a dot, so the directory will be hidden by default.", name), QStringList(), i18n("Do not ask again"), nullptr, KMessageBox::NoExec, QString()); QObject::connect(buttonBox, SIGNAL(accepted()), q, SLOT(_k_slotCreateHiddenDirectory())); QObject::connect(buttonBox, &QDialogButtonBox::rejected, q, &KNewFileMenu::createDirectory); m_fileDialog = confirmDialog; confirmDialog->show(); } void KNewFileMenuPrivate::executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry) { if (!checkSourceExists(entry.templatePath)) { return; } QList::const_iterator it = m_popupFiles.constBegin(); for (; it != m_popupFiles.constEnd(); ++it) { QString text = entry.text; text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895 // KDE5 TODO: remove the "..." from link*.desktop files and use i18n("%1...") when making // the action. QString name = text; text.append(QStringLiteral(".desktop")); const QUrl directory = mostLocalUrl(*it); const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text)); if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) { text = KIO::suggestName(directory, text); } QUrl templateUrl; bool usingTemplate = false; if (entry.templatePath.startsWith(QLatin1String(":/"))) { QTemporaryFile *tmpFile = QTemporaryFile::createNativeFile(entry.templatePath); tmpFile->setAutoRemove(false); QString tempFileName = tmpFile->fileName(); tmpFile->close(); KDesktopFile df(tempFileName); KConfigGroup group = df.desktopGroup(); group.writeEntry("Name", name); templateUrl = QUrl::fromLocalFile(tempFileName); m_tempFileToDelete = tempFileName; usingTemplate = true; } else { templateUrl = QUrl::fromLocalFile(entry.templatePath); } QDialog *dlg = new KPropertiesDialog(templateUrl, directory, text, m_parentWidget); dlg->setModal(q->isModal()); dlg->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(dlg, SIGNAL(applied()), q, SLOT(_k_slotOtherDesktopFile())); if (usingTemplate) { QObject::connect(dlg, SIGNAL(propertiesClosed()), q, SLOT(_k_slotOtherDesktopFileClosed())); } dlg->show(); } // We don't set m_src here -> there will be no copy, we are done. } void KNewFileMenuPrivate::executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry) { // The template is not a desktop file // Show the small dialog for getting the destination filename QString text = entry.text; text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895 m_copyData.m_src = entry.templatePath; const QUrl directory = mostLocalUrl(m_popupFiles.first()); const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text)); if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) { text = KIO::suggestName(directory, text); } QDialog *fileDialog = new QDialog(m_parentWidget); fileDialog->setAttribute(Qt::WA_DeleteOnClose); fileDialog->setModal(q->isModal()); QVBoxLayout *layout = new QVBoxLayout; QLabel *label = new QLabel(entry.comment, fileDialog); QLineEdit *lineEdit = new QLineEdit(fileDialog); lineEdit->setClearButtonEnabled(true); lineEdit->setText(text); _k_slotTextChanged(text); QObject::connect(lineEdit, SIGNAL(textChanged(QString)), q, SLOT(_k_slotTextChanged(QString))); QDialogButtonBox *buttonBox = new QDialogButtonBox(fileDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QObject::connect(buttonBox, &QDialogButtonBox::accepted, fileDialog, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, fileDialog, &QDialog::reject); layout->addWidget(label); layout->addWidget(lineEdit); layout->addWidget(buttonBox); fileDialog->setLayout(layout); QObject::connect(fileDialog, SIGNAL(accepted()), q, SLOT(_k_slotRealFileOrDir())); QObject::connect(fileDialog, SIGNAL(rejected()), q, SLOT(_k_slotAbortDialog())); fileDialog->show(); lineEdit->selectAll(); lineEdit->setFocus(); } void KNewFileMenuPrivate::executeSymLink(const KNewFileMenuSingleton::Entry &entry) { KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("File name:"), entry.comment, m_popupFiles.first(), m_parentWidget); dlg->setModal(q->isModal()); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setWindowTitle(i18n("Create Symlink")); m_fileDialog = dlg; QObject::connect(dlg, SIGNAL(accepted()), q, SLOT(_k_slotSymLink())); dlg->show(); } void KNewFileMenuPrivate::executeStrategy() { m_tempFileToDelete = m_copyData.tempFileToDelete(); const QString src = m_copyData.sourceFileToCopy(); QString chosenFileName = expandTilde(m_copyData.chosenFileName(), true); if (src.isEmpty()) { return; } QUrl uSrc(QUrl::fromLocalFile(src)); // In case the templates/.source directory contains symlinks, resolve // them to the target files. Fixes bug #149628. KFileItem item(uSrc, QString(), KFileItem::Unknown); if (item.isLink()) { uSrc.setPath(item.linkDest()); } if (!m_copyData.m_isSymlink) { // If the file is not going to be detected as a desktop file, due to a // known extension (e.g. ".pl"), append ".desktop". #224142. QFile srcFile(uSrc.toLocalFile()); if (srcFile.open(QIODevice::ReadOnly)) { QMimeDatabase db; QMimeType wantedMime = db.mimeTypeForUrl(uSrc); QMimeType mime = db.mimeTypeForFileNameAndData(m_copyData.m_chosenFileName, srcFile.read(1024)); //qDebug() << "mime=" << mime->name() << "wantedMime=" << wantedMime->name(); if (!mime.inherits(wantedMime.name())) if (!wantedMime.preferredSuffix().isEmpty()) { chosenFileName += QLatin1Char('.') + wantedMime.preferredSuffix(); } } } // The template is not a desktop file [or it's a URL one] // Copy it. QList::const_iterator it = m_popupFiles.constBegin(); for (; it != m_popupFiles.constEnd(); ++it) { QUrl dest = *it; dest.setPath(concatPaths(dest.path(), KIO::encodeFileName(chosenFileName))); QList lstSrc; lstSrc.append(uSrc); KIO::Job *kjob; if (m_copyData.m_isSymlink) { KIO::CopyJob *linkJob = KIO::linkAs(uSrc, dest); kjob = linkJob; KIO::FileUndoManager::self()->recordCopyJob(linkJob); } else if (src.startsWith(QLatin1String(":/"))) { QFile srcFile(src); if (!srcFile.open(QIODevice::ReadOnly)) { return; } // The QFile won't live long enough for the job, so let's buffer the contents const QByteArray srcBuf(srcFile.readAll()); KIO::StoredTransferJob* putJob = KIO::storedPut(srcBuf, dest, -1); kjob = putJob; KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Put, QList(), dest, putJob); } else { //qDebug() << "KIO::copyAs(" << uSrc.url() << "," << dest.url() << ")"; KIO::CopyJob *job = KIO::copyAs(uSrc, dest); job->setDefaultPermissions(true); kjob = job; KIO::FileUndoManager::self()->recordCopyJob(job); } KJobWidgets::setWindow(kjob, m_parentWidget); QObject::connect(kjob, &KJob::result, q, &KNewFileMenu::slotResult); } } void KNewFileMenuPrivate::executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry) { KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("File name:"), entry.comment, m_popupFiles.first(), m_parentWidget); m_copyData.m_templatePath = entry.templatePath; dlg->setModal(q->isModal()); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setWindowTitle(i18n("Create link to URL")); m_fileDialog = dlg; QObject::connect(dlg, SIGNAL(accepted()), q, SLOT(_k_slotUrlDesktopFile())); dlg->show(); } void KNewFileMenuPrivate::fillMenu() { QMenu *menu = q->menu(); menu->clear(); m_menuDev->menu()->clear(); m_newDirAction = nullptr; QSet seenTexts; QString lastTemplatePath; // these shall be put at special positions QAction *linkURL = nullptr; QAction *linkApp = nullptr; QAction *linkPath = nullptr; KNewFileMenuSingleton *s = kNewMenuGlobals(); int i = 1; KNewFileMenuSingleton::EntryList::iterator templ = s->templatesList->begin(); const KNewFileMenuSingleton::EntryList::iterator templ_end = s->templatesList->end(); for (; templ != templ_end; ++templ, ++i) { KNewFileMenuSingleton::Entry &entry = *templ; if (entry.entryType != KNewFileMenuSingleton::Separator) { // There might be a .desktop for that one already, if it's a kdelnk // This assumes we read .desktop files before .kdelnk files ... // In fact, we skip any second item that has the same text as another one. // Duplicates in a menu look bad in any case. const bool bSkip = seenTexts.contains(entry.text); if (bSkip) { // qDebug() << "skipping" << entry.filePath; } else { seenTexts.insert(entry.text); //const KNewFileMenuSingleton::Entry entry = templatesList->at(i-1); const QString templatePath = entry.templatePath; // The best way to identify the "Create Directory", "Link to Location", "Link to Application" was the template if (templatePath.endsWith(QLatin1String("emptydir"))) { QAction *act = new QAction(q); m_newDirAction = act; act->setIcon(QIcon::fromTheme(entry.icon)); act->setText(i18nc("@item:inmenu Create New", "%1", entry.text)); act->setActionGroup(m_newMenuGroup); // If there is a shortcut available in the action collection, use it. QAction *act2 = m_actionCollection->action(QStringLiteral("create_dir")); if (act2) { act->setShortcuts(act2->shortcuts()); // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog. act->setShortcutContext(Qt::WidgetShortcut); // We also need to react to shortcut changes. QObject::connect(act2, &QAction::changed, act, [=]() { act->setShortcuts(act2->shortcuts()); }); } menu->addAction(act); menu->addSeparator(); } else { if (lastTemplatePath.startsWith(QDir::homePath()) && !templatePath.startsWith(QDir::homePath())) { menu->addSeparator(); } if (!m_supportedMimeTypes.isEmpty()) { bool keep = false; // We need to do mimetype filtering, for real files. const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__"); if (createSymlink) { keep = true; } else if (!KDesktopFile::isDesktopFile(entry.templatePath)) { // Determine mimetype on demand QMimeDatabase db; QMimeType mime; if (entry.mimeType.isEmpty()) { mime = db.mimeTypeForFile(entry.templatePath); //qDebug() << entry.templatePath << "is" << mime.name(); entry.mimeType = mime.name(); } else { mime = db.mimeTypeForName(entry.mimeType); } Q_FOREACH (const QString &supportedMime, m_supportedMimeTypes) { if (mime.inherits(supportedMime)) { keep = true; break; } } } if (!keep) { //qDebug() << "Not keeping" << entry.templatePath; continue; } } QAction *act = new QAction(q); act->setData(i); act->setIcon(QIcon::fromTheme(entry.icon)); act->setText(i18nc("@item:inmenu Create New", "%1", entry.text)); act->setActionGroup(m_newMenuGroup); //qDebug() << templatePath << entry.filePath; if (templatePath.endsWith(QLatin1String("/URL.desktop"))) { linkURL = act; } else if (templatePath.endsWith(QLatin1String("/Program.desktop"))) { linkApp = act; } else if (entry.filePath.endsWith(QLatin1String("/linkPath.desktop"))) { linkPath = act; } else if (KDesktopFile::isDesktopFile(templatePath)) { KDesktopFile df(templatePath); if (df.readType() == QLatin1String("FSDevice")) { m_menuDev->menu()->addAction(act); } else { menu->addAction(act); } } else { menu->addAction(act); } } } lastTemplatePath = entry.templatePath; } else { // Separate system from personal templates Q_ASSERT(entry.entryType != 0); menu->addSeparator(); } } if (m_supportedMimeTypes.isEmpty()) { menu->addSeparator(); if (linkURL) { menu->addAction(linkURL); } if (linkPath) { menu->addAction(linkPath); } if (linkApp) { menu->addAction(linkApp); } Q_ASSERT(m_menuDev); if (!m_menuDev->menu()->isEmpty()) { menu->addAction(m_menuDev); } } } QUrl KNewFileMenuPrivate::mostLocalUrl(const QUrl &url) { if (url.isLocalFile()) { return url; } KIO::StatJob *job = KIO::stat(url); KJobWidgets::setWindow(job, m_parentWidget); if (!job->exec()) { return url; } KIO::UDSEntry entry = job->statResult(); const QString path = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); return path.isEmpty() ? url : QUrl::fromLocalFile(path); } void KNewFileMenuPrivate::_k_slotAbortDialog() { m_text = QString(); } void KNewFileMenuPrivate::_k_slotActionTriggered(QAction *action) { q->trigger(); // was for kdesktop's slotNewMenuActivated() in kde3 times. Can't hurt to keep it... if (action == m_newDirAction) { q->createDirectory(); return; } const int id = action->data().toInt(); Q_ASSERT(id > 0); KNewFileMenuSingleton *s = kNewMenuGlobals(); const KNewFileMenuSingleton::Entry entry = s->templatesList->at(id - 1); const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__"); m_copyData = KNewFileMenuCopyData(); if (createSymlink) { m_copyData.m_isSymlink = true; executeSymLink(entry); } else if (KDesktopFile::isDesktopFile(entry.templatePath)) { KDesktopFile df(entry.templatePath); if (df.readType() == QLatin1String("Link")) { executeUrlDesktopFile(entry); } else { // any other desktop file (Device, App, etc.) executeOtherDesktopFile(entry); } } else { executeRealFileOrDir(entry); } } void KNewFileMenuPrivate::_k_slotCreateDirectory(bool writeHiddenDir) { QUrl url; QUrl baseUrl = m_popupFiles.first(); QString name = expandTilde(m_text); if (!name.isEmpty()) { if (QDir::isAbsolutePath(name)) { url = QUrl::fromLocalFile(name); } else { if (name == QLatin1String(".") || name == QLatin1String("..")) { KGuiItem enterNewNameGuiItem(KStandardGuiItem::ok()); enterNewNameGuiItem.setText(i18nc("@action:button", "Enter a Different Name")); enterNewNameGuiItem.setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); QDialog *confirmDialog = new QDialog(m_parentWidget); confirmDialog->setWindowTitle(i18n("Invalid Directory Name")); confirmDialog->setModal(m_modal); confirmDialog->setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(confirmDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), enterNewNameGuiItem); KMessageBox::createKMessageBox(confirmDialog, buttonBox, QMessageBox::Critical, xi18n("Could not create a folder with the name %1because it is reserved for use by the operating system.", name), QStringList(), QString(), nullptr, KMessageBox::NoExec, QString()); QObject::connect(buttonBox, &QDialogButtonBox::accepted, q, &KNewFileMenu::createDirectory); m_fileDialog = confirmDialog; confirmDialog->show(); _k_slotAbortDialog(); return; } if (!m_viewShowsHiddenFiles && name.startsWith(QLatin1Char('.'))) { if (!writeHiddenDir) { confirmCreatingHiddenDir(name); return; } } url = baseUrl; url.setPath(concatPaths(url.path(), name)); } } // Note that we use mkpath so that a/b/c works. // On the other hand it means that passing the name of a directory that already exists will do nothing. KIO::Job *job = KIO::mkpath(url, baseUrl); job->setProperty("mkpathUrl", url); KJobWidgets::setWindow(job, m_parentWidget); job->uiDelegate()->setAutoErrorHandlingEnabled(true); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Mkpath, QList(), url, job); if (job) { // We want the error handling to be done by slotResult so that subclasses can reimplement it job->uiDelegate()->setAutoErrorHandlingEnabled(false); QObject::connect(job, &KJob::result, q, &KNewFileMenu::slotResult); } _k_slotAbortDialog(); } void KNewFileMenuPrivate::_k_slotCreateHiddenDirectory() { _k_slotCreateDirectory(true); } struct EntryWithName { QString key; KNewFileMenuSingleton::Entry entry; }; void KNewFileMenuPrivate::_k_slotFillTemplates() { KNewFileMenuSingleton *s = kNewMenuGlobals(); //qDebug(); const QStringList qrcTemplates = { QStringLiteral(":/kio5/newfile-templates") }; QStringList installedTemplates = { QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("templates"), QStandardPaths::LocateDirectory) }; // Qt does not provide an easy way to receive the xdg dir for templates so we have to find it on our own #ifdef Q_OS_UNIX QString xdgUserDirs = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("user-dirs.dirs"), QStandardPaths::LocateFile); QFile xdgUserDirsFile(xdgUserDirs); if (!xdgUserDirs.isEmpty() && xdgUserDirsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in(&xdgUserDirsFile); while (!in.atEnd()) { QString line = in.readLine(); if (line.startsWith(QLatin1String("XDG_TEMPLATES_DIR="))) { QString xdgTemplates = line.mid(19, line.size()-20); xdgTemplates.replace(QStringLiteral("$HOME"), QDir::homePath()); QDir xdgTemplatesDir(xdgTemplates); if (xdgTemplatesDir.exists()) { installedTemplates << xdgTemplates; } break; } } } #endif const QStringList templates = qrcTemplates + installedTemplates; // Ensure any changes in the templates dir will call this if (! s->dirWatch) { s->dirWatch = new KDirWatch; for (const QString &dir : qAsConst(installedTemplates)) { s->dirWatch->addDir(dir); } QObject::connect(s->dirWatch, SIGNAL(dirty(QString)), q, SLOT(_k_slotFillTemplates())); QObject::connect(s->dirWatch, SIGNAL(created(QString)), q, SLOT(_k_slotFillTemplates())); QObject::connect(s->dirWatch, SIGNAL(deleted(QString)), q, SLOT(_k_slotFillTemplates())); // Ok, this doesn't cope with new dirs in XDG_DATA_DIRS, but that's another story } ++s->templatesVersion; s->filesParsed = false; s->templatesList->clear(); // Look into "templates" dirs. QStringList files; QDir dir; for (const QString &path : templates) { dir.setPath(path); const QStringList &entryList(dir.entryList(QStringList() << QStringLiteral("*.desktop"), QDir::Files)); Q_FOREACH (const QString &entry, entryList) { const QString file = concatPaths(dir.path(), entry); files.append(file); } } QMap slist; // used for sorting QMap ulist; // entries with unique URLs Q_FOREACH (const QString &file, files) { //qDebug() << file; if (file[0] != QLatin1Char('.')) { KNewFileMenuSingleton::Entry e; e.filePath = file; e.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet // Put Directory first in the list (a bit hacky), // and TextFile before others because it's the most used one. // This also sorts by user-visible name. // The rest of the re-ordering is done in fillMenu. const KDesktopFile config(file); QString url = config.desktopGroup().readEntry("URL"); QString key = config.desktopGroup().readEntry("Name"); if (file.endsWith(QLatin1String("Directory.desktop"))) { key.prepend(QLatin1Char('0')); } else if (file.startsWith(QDir::homePath())) { key.prepend(QLatin1Char('1')); } else if (file.endsWith(QLatin1String("TextFile.desktop"))) { key.prepend(QLatin1Char('2')); } else { key.prepend(QLatin1Char('3')); } EntryWithName en = { key, e }; if (ulist.contains(url)) { ulist.remove(url); } ulist.insert(url, en); } } QMap::iterator it = ulist.begin(); for (; it != ulist.end(); ++it) { EntryWithName ewn = *it; slist.insert(ewn.key, ewn.entry); } (*s->templatesList) += slist.values(); } void KNewFileMenuPrivate::_k_slotOtherDesktopFile() { // The properties dialog took care of the copying, so we're done KPropertiesDialog *dialog = qobject_cast(q->sender()); emit q->fileCreated(dialog->url()); } void KNewFileMenuPrivate::_k_slotOtherDesktopFileClosed() { QFile::remove(m_tempFileToDelete); } void KNewFileMenuPrivate::_k_slotRealFileOrDir() { m_copyData.m_chosenFileName = m_text; _k_slotAbortDialog(); executeStrategy(); } void KNewFileMenuPrivate::_k_slotSymLink() { KNameAndUrlInputDialog *dlg = static_cast(m_fileDialog); m_copyData.m_chosenFileName = dlg->name(); // no path const QString linkTarget = dlg->urlText(); if (m_copyData.m_chosenFileName.isEmpty() || linkTarget.isEmpty()) { return; } m_copyData.m_src = linkTarget; executeStrategy(); } void KNewFileMenuPrivate::_k_slotTextChanged(const QString &text) { m_text = text; } void KNewFileMenuPrivate::_k_slotUrlDesktopFile() { KNameAndUrlInputDialog *dlg = static_cast(m_fileDialog); m_copyData.m_chosenFileName = dlg->name(); // no path QUrl linkUrl = dlg->url(); // Filter user input so that short uri entries, e.g. www.kde.org, are // handled properly. This not only makes the icon detection below work // properly, but opening the URL link where the short uri will not be // sent to the application (opening such link Konqueror fails). KUriFilterData uriData; uriData.setData(linkUrl); // the url to put in the file uriData.setCheckForExecutables(false); if (KUriFilter::self()->filterUri(uriData, QStringList() << QStringLiteral("kshorturifilter"))) { linkUrl = uriData.uri(); } if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty()) { return; } // It's a "URL" desktop file; we need to make a temp copy of it, to modify it // before copying it to the final destination [which could be a remote protocol] QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // done below if (!tmpFile.open()) { qCritical() << "Couldn't create temp file!"; return; } if (!checkSourceExists(m_copyData.m_templatePath)) { return; } // First copy the template into the temp file QFile file(m_copyData.m_templatePath); if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Couldn't open template" << m_copyData.m_templatePath; return; } const QByteArray data = file.readAll(); tmpFile.write(data); const QString tempFileName = tmpFile.fileName(); Q_ASSERT(!tempFileName.isEmpty()); tmpFile.close(); file.close(); KDesktopFile df(tempFileName); KConfigGroup group = df.desktopGroup(); group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.scheme())); group.writePathEntry("URL", linkUrl.toDisplayString()); df.sync(); m_copyData.m_src = tempFileName; m_copyData.m_tempFileToDelete = tempFileName; executeStrategy(); } KNewFileMenu::KNewFileMenu(KActionCollection *collection, const QString &name, QObject *parent) : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent), d(new KNewFileMenuPrivate(this)) { // Don't fill the menu yet // We'll do that in checkUpToDate (should be connected to aboutToShow) d->m_newMenuGroup = new QActionGroup(this); connect(d->m_newMenuGroup, SIGNAL(triggered(QAction*)), this, SLOT(_k_slotActionTriggered(QAction*))); d->m_actionCollection = collection; d->m_parentWidget = qobject_cast(parent); d->m_newDirAction = nullptr; if (d->m_actionCollection) { d->m_actionCollection->addAction(name, this); } d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this); } KNewFileMenu::~KNewFileMenu() { //qDebug() << this; delete d; } void KNewFileMenu::checkUpToDate() { KNewFileMenuSingleton *s = kNewMenuGlobals(); //qDebug() << this << "m_menuItemsVersion=" << d->m_menuItemsVersion // << "s->templatesVersion=" << s->templatesVersion; if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) { //qDebug() << "recreating actions"; // We need to clean up the action collection // We look for our actions using the group foreach (QAction *action, d->m_newMenuGroup->actions()) { delete action; } if (!s->templatesList) { // No templates list up to now s->templatesList = new KNewFileMenuSingleton::EntryList; d->_k_slotFillTemplates(); s->parseFiles(); } // This might have been already done for other popupmenus, // that's the point in s->filesParsed. if (!s->filesParsed) { s->parseFiles(); } d->fillMenu(); d->m_menuItemsVersion = s->templatesVersion; } } void KNewFileMenu::createDirectory() { if (d->m_popupFiles.isEmpty()) { return; } QUrl baseUrl = d->m_popupFiles.first(); KIO::StatJob *job = KIO::mostLocalUrl(baseUrl); if (job->exec()) { baseUrl = job->mostLocalUrl(); } QString name = d->m_text.isEmpty() ? i18nc("Default name for a new folder", "New Folder") : d->m_text; if (baseUrl.isLocalFile() && QFileInfo::exists(baseUrl.toLocalFile() + QLatin1Char('/') + name)) { name = KIO::suggestName(baseUrl, name); } QDialog *fileDialog = new QDialog(d->m_parentWidget); fileDialog->setModal(isModal()); fileDialog->setAttribute(Qt::WA_DeleteOnClose); fileDialog->setWindowTitle(i18nc("@title:window", "New Folder")); QVBoxLayout *layout = new QVBoxLayout; QLabel *label = new QLabel(i18n("Create new folder in:\n%1", baseUrl.toDisplayString(QUrl::PreferLocalFile)), fileDialog); QLineEdit *lineEdit = new QLineEdit(fileDialog); lineEdit->setClearButtonEnabled(true); lineEdit->setText(name); d->_k_slotTextChanged(name); // have to save string in d->m_text in case user does not touch dialog connect(lineEdit, SIGNAL(textChanged(QString)), this, SLOT(_k_slotTextChanged(QString))); QDialogButtonBox *buttonBox = new QDialogButtonBox(fileDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QObject::connect(buttonBox, &QDialogButtonBox::accepted, fileDialog, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, fileDialog, &QDialog::reject); layout->addWidget(label); layout->addWidget(lineEdit); layout->addWidget(buttonBox); fileDialog->setLayout(layout); connect(fileDialog, SIGNAL(accepted()), this, SLOT(_k_slotCreateDirectory())); connect(fileDialog, SIGNAL(rejected()), this, SLOT(_k_slotAbortDialog())); d->m_fileDialog = fileDialog; fileDialog->show(); lineEdit->selectAll(); lineEdit->setFocus(); } bool KNewFileMenu::isModal() const { return d->m_modal; } QList KNewFileMenu::popupFiles() const { return d->m_popupFiles; } void KNewFileMenu::setModal(bool modal) { d->m_modal = modal; } void KNewFileMenu::setPopupFiles(const QList &files) { d->m_popupFiles = files; if (files.isEmpty()) { d->m_newMenuGroup->setEnabled(false); } else { QUrl firstUrl = files.first(); if (KProtocolManager::supportsWriting(firstUrl)) { d->m_newMenuGroup->setEnabled(true); if (d->m_newDirAction) { d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(firstUrl)); // e.g. trash:/ } } else { d->m_newMenuGroup->setEnabled(true); } } } void KNewFileMenu::setParentWidget(QWidget *parentWidget) { d->m_parentWidget = parentWidget; } void KNewFileMenu::setSupportedMimeTypes(const QStringList &mime) { d->m_supportedMimeTypes = mime; } void KNewFileMenu::setViewShowsHiddenFiles(bool b) { d->m_viewShowsHiddenFiles = b; } void KNewFileMenu::slotResult(KJob *job) { if (job->error()) { static_cast(job)->uiDelegate()->showErrorMessage(); } else { // Was this a copy or a mkdir? KIO::CopyJob *copyJob = ::qobject_cast(job); if (copyJob) { const QUrl destUrl = copyJob->destUrl(); const QUrl localUrl = d->mostLocalUrl(destUrl); if (localUrl.isLocalFile()) { // Normal (local) file. Need to "touch" it, kio_file copied the mtime. (void) ::utime(QFile::encodeName(localUrl.toLocalFile()).constData(), nullptr); } emit fileCreated(destUrl); } else if (KIO::SimpleJob *simpleJob = ::qobject_cast(job)) { // Called in the storedPut() case org::kde::KDirNotify::emitFilesAdded(simpleJob->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); emit fileCreated(simpleJob->url()); } else { // Can be mkdir QUrl mkpathUrl = job->property("mkpathUrl").toUrl(); if (mkpathUrl.isValid()) { emit directoryCreated(mkpathUrl); } else { qWarning() << "Neither copy, put nor mkdir, internal error"; } } } if (!d->m_tempFileToDelete.isEmpty()) { QFile::remove(d->m_tempFileToDelete); } } QStringList KNewFileMenu::supportedMimeTypes() const { return d->m_supportedMimeTypes; } #include "moc_knewfilemenu.cpp" diff --git a/src/ioslaves/http/http.cpp b/src/ioslaves/http/http.cpp index 4e370ca2..d3b91a84 100644 --- a/src/ioslaves/http/http.cpp +++ b/src/ioslaves/http/http.cpp @@ -1,5628 +1,5628 @@ /* Copyright (C) 2000-2003 Waldo Bastian Copyright (C) 2000-2002 George Staikos Copyright (C) 2000-2002 Dawit Alemayehu Copyright (C) 2001,2002 Hamish Rodda Copyright (C) 2007 Nick Shaforostoff Copyright (C) 2007-2018 Daniel Nicoletti Copyright (C) 2008,2009 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO delete / do not save very big files; "very big" to be defined #include "http.h" #include #include // must be explicitly included for MacOSX #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "httpauthentication.h" #include "kioglobal_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_HTTP) Q_LOGGING_CATEGORY(KIO_HTTP, "kf5.kio.kio_http", QtWarningMsg) // disable debug by default // HeaderTokenizer declarations #include "parsinghelpers.h" //string parsing helpers and HeaderTokenizer implementation #include "parsinghelpers.cpp" // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.http" FILE "http.json") }; static bool supportedProxyScheme(const QString &scheme) { // scheme is supposed to be lowercase return (scheme.startsWith(QLatin1String("http")) || scheme == QLatin1String("socks")); } // see filenameFromUrl(): a sha1 hash is 160 bits static const int s_hashedUrlBits = 160; // this number should always be divisible by eight static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; static const int s_MaxInMemPostBufSize = 256 * 1024; // Write anything over 256 KB to file... using namespace KIO; extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier app.setApplicationName(QStringLiteral("kio_http")); if (argc != 4) { fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n"); exit(-1); } HTTPProtocol slave(argv[1], argv[2], argv[3]); slave.dispatchLoop(); return 0; } /*********************************** Generic utility functions ********************/ static QString toQString(const QByteArray &value) { return QString::fromLatin1(value.constData(), value.size()); } static bool isCrossDomainRequest(const QString &fqdn, const QString &originURL) { //TODO read the RFC if (originURL == QLatin1String("true")) { // Backwards compatibility return true; } QUrl url(originURL); // Document Origin domain QString a = url.host(); // Current request domain QString b = fqdn; if (a == b) { return false; } QStringList la = a.split(QLatin1Char('.'), QString::SkipEmptyParts); QStringList lb = b.split(QLatin1Char('.'), QString::SkipEmptyParts); if (qMin(la.count(), lb.count()) < 2) { return true; // better safe than sorry... } while (la.count() > 2) { la.pop_front(); } while (lb.count() > 2) { lb.pop_front(); } return la != lb; } /* Eliminates any custom header that could potentially alter the request */ static QString sanitizeCustomHTTPHeader(const QString &_header) { QString sanitizedHeaders; const QStringList headers = _header.split(QRegExp(QStringLiteral("[\r\n]"))); for (QStringList::ConstIterator it = headers.begin(); it != headers.end(); ++it) { // Do not allow Request line to be specified and ignore // the other HTTP headers. if (!(*it).contains(QLatin1Char(':')) || (*it).startsWith(QLatin1String("host"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("via"), Qt::CaseInsensitive)) { continue; } sanitizedHeaders += (*it) + QLatin1String("\r\n"); } sanitizedHeaders.chop(2); return sanitizedHeaders; } static bool isPotentialSpoofingAttack(const HTTPProtocol::HTTPRequest &request, const KConfigGroup *config) { qCDebug(KIO_HTTP) << request.url << "response code: " << request.responseCode << "previous response code:" << request.prevResponseCode; if (config->readEntry("no-spoof-check", false)) { return false; } if (request.url.userName().isEmpty()) { return false; } // We already have cached authentication. if (config->readEntry(QStringLiteral("cached-www-auth"), false)) { return false; } const QString userName = config->readEntry(QStringLiteral("LastSpoofedUserName"), QString()); return ((userName.isEmpty() || userName != request.url.userName()) && request.responseCode != 401 && request.prevResponseCode != 401); } // for a given response code, conclude if the response is going to/likely to have a response body static bool canHaveResponseBody(int responseCode, KIO::HTTP_METHOD method) { /* RFC 2616 says... 1xx: false 200: method HEAD: false, otherwise:true 201: true 202: true 203: see 200 204: false 205: false 206: true 300: see 200 301: see 200 302: see 200 303: see 200 304: false 305: probably like 300, RFC seems to expect disconnection afterwards... 306: (reserved), for simplicity do it just like 200 307: see 200 4xx: see 200 5xx :see 200 */ if (responseCode >= 100 && responseCode < 200) { return false; } switch (responseCode) { case 201: case 202: case 206: // RFC 2616 does not mention HEAD in the description of the above. if the assert turns out // to be a problem the response code should probably be treated just like 200 and friends. Q_ASSERT(method != HTTP_HEAD); return true; case 204: case 205: case 304: return false; default: break; } // safe (and for most remaining response codes exactly correct) default return method != HTTP_HEAD; } static bool isEncryptedHttpVariety(const QByteArray &p) { return p == "https" || p == "webdavs"; } static bool isValidProxy(const QUrl &u) { return u.isValid() && !u.host().isEmpty(); } static bool isHttpProxy(const QUrl &u) { return isValidProxy(u) && u.scheme() == QLatin1String("http"); } static QIODevice *createPostBufferDeviceFor(KIO::filesize_t size) { QIODevice *device; if (size > static_cast(s_MaxInMemPostBufSize)) { device = new QTemporaryFile; } else { device = new QBuffer; } if (!device->open(QIODevice::ReadWrite)) { return nullptr; } return device; } QByteArray HTTPProtocol::HTTPRequest::methodString() const { if (!methodStringOverride.isEmpty()) { return (methodStringOverride).toLatin1(); } switch (method) { case HTTP_GET: return "GET"; case HTTP_PUT: return "PUT"; case HTTP_POST: return "POST"; case HTTP_HEAD: return "HEAD"; case HTTP_DELETE: return "DELETE"; case HTTP_OPTIONS: return "OPTIONS"; case DAV_PROPFIND: return "PROPFIND"; case DAV_PROPPATCH: return "PROPPATCH"; case DAV_MKCOL: return "MKCOL"; case DAV_COPY: return "COPY"; case DAV_MOVE: return "MOVE"; case DAV_LOCK: return "LOCK"; case DAV_UNLOCK: return "UNLOCK"; case DAV_SEARCH: return "SEARCH"; case DAV_SUBSCRIBE: return "SUBSCRIBE"; case DAV_UNSUBSCRIBE: return "UNSUBSCRIBE"; case DAV_POLL: return "POLL"; case DAV_NOTIFY: return "NOTIFY"; case DAV_REPORT: return "REPORT"; default: Q_ASSERT(false); return QByteArray(); } } static QString formatHttpDate(const QDateTime &date) { return QLocale::c().toString(date, QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'")); } static bool isAuthenticationRequired(int responseCode) { return (responseCode == 401) || (responseCode == 407); } static void changeProtocolToHttp(QUrl* url) { const QString protocol(url->scheme()); if (protocol == QLatin1String("webdavs")) { url->setScheme(QStringLiteral("https")); } else if (protocol == QLatin1String("webdav")) { url->setScheme(QStringLiteral("http")); } } #define NO_SIZE ((KIO::filesize_t) -1) #if HAVE_STRTOLL #define STRTOLL strtoll #else #define STRTOLL strtol #endif /************************************** HTTPProtocol **********************************************/ HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) : TCPSlaveBase(protocol, pool, app, isEncryptedHttpVariety(protocol)) , m_iSize(NO_SIZE) , m_iPostDataSize(NO_SIZE) , m_isBusy(false) , m_POSTbuf(nullptr) , m_maxCacheAge(DEFAULT_MAX_CACHE_AGE) , m_maxCacheSize(DEFAULT_MAX_CACHE_SIZE) , m_protocol(protocol) , m_wwwAuth(nullptr) , m_triedWwwCredentials(NoCredentials) , m_proxyAuth(nullptr) , m_triedProxyCredentials(NoCredentials) , m_socketProxyAuth(nullptr) , m_networkConfig(nullptr) , m_kioError(0) , m_isLoadingErrorPage(false) , m_remoteRespTimeout(DEFAULT_RESPONSE_TIMEOUT) , m_iEOFRetryCount(0) { reparseConfiguration(); setBlocking(true); connect(socket(), SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), this, SLOT(proxyAuthenticationForSocket(QNetworkProxy,QAuthenticator*))); } HTTPProtocol::~HTTPProtocol() { httpClose(false); } void HTTPProtocol::reparseConfiguration() { qCDebug(KIO_HTTP); delete m_proxyAuth; delete m_wwwAuth; m_proxyAuth = nullptr; m_wwwAuth = nullptr; m_request.proxyUrl.clear(); //TODO revisit m_request.proxyUrls.clear(); TCPSlaveBase::reparseConfiguration(); } void HTTPProtocol::resetConnectionSettings() { m_isEOF = false; m_kioError = 0; m_isLoadingErrorPage = false; } quint16 HTTPProtocol::defaultPort() const { return isEncryptedHttpVariety(m_protocol) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT; } void HTTPProtocol::resetResponseParsing() { m_isRedirection = false; m_isChunked = false; m_iSize = NO_SIZE; clearUnreadBuffer(); m_responseHeaders.clear(); m_contentEncodings.clear(); m_transferEncodings.clear(); m_contentMD5.clear(); m_mimeType.clear(); setMetaData(QStringLiteral("request-id"), m_request.id); } void HTTPProtocol::resetSessionSettings() { // Follow HTTP/1.1 spec and enable keep-alive by default // unless the remote side tells us otherwise or we determine // the persistent link has been terminated by the remote end. m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; m_request.redirectUrl = QUrl(); m_request.useCookieJar = config()->readEntry("Cookies", false); m_request.cacheTag.useCache = config()->readEntry("UseCache", true); m_request.preferErrorPage = config()->readEntry("errorPage", true); const bool noAuth = config()->readEntry("no-auth", false); m_request.doNotWWWAuthenticate = config()->readEntry("no-www-auth", noAuth); m_request.doNotProxyAuthenticate = config()->readEntry("no-proxy-auth", noAuth); m_strCacheDir = config()->readPathEntry("CacheDir", QString()); m_maxCacheAge = config()->readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); m_request.windowId = config()->readEntry("window-id"); m_request.methodStringOverride = metaData(QStringLiteral("CustomHTTPMethod")); m_request.sentMethodString.clear(); qCDebug(KIO_HTTP) << "Window Id =" << m_request.windowId; qCDebug(KIO_HTTP) << "ssl_was_in_use =" << metaData(QStringLiteral("ssl_was_in_use")); m_request.referrer.clear(); // RFC 2616: do not send the referrer if the referrer page was served using SSL and // the current page does not use SSL. if (config()->readEntry("SendReferrer", true) && (isEncryptedHttpVariety(m_protocol) || metaData(QStringLiteral("ssl_was_in_use")) != QLatin1String("TRUE"))) { QUrl refUrl(metaData(QStringLiteral("referrer"))); if (refUrl.isValid()) { // Sanitize QString protocol = refUrl.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, 6, QStringLiteral("http")); refUrl.setScheme(protocol); } if (protocol.startsWith(QLatin1String("http"))) { m_request.referrer = toQString(refUrl.toEncoded(QUrl::RemoveUserInfo | QUrl::RemoveFragment)); } } } if (config()->readEntry("SendLanguageSettings", true)) { m_request.charsets = config()->readEntry("Charsets", DEFAULT_PARTIAL_CHARSET_HEADER); if (!m_request.charsets.contains(QLatin1String("*;"), Qt::CaseInsensitive)) { m_request.charsets += QLatin1String(",*;q=0.5"); } m_request.languages = config()->readEntry("Languages", DEFAULT_LANGUAGE_HEADER); } else { m_request.charsets.clear(); m_request.languages.clear(); } // Adjust the offset value based on the "range-start" meta-data. QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { m_request.offset = resumeOffset.toULongLong(); } else { m_request.offset = 0; } // Same procedure for endoffset. QString resumeEndOffset = metaData(QStringLiteral("range-end")); if (resumeEndOffset.isEmpty()) { resumeEndOffset = metaData(QStringLiteral("resume_until")); // old name } if (!resumeEndOffset.isEmpty()) { m_request.endoffset = resumeEndOffset.toULongLong(); } else { m_request.endoffset = 0; } m_request.disablePassDialog = config()->readEntry("DisablePassDlg", false); m_request.allowTransferCompression = config()->readEntry("AllowCompressedPage", true); m_request.id = metaData(QStringLiteral("request-id")); // Store user agent for this host. if (config()->readEntry("SendUserAgent", true)) { m_request.userAgent = metaData(QStringLiteral("UserAgent")); } else { m_request.userAgent.clear(); } m_request.cacheTag.etag.clear(); m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); m_request.responseCode = 0; m_request.prevResponseCode = 0; delete m_wwwAuth; m_wwwAuth = nullptr; delete m_socketProxyAuth; m_socketProxyAuth = nullptr; m_blacklistedWwwAuthMethods.clear(); m_triedWwwCredentials = NoCredentials; m_blacklistedProxyAuthMethods.clear(); m_triedProxyCredentials = NoCredentials; // Obtain timeout values m_remoteRespTimeout = responseTimeout(); // Bounce back the actual referrer sent setMetaData(QStringLiteral("referrer"), m_request.referrer); // Reset the post data size m_iPostDataSize = NO_SIZE; // Reset the EOF retry counter m_iEOFRetryCount = 0; } void HTTPProtocol::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) { // Reset the webdav-capable flags for this host if (m_request.url.host() != host) { m_davHostOk = m_davHostUnsupported = false; } m_request.url.setHost(host); // is it an IPv6 address? if (host.indexOf(QLatin1Char(':')) == -1) { m_request.encoded_hostname = toQString(QUrl::toAce(host)); } else { int pos = host.indexOf(QLatin1Char('%')); if (pos == -1) { m_request.encoded_hostname = QLatin1Char('[') + host + QLatin1Char(']'); } else // don't send the scope-id in IPv6 addresses to the server { - m_request.encoded_hostname = QLatin1Char('[') + host.left(pos) + QLatin1Char(']'); + m_request.encoded_hostname = QLatin1Char('[') + host.leftRef(pos) + QLatin1Char(']'); } } m_request.url.setPort((port > 0 && port != defaultPort()) ? port : -1); m_request.url.setUserName(user); m_request.url.setPassword(pass); // On new connection always clear previous proxy information... m_request.proxyUrl.clear(); m_request.proxyUrls.clear(); qCDebug(KIO_HTTP) << "Hostname is now:" << m_request.url.host() << "(" << m_request.encoded_hostname << ")"; } bool HTTPProtocol::maybeSetRequestUrl(const QUrl &u) { qCDebug(KIO_HTTP) << u; m_request.url = u; m_request.url.setPort(u.port(defaultPort()) != defaultPort() ? u.port() : -1); if (u.host().isEmpty()) { error(KIO::ERR_UNKNOWN_HOST, i18n("No host specified.")); return false; } if (u.path().isEmpty()) { QUrl newUrl(u); newUrl.setPath(QStringLiteral("/")); redirection(newUrl); finished(); return false; } return true; } void HTTPProtocol::proceedUntilResponseContent(bool dataInternal /* = false */) { qCDebug(KIO_HTTP); const bool status = proceedUntilResponseHeader() && readBody(dataInternal || m_kioError); // If not an error condition or internal request, close // the connection based on the keep alive settings... if (!m_kioError && !dataInternal) { httpClose(m_request.isKeepAlive); } // if data is required internally or we got error, don't finish, // it is processed before we finish() if (dataInternal || !status) { return; } if (!sendHttpError()) { finished(); } } bool HTTPProtocol::proceedUntilResponseHeader() { qCDebug(KIO_HTTP); // Retry the request until it succeeds or an unrecoverable error occurs. // Recoverable errors are, for example: // - Proxy or server authentication required: Ask for credentials and try again, // this time with an authorization header in the request. // - Server-initiated timeout on keep-alive connection: Reconnect and try again while (true) { if (!sendQuery()) { return false; } if (readResponseHeader()) { // Success, finish the request. break; } // If not loading error page and the response code requires us to resend the query, // then throw away any error message that might have been sent by the server. if (!m_isLoadingErrorPage && isAuthenticationRequired(m_request.responseCode)) { // This gets rid of any error page sent with 401 or 407 authentication required response... readBody(true); } // no success, close the cache file so the cache state is reset - that way most other code // doesn't have to deal with the cache being in various states. cacheFileClose(); if (m_kioError || m_isLoadingErrorPage) { // Unrecoverable error, abort everything. // Also, if we've just loaded an error page there is nothing more to do. // In that case we abort to avoid loops; some webservers manage to send 401 and // no authentication request. Or an auth request we don't understand. setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); return false; } if (!m_request.isKeepAlive) { httpCloseConnection(); m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; } } // Do not save authorization if the current response code is // 4xx (client error) or 5xx (server error). qCDebug(KIO_HTTP) << "Previous Response:" << m_request.prevResponseCode; qCDebug(KIO_HTTP) << "Current Response:" << m_request.responseCode; setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); setMetaData(QStringLiteral("content-type"), m_mimeType); // At this point sendBody() should have delivered any POST data. clearPostDataBuffer(); return true; } void HTTPProtocol::stat(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); if (m_protocol != "webdav" && m_protocol != "webdavs") { QString statSide = metaData(QStringLiteral("statSide")); if (statSide != QLatin1String("source")) { // When uploading we assume the file doesn't exit error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } // When downloading we assume it exists UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, url.fileName()); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody statEntry(entry); finished(); return; } davStatList(url); } void HTTPProtocol::listDir(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url, false); } void HTTPProtocol::davSetRequest(const QByteArray &requestXML) { // insert the document into the POST buffer, kill trailing zero byte cachePostData(requestXML); } void HTTPProtocol::davStatList(const QUrl &url, bool stat) { UDSEntry entry; // check to make sure this host supports WebDAV if (!davHostOk()) { return; } QMimeDatabase db; // Maybe it's a disguised SEARCH... QString query = metaData(QStringLiteral("davSearchQuery")); if (!query.isEmpty()) { QByteArray request = "\r\n"; request.append("\r\n"); request.append(query.toUtf8()); request.append("\r\n"); davSetRequest(request); } else { // We are only after certain features... QByteArray request; request = "" ""; // insert additional XML request from the davRequestResponse metadata if (hasMetaData(QStringLiteral("davRequestResponse"))) { request += metaData(QStringLiteral("davRequestResponse")).toUtf8(); } else { // No special request, ask for default properties request += "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } request += ""; davSetRequest(request); } // WebDAV Stat or List... m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = stat ? 0 : 1; if (!stat) { if (!m_request.url.path().endsWith(QLatin1Char('/'))) { m_request.url.setPath(m_request.url.path() + QLatin1Char('/')); } } proceedUntilResponseContent(true); infoMessage(QLatin1String("")); // Has a redirection already been called? If so, we're done. if (m_isRedirection || m_kioError) { if (m_isRedirection) { davFinished(); } return; } QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); bool hasResponse = false; qCDebug(KIO_HTTP) << endl << multiResponse.toString(2); for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement thisResponse = n.toElement(); if (thisResponse.isNull()) { continue; } hasResponse = true; QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { entry.clear(); const QUrl thisURL(href.text()); // href.text() is a percent-encoded url. if (thisURL.isValid()) { const QUrl adjustedThisURL = thisURL.adjusted(QUrl::StripTrailingSlash); const QUrl adjustedUrl = url.adjusted(QUrl::StripTrailingSlash); // base dir of a listDir(): name should be "." QString name; if (!stat && adjustedThisURL.path() == adjustedUrl.path()) { name = QLatin1Char('.'); } else { name = adjustedThisURL.fileName(); } entry.fastInsert(KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name); } QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat")); davParsePropstats(propstats, entry); // Since a lot of webdav servers seem not to send the content-type information // for the requested directory listings, we attempt to guess the mime-type from // the resource name so long as the resource is not a directory. if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) { QMimeType mime = db.mimeTypeForFile(thisURL.path(), QMimeDatabase::MatchExtension); if (mime.isValid() && !mime.isDefault()) { qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed mime type for" << thisURL.path(); entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime.name()); } } if (stat) { // return an item statEntry(entry); davFinished(); return; } listEntry(entry); } else { qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url; } } if (stat || !hasResponse) { error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } davFinished(); } void HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // WebDAV method m_request.method = method; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } int HTTPProtocol::codeFromResponse(const QString &response) { const int firstSpace = response.indexOf(QLatin1Char(' ')); const int secondSpace = response.indexOf(QLatin1Char(' '), firstSpace + 1); return response.midRef(firstSpace + 1, secondSpace - firstSpace - 1).toInt(); } void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, UDSEntry &entry) { QString mimeType; bool foundExecutable = false; bool isDirectory = false; uint lockCount = 0; uint supportedLockCount = 0; qlonglong quotaUsed = -1; qlonglong quotaAvailable = -1; for (int i = 0; i < propstats.count(); i++) { QDomElement propstat = propstats.item(i).toElement(); QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement(); if (status.isNull()) { // error, no status code in this propstat qCDebug(KIO_HTTP) << "Error, no status code in this propstat"; return; } int code = codeFromResponse(status.text()); if (code != 200) { qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)"; continue; } QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement(); if (prop.isNull()) { qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat."; return; } if (hasMetaData(QStringLiteral("davRequestResponse"))) { QDomDocument doc; doc.appendChild(prop); entry.replace(KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString()); } for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement property = n.toElement(); if (property.isNull()) { continue; } if (property.namespaceURI() != QLatin1String("DAV:")) { // break out - we're only interested in properties from the DAV namespace continue; } if (property.tagName() == QLatin1String("creationdate")) { // Resource creation date. Should be is ISO 8601 format. entry.replace(KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toTime_t()); } else if (property.tagName() == QLatin1String("getcontentlength")) { // Content length (file size) entry.replace(KIO::UDSEntry::UDS_SIZE, property.text().toULong()); } else if (property.tagName() == QLatin1String("displayname")) { // Name suitable for presentation to the user setMetaData(QStringLiteral("davDisplayName"), property.text()); } else if (property.tagName() == QLatin1String("source")) { // Source template location QDomElement source = property.namedItem(QStringLiteral("link")).toElement() .namedItem(QStringLiteral("dst")).toElement(); if (!source.isNull()) { setMetaData(QStringLiteral("davSource"), source.text()); } } else if (property.tagName() == QLatin1String("getcontentlanguage")) { // equiv. to Content-Language header on a GET setMetaData(QStringLiteral("davContentLanguage"), property.text()); } else if (property.tagName() == QLatin1String("getcontenttype")) { // Content type (mime type) // This may require adjustments for other server-side webdav implementations // (tested with Apache + mod_dav 1.0.3) if (property.text() == QLatin1String("httpd/unix-directory")) { isDirectory = true; } else { mimeType = property.text(); } } else if (property.tagName() == QLatin1String("executable")) { // File executable status if (property.text() == QLatin1String("T")) { foundExecutable = true; } } else if (property.tagName() == QLatin1String("getlastmodified")) { // Last modification date entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toTime_t()); } else if (property.tagName() == QLatin1String("getetag")) { // Entity tag setMetaData(QStringLiteral("davEntityTag"), property.text()); } else if (property.tagName() == QLatin1String("supportedlock")) { // Supported locking specifications for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { QDomElement lockEntry = n2.toElement(); if (lockEntry.tagName() == QLatin1String("lockentry")) { QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement(); QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement(); if (!lockScope.isNull() && !lockType.isNull()) { // Lock type was properly specified supportedLockCount++; const QString lockCountStr = QString::number(supportedLockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); setMetaData(QLatin1String("davSupportedLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davSupportedLockType") + lockCountStr, type); } } } } else if (property.tagName() == QLatin1String("lockdiscovery")) { // Lists the available locks davParseActiveLocks(property.elementsByTagName(QStringLiteral("activelock")), lockCount); } else if (property.tagName() == QLatin1String("resourcetype")) { // Resource type. "Specifies the nature of the resource." if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) { // This is a collection (directory) isDirectory = true; } } else if (property.tagName() == QLatin1String("quota-used-bytes")) { // Quota-used-bytes. "Contains the amount of storage already in use." quotaUsed = property.text().toLongLong(); } else if (property.tagName() == QLatin1String("quota-available-bytes")) { // Quota-available-bytes. "Indicates the maximum amount of additional storage available." quotaAvailable = property.text().toLongLong(); } else { qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName(); } } } setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); setMetaData(QStringLiteral("davSupportedLockCount"), QString::number(supportedLockCount)); entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG); if (foundExecutable || isDirectory) { // File was executable, or is a directory. entry.replace(KIO::UDSEntry::UDS_ACCESS, 0700); } else { entry.replace(KIO::UDSEntry::UDS_ACCESS, 0600); } if (!isDirectory && !mimeType.isEmpty()) { entry.replace(KIO::UDSEntry::UDS_MIME_TYPE, mimeType); } if (quotaUsed >= 0 && quotaAvailable >= 0) { // Only used and available storage properties exist, the total storage size has to be calculated. setMetaData(QStringLiteral("total"), QString::number(quotaUsed + quotaAvailable)); setMetaData(QStringLiteral("available"), QString::number(quotaAvailable)); } } void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount) { for (int i = 0; i < activeLocks.count(); i++) { const QDomElement activeLock = activeLocks.item(i).toElement(); lockCount++; // required const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement(); const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement(); const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement(); // optional const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement(); const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement(); const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement(); if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) { // lock was properly specified lockCount++; const QString lockCountStr = QString::number(lockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); const QString depth = lockDepth.text(); setMetaData(QLatin1String("davLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davLockType") + lockCountStr, type); setMetaData(QLatin1String("davLockDepth") + lockCountStr, depth); if (!lockOwner.isNull()) { setMetaData(QLatin1String("davLockOwner") + lockCountStr, lockOwner.text()); } if (!lockTimeout.isNull()) { setMetaData(QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text()); } if (!lockToken.isNull()) { QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement(); if (!tokenVal.isNull()) { setMetaData(QLatin1String("davLockToken") + lockCountStr, tokenVal.text()); } } } } } QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type) { if (type == QLatin1String("dateTime.tz")) { return QDateTime::fromString(input, Qt::ISODate); } else if (type == QLatin1String("dateTime.rfc1123")) { return QDateTime::fromString(input, Qt::RFC2822Date); } // format not advertised... try to parse anyway QDateTime time = QDateTime::fromString(input, Qt::RFC2822Date); if (time.isValid()) { return time; } return QDateTime::fromString(input, Qt::ISODate); } QString HTTPProtocol::davProcessLocks() { if (hasMetaData(QStringLiteral("davLockCount"))) { QString response = QStringLiteral("If:"); int numLocks = metaData(QStringLiteral("davLockCount")).toInt(); bool bracketsOpen = false; for (int i = 0; i < numLocks; i++) { const QString countStr = QString::number(i); if (hasMetaData(QLatin1String("davLockToken") + countStr)) { if (hasMetaData(QLatin1String("davLockURL") + countStr)) { if (bracketsOpen) { response += QLatin1Char(')'); bracketsOpen = false; } response += QLatin1String(" <") + metaData(QLatin1String("davLockURL") + countStr) + QLatin1Char('>'); } if (!bracketsOpen) { response += QLatin1String(" ("); bracketsOpen = true; } else { response += QLatin1Char(' '); } if (hasMetaData(QLatin1String("davLockNot") + countStr)) { response += QLatin1String("Not "); } response += QLatin1Char('<') + metaData(QLatin1String("davLockToken") + countStr) + QLatin1Char('>'); } } if (bracketsOpen) { response += QLatin1Char(')'); } response += QLatin1String("\r\n"); return response; } return QString(); } bool HTTPProtocol::davHostOk() { // FIXME needs to be reworked. Switched off for now. return true; // cached? if (m_davHostOk) { qCDebug(KIO_HTTP) << "true"; return true; } else if (m_davHostUnsupported) { qCDebug(KIO_HTTP) << " false"; davError(-2); return false; } m_request.method = HTTP_OPTIONS; // query the server's capabilities generally, not for a specific URL m_request.url.setPath(QStringLiteral("*")); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; // clear davVersions variable, which holds the response to the DAV: header m_davCapabilities.clear(); proceedUntilResponseHeader(); if (m_davCapabilities.count()) { for (int i = 0; i < m_davCapabilities.count(); i++) { bool ok; uint verNo = m_davCapabilities[i].toUInt(&ok); if (ok && verNo > 0 && verNo < 3) { m_davHostOk = true; qCDebug(KIO_HTTP) << "Server supports DAV version" << verNo; } } if (m_davHostOk) { return true; } } m_davHostUnsupported = true; davError(-2); return false; } // This function is for closing proceedUntilResponseHeader(); requests // Required because there may or may not be further info expected void HTTPProtocol::davFinished() { // TODO: Check with the DAV extension developers httpClose(m_request.isKeepAlive); finished(); } void HTTPProtocol::mkdir(const QUrl &url, int) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_MKCOL; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::get(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_GET; QString tmp(metaData(QStringLiteral("cache"))); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } proceedUntilResponseContent(); } void HTTPProtocol::put(const QUrl &url, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // Webdav hosts are capable of observing overwrite == false if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { error(ERR_FILE_ALREADY_EXIST, QString()); return; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; } } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } void HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; const bool isSourceLocal = src.isLocalFile(); const bool isDestinationLocal = dest.isLocalFile(); if (isSourceLocal && !isDestinationLocal) { copyPut(src, dest, flags); } else { if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http(s)://..." QUrl newDest (dest); changeProtocolToHttp(&newDest); m_request.method = DAV_COPY; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion if (m_request.responseCode == 201 || m_request.responseCode == 204) { davFinished(); } else { davError(); } } } void HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http://..." QUrl newDest(dest); changeProtocolToHttp(&newDest); m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // Work around strict Apache-2 WebDAV implementation which refuses to cooperate // with webdav://host/directory, instead requiring webdav://host/directory/ // (strangely enough it accepts Destination: without a trailing slash) // See BR# 209508 and BR#187970 if (m_request.responseCode == 301) { m_request.url = m_request.redirectUrl; m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; proceedUntilResponseHeader(); } if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::del(const QUrl &url, bool) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_DELETE; m_request.cacheTag.policy = CC_Reload; if (m_protocol.startsWith("webdav")) { //krazy:exclude=strings due to QByteArray m_request.url.setQuery(QString()); if (!proceedUntilResponseHeader()) { return; } // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content // on successful completion. if (m_request.responseCode == 200 || m_request.responseCode == 204 || m_isRedirection) { davFinished(); } else { davError(); } return; } proceedUntilResponseContent(); } void HTTPProtocol::post(const QUrl &url, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_POST; m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } void HTTPProtocol::davLock(const QUrl &url, const QString &scope, const QString &type, const QString &owner) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_LOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; /* Create appropriate lock XML request. */ QDomDocument lockReq; QDomElement lockInfo = lockReq.createElementNS(QStringLiteral("DAV:"), QStringLiteral("lockinfo")); lockReq.appendChild(lockInfo); QDomElement lockScope = lockReq.createElement(QStringLiteral("lockscope")); lockInfo.appendChild(lockScope); lockScope.appendChild(lockReq.createElement(scope)); QDomElement lockType = lockReq.createElement(QStringLiteral("locktype")); lockInfo.appendChild(lockType); lockType.appendChild(lockReq.createElement(type)); if (!owner.isNull()) { QDomElement ownerElement = lockReq.createElement(QStringLiteral("owner")); lockReq.appendChild(ownerElement); QDomElement ownerHref = lockReq.createElement(QStringLiteral("href")); ownerElement.appendChild(ownerHref); ownerHref.appendChild(lockReq.createTextNode(owner)); } // insert the document into the POST buffer cachePostData(lockReq.toByteArray()); proceedUntilResponseContent(true); if (m_request.responseCode == 200) { // success QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement prop = multiResponse.documentElement().namedItem(QStringLiteral("prop")).toElement(); QDomElement lockdiscovery = prop.namedItem(QStringLiteral("lockdiscovery")).toElement(); uint lockCount = 0; davParseActiveLocks(lockdiscovery.elementsByTagName(QStringLiteral("activelock")), lockCount); setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); finished(); } else { davError(); } } void HTTPProtocol::davUnlock(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_UNLOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 200) { finished(); } else { davError(); } } QString HTTPProtocol::davError(int code /* = -1 */, const QString &_url) { bool callError = false; if (code == -1) { code = m_request.responseCode; callError = true; } if (code == -2) { callError = true; } QString url = _url; if (!url.isNull()) { url = m_request.url.toDisplayString(); } QString action, errorString; int errorCode = ERR_SLAVE_DEFINED; // for 412 Precondition Failed QString ow = i18n("Otherwise, the request would have succeeded."); switch (m_request.method) { case DAV_PROPFIND: action = i18nc("request type", "retrieve property values"); break; case DAV_PROPPATCH: action = i18nc("request type", "set property values"); break; case DAV_MKCOL: action = i18nc("request type", "create the requested folder"); break; case DAV_COPY: action = i18nc("request type", "copy the specified file or folder"); break; case DAV_MOVE: action = i18nc("request type", "move the specified file or folder"); break; case DAV_SEARCH: action = i18nc("request type", "search in the specified folder"); break; case DAV_LOCK: action = i18nc("request type", "lock the specified file or folder"); break; case DAV_UNLOCK: action = i18nc("request type", "unlock the specified file or folder"); break; case HTTP_DELETE: action = i18nc("request type", "delete the specified file or folder"); break; case HTTP_OPTIONS: action = i18nc("request type", "query the server's capabilities"); break; case HTTP_GET: action = i18nc("request type", "retrieve the contents of the specified file or folder"); break; case DAV_REPORT: action = i18nc("request type", "run a report in the specified folder"); break; case HTTP_PUT: case HTTP_POST: case HTTP_HEAD: default: // this should not happen, this function is for webdav errors only Q_ASSERT(0); } // default error message if the following code fails errorString = i18nc("%1: code, %2: request type", "An unexpected error (%1) occurred " "while attempting to %2.", code, action); switch (code) { case -2: // internal error: OPTIONS request did not specify DAV compliance // ERR_UNSUPPORTED_PROTOCOL errorString = i18n("The server does not support the WebDAV protocol."); break; case 207: // 207 Multi-status { // our error info is in the returned XML document. // retrieve the XML document // there was an error retrieving the XML document. if (!readBody(true) && m_kioError) { return QString(); } QStringList errors; QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement(); QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response")); for (int i = 0; i < responses.count(); i++) { int errCode; QString errUrl; QDomElement response = responses.item(i).toElement(); QDomElement code = response.namedItem(QStringLiteral("status")).toElement(); if (!code.isNull()) { errCode = codeFromResponse(code.text()); QDomElement href = response.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { errUrl = href.text(); } errors << davError(errCode, errUrl); } } //kError = ERR_SLAVE_DEFINED; errorString = i18nc("%1: request type, %2: url", "An error occurred while attempting to %1, %2. A " "summary of the reasons is below.", action, url); errorString += QLatin1String("
    "); Q_FOREACH (const QString &error, errors) { errorString += QLatin1String("
  • ") + error + QLatin1String("
  • "); } errorString += QLatin1String("
"); } break; case 403: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); break; case 405: // 405 Method Not Allowed if (m_request.method == DAV_MKCOL) { // ERR_DIR_ALREADY_EXIST errorString = url; errorCode = ERR_DIR_ALREADY_EXIST; } break; case 409: // 409 Conflict // ERR_ACCESS_DENIED errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); break; case 412: // 412 Precondition failed if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_ACCESS_DENIED errorString = i18n("The server was unable to maintain the liveness of " "the properties listed in the propertybehavior XML " "element\n or you attempted to overwrite a file while " "requesting that files are not overwritten.\n %1", ow); } else if (m_request.method == DAV_LOCK) { // ERR_ACCESS_DENIED errorString = i18n("The requested lock could not be granted. %1", ow); } break; case 415: // 415 Unsupported Media Type // ERR_ACCESS_DENIED errorString = i18n("The server does not support the request type of the body."); break; case 423: // 423 Locked // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); break; case 425: // 424 Failed Dependency errorString = i18n("This action was prevented by another error."); break; case 502: // 502 Bad Gateway if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_WRITE_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); } break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); break; default: break; } // if ( kError != ERR_SLAVE_DEFINED ) //errorString += " (" + url + ')'; if (callError) { error(errorCode, errorString); } return errorString; } // HTTP generic error static int httpGenericError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; errorString->clear(); if (request.responseCode == 204) { errorCode = ERR_NO_CONTENT; } return errorCode; } // HTTP DELETE specific errors static int httpDelError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; errorString->clear(); switch (responseCode) { case 204: errorCode = ERR_NO_CONTENT; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18n("The resource cannot be deleted."); } return errorCode; } // HTTP PUT specific errors static int httpPutError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; const QString action(i18nc("request type", "upload %1", request.url.toDisplayString())); switch (responseCode) { case 403: case 405: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // 405 Method Not Allowed // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); errorCode = ERR_SLAVE_DEFINED; break; case 409: // 409 Conflict // ERR_ACCESS_DENIED *errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); errorCode = ERR_SLAVE_DEFINED; break; case 423: // 423 Locked // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); errorCode = ERR_SLAVE_DEFINED; break; case 502: // 502 Bad Gateway // ERR_WRITE_ACCESS_DENIED; *errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); errorCode = ERR_SLAVE_DEFINED; break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL *errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); errorCode = ERR_SLAVE_DEFINED; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action); } return errorCode; } bool HTTPProtocol::sendHttpError() { QString errorString; int errorCode = 0; switch (m_request.method) { case HTTP_GET: case HTTP_POST: errorCode = httpGenericError(m_request, &errorString); break; case HTTP_PUT: errorCode = httpPutError(m_request, &errorString); break; case HTTP_DELETE: errorCode = httpDelError(m_request, &errorString); break; default: break; } // Force any message previously shown by the client to be cleared. infoMessage(QLatin1String("")); if (errorCode) { error(errorCode, errorString); return true; } return false; } bool HTTPProtocol::sendErrorPageNotification() { if (!m_request.preferErrorPage) { return false; } if (m_isLoadingErrorPage) { qCWarning(KIO_HTTP) << "called twice during one request, something is probably wrong."; } m_isLoadingErrorPage = true; SlaveBase::errorPage(); return true; } bool HTTPProtocol::isOffline() { if (!m_networkConfig) { m_networkConfig = new QNetworkConfigurationManager(this); } return !m_networkConfig->isOnline(); } void HTTPProtocol::multiGet(const QByteArray &data) { QDataStream stream(data); quint32 n; stream >> n; qCDebug(KIO_HTTP) << n; HTTPRequest saveRequest; if (m_isBusy) { saveRequest = m_request; } resetSessionSettings(); for (unsigned i = 0; i < n; ++i) { QUrl url; stream >> url >> mIncomingMetaData; if (!maybeSetRequestUrl(url)) { continue; } //### should maybe call resetSessionSettings() if the server/domain is // different from the last request! qCDebug(KIO_HTTP) << url; m_request.method = HTTP_GET; m_request.isKeepAlive = true; //readResponseHeader clears it if necessary QString tmp = metaData(QStringLiteral("cache")); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } m_requestQueue.append(m_request); } if (m_isBusy) { m_request = saveRequest; } #if 0 if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); while (it.hasNext()) { m_request = it.next(); it.remove(); proceedUntilResponseContent(); } m_isBusy = false; } #endif if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); // send the requests while (it.hasNext()) { m_request = it.next(); sendQuery(); // save the request state so we can pick it up again in the collection phase it.setValue(m_request); qCDebug(KIO_HTTP) << "check one: isKeepAlive =" << m_request.isKeepAlive; if (m_request.cacheTag.ioMode != ReadFromCache) { m_server.initFrom(m_request); } } // collect the responses //### for the moment we use a hack: instead of saving and restoring request-id // we just count up like ParallelGetJobs does. int requestId = 0; Q_FOREACH (const HTTPRequest &r, m_requestQueue) { m_request = r; qCDebug(KIO_HTTP) << "check two: isKeepAlive =" << m_request.isKeepAlive; setMetaData(QStringLiteral("request-id"), QString::number(requestId++)); sendAndKeepMetaData(); if (!(readResponseHeader() && readBody())) { return; } // the "next job" signal for ParallelGetJob is data of size zero which // readBody() sends without our intervention. qCDebug(KIO_HTTP) << "check three: isKeepAlive =" << m_request.isKeepAlive; httpClose(m_request.isKeepAlive); //actually keep-alive is mandatory for pipelining } finished(); m_requestQueue.clear(); m_isBusy = false; } } ssize_t HTTPProtocol::write(const void *_buf, size_t nbytes) { size_t sent = 0; const char *buf = static_cast(_buf); while (sent < nbytes) { int n = TCPSlaveBase::write(buf + sent, nbytes - sent); if (n < 0) { // some error occurred return -1; } sent += n; } return sent; } void HTTPProtocol::clearUnreadBuffer() { m_unreadBuf.clear(); } // Note: the implementation of unread/readBuffered assumes that unread will only // be used when there is extra data we don't want to handle, and not to wait for more data. void HTTPProtocol::unread(char *buf, size_t size) { // implement LIFO (stack) semantics const int newSize = m_unreadBuf.size() + size; m_unreadBuf.resize(newSize); for (size_t i = 0; i < size; i++) { m_unreadBuf.data()[newSize - i - 1] = buf[i]; } if (size) { //hey, we still have data, closed connection or not! m_isEOF = false; } } size_t HTTPProtocol::readBuffered(char *buf, size_t size, bool unlimited) { size_t bytesRead = 0; if (!m_unreadBuf.isEmpty()) { const int bufSize = m_unreadBuf.size(); bytesRead = qMin((int)size, bufSize); for (size_t i = 0; i < bytesRead; i++) { buf[i] = m_unreadBuf.constData()[bufSize - i - 1]; } m_unreadBuf.chop(bytesRead); // If we have an unread buffer and the size of the content returned by the // server is unknown, e.g. chuncked transfer, return the bytes read here since // we may already have enough data to complete the response and don't want to // wait for more. See BR# 180631. if (unlimited) { return bytesRead; } } if (bytesRead < size) { int rawRead = TCPSlaveBase::read(buf + bytesRead, size - bytesRead); if (rawRead < 1) { m_isEOF = true; return bytesRead; } bytesRead += rawRead; } return bytesRead; } //### this method will detect an n*(\r\n) sequence if it crosses invocations. // it will look (n*2 - 1) bytes before start at most and never before buf, naturally. // supported number of newlines are one and two, in line with HTTP syntax. // return true if numNewlines newlines were found. bool HTTPProtocol::readDelimitedText(char *buf, int *idx, int end, int numNewlines) { Q_ASSERT(numNewlines >= 1 && numNewlines <= 2); char mybuf[64]; //somewhere close to the usual line length to avoid unread()ing too much int pos = *idx; while (pos < end && !m_isEOF) { int step = qMin((int)sizeof(mybuf), end - pos); if (m_isChunked) { //we might be reading the end of the very last chunk after which there is no data. //don't try to read any more bytes than there are because it causes stalls //(yes, it shouldn't stall but it does) step = 1; } size_t bufferFill = readBuffered(mybuf, step); for (size_t i = 0; i < bufferFill; ++i, ++pos) { // we copy the data from mybuf to buf immediately and look for the newlines in buf. // that way we don't miss newlines split over several invocations of this method. buf[pos] = mybuf[i]; // did we just copy one or two times the (usually) \r\n delimiter? // until we find even more broken webservers in the wild let's assume that they either // send \r\n (RFC compliant) or \n (broken) as delimiter... if (buf[pos] == '\n') { bool found = numNewlines == 1; if (!found) { // looking for two newlines // Detect \n\n and \n\r\n. The other cases (\r\n\n, \r\n\r\n) are covered by the first two. found = ((pos >= 1 && buf[pos - 1] == '\n') || (pos >= 2 && buf[pos - 2] == '\n' && buf[pos - 1] == '\r')); } if (found) { i++; // unread bytes *after* CRLF unread(&mybuf[i], bufferFill - i); *idx = pos + 1; return true; } } } } *idx = pos; return false; } static bool isCompatibleNextUrl(const QUrl &previous, const QUrl &now) { if (previous.host() != now.host() || previous.port() != now.port()) { return false; } if (previous.userName().isEmpty() && previous.password().isEmpty()) { return true; } return previous.userName() == now.userName() && previous.password() == now.password(); } bool HTTPProtocol::httpShouldCloseConnection() { qCDebug(KIO_HTTP); if (!isConnected()) { return false; } if (!m_request.proxyUrls.isEmpty() && !isAutoSsl()) { Q_FOREACH (const QString &url, m_request.proxyUrls) { if (url != QLatin1String("DIRECT")) { if (isCompatibleNextUrl(m_server.proxyUrl, QUrl(url))) { return false; } } } return true; } return !isCompatibleNextUrl(m_server.url, m_request.url); } bool HTTPProtocol::httpOpenConnection() { qCDebug(KIO_HTTP); m_server.clear(); // Only save proxy auth information after proxy authentication has // actually taken place, which will set up exactly this connection. disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); clearUnreadBuffer(); int connectError = 0; QString errorString; // Get proxy information... if (m_request.proxyUrls.isEmpty()) { m_request.proxyUrls = config()->readEntry("ProxyUrls", QStringList()); qCDebug(KIO_HTTP) << "Proxy URLs:" << m_request.proxyUrls; } if (m_request.proxyUrls.isEmpty()) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); } else { QList badProxyUrls; Q_FOREACH (const QString &proxyUrl, m_request.proxyUrls) { if (proxyUrl == QLatin1String("DIRECT")) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { //qDebug() << "Connected DIRECT: host=" << m_request.url.host() << "port=" << m_request.url.port(defaultPort()); break; } else { continue; } } const QUrl url(proxyUrl); const QString proxyScheme(url.scheme()); if (!supportedProxyScheme(proxyScheme)) { connectError = ERR_CANNOT_CONNECT; errorString = url.toDisplayString(); badProxyUrls << url; continue; } QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy; if (proxyScheme == QLatin1String("socks")) { proxyType = QNetworkProxy::Socks5Proxy; } else if (isAutoSsl()) { proxyType = QNetworkProxy::HttpProxy; } qCDebug(KIO_HTTP) << "Connecting to proxy: address=" << proxyUrl << "type=" << proxyType; if (proxyType == QNetworkProxy::NoProxy) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(url.host(), url.port(), &errorString); if (connectError == 0) { m_request.proxyUrl = url; //qDebug() << "Connected to proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } //qDebug() << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; } } else { QNetworkProxy proxy(proxyType, url.host(), url.port(), url.userName(), url.password()); QNetworkProxy::setApplicationProxy(proxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { qCDebug(KIO_HTTP) << "Tunneling thru proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } qCDebug(KIO_HTTP) << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); } } } if (!badProxyUrls.isEmpty()) { //TODO: Notify the client of BAD proxy addresses (needed for PAC setups). } } if (connectError != 0) { error(connectError, errorString); return false; } // Disable Nagle's algorithm, i.e turn on TCP_NODELAY. KTcpSocket *sock = qobject_cast(socket()); if (sock) { qCDebug(KIO_HTTP) << "TCP_NODELAY:" << sock->socketOption(QAbstractSocket::LowDelayOption); sock->setSocketOption(QAbstractSocket::LowDelayOption, 1); } m_server.initFrom(m_request); connected(); return true; } bool HTTPProtocol::satisfyRequestFromCache(bool *cacheHasPage) { qCDebug(KIO_HTTP); if (m_request.cacheTag.useCache) { const bool offline = isOffline(); if (offline && m_request.cacheTag.policy != KIO::CC_Reload) { m_request.cacheTag.policy = KIO::CC_CacheOnly; } const bool isCacheOnly = m_request.cacheTag.policy == KIO::CC_CacheOnly; const CacheTag::CachePlan plan = m_request.cacheTag.plan(m_maxCacheAge); bool openForReading = false; if (plan == CacheTag::UseCached || plan == CacheTag::ValidateCached) { openForReading = cacheFileOpenRead(); if (!openForReading && (isCacheOnly || offline)) { // cache-only or offline -> we give a definite answer and it is "no" *cacheHasPage = false; if (isCacheOnly) { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } else if (offline) { error(ERR_CANNOT_CONNECT, m_request.url.toDisplayString()); } return true; } } if (openForReading) { m_request.cacheTag.ioMode = ReadFromCache; *cacheHasPage = true; // return false if validation is required, so a network request will be sent return m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached; } } *cacheHasPage = false; return false; } QString HTTPProtocol::formatRequestUri() const { // Only specify protocol, host and port when they are not already clear, i.e. when // we handle HTTP proxying ourself and the proxy server needs to know them. // Sending protocol/host/port in other cases confuses some servers, and it's not their fault. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QUrl u; QString protocol = m_request.url.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, qstrlen("webdav"), QStringLiteral("http")); } u.setScheme(protocol); u.setHost(m_request.url.host()); // if the URL contained the default port it should have been stripped earlier Q_ASSERT(m_request.url.port() != defaultPort()); u.setPort(m_request.url.port()); u.setPath(m_request.url.path(QUrl::FullyEncoded)); u.setQuery(m_request.url.query(QUrl::FullyEncoded)); return u.toString(QUrl::FullyEncoded); } else { QString result = m_request.url.path(QUrl::FullyEncoded); if (m_request.url.hasQuery()) { result += QLatin1Char('?') + m_request.url.query(QUrl::FullyEncoded); } return result; } } /** * This function is responsible for opening up the connection to the remote * HTTP server and sending the header. If this requires special * authentication or other such fun stuff, then it will handle it. This * function will NOT receive anything from the server, however. This is in * contrast to previous incarnations of 'httpOpen' as this method used to be * called. * * The basic process now is this: * * 1) Open up the socket and port * 2) Format our request/header * 3) Send the header to the remote server * 4) Call sendBody() if the HTTP method requires sending body data */ bool HTTPProtocol::sendQuery() { qCDebug(KIO_HTTP); // Cannot have an https request without autoSsl! This can // only happen if the current installation does not support SSL... if (isEncryptedHttpVariety(m_protocol) && !isAutoSsl()) { error(ERR_UNSUPPORTED_PROTOCOL, toQString(m_protocol)); return false; } // Check the reusability of the current connection. if (httpShouldCloseConnection()) { httpCloseConnection(); } // Create a new connection to the remote machine if we do // not already have one... // NB: the !m_socketProxyAuth condition is a workaround for a proxied Qt socket sometimes // looking disconnected after receiving the initial 407 response. // I guess the Qt socket fails to hide the effect of proxy-connection: close after receiving // the 407 header. if ((!isConnected() && !m_socketProxyAuth)) { if (!httpOpenConnection()) { qCDebug(KIO_HTTP) << "Couldn't connect, oopsie!"; return false; } } m_request.cacheTag.ioMode = NoCache; m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); QString header; bool hasBodyData = false; bool hasDavData = false; { m_request.sentMethodString = m_request.methodString(); header = toQString(m_request.sentMethodString) + QLatin1Char(' '); QString davHeader; // Fill in some values depending on the HTTP method to guide further processing switch (m_request.method) { case HTTP_GET: { bool cacheHasPage = false; if (satisfyRequestFromCache(&cacheHasPage)) { qCDebug(KIO_HTTP) << "cacheHasPage =" << cacheHasPage; return cacheHasPage; } if (!cacheHasPage) { // start a new cache file later if appropriate m_request.cacheTag.ioMode = WriteToCache; } break; } case HTTP_HEAD: break; case HTTP_PUT: case HTTP_POST: hasBodyData = true; break; case HTTP_DELETE: case HTTP_OPTIONS: break; case DAV_PROPFIND: hasDavData = true; davHeader = QStringLiteral("Depth: "); if (hasMetaData(QStringLiteral("davDepth"))) { qCDebug(KIO_HTTP) << "Reading DAV depth from metadata:" << metaData( QStringLiteral("davDepth") ); davHeader += metaData(QStringLiteral("davDepth")); } else { if (m_request.davData.depth == 2) { davHeader += QLatin1String("infinity"); } else { davHeader += QString::number(m_request.davData.depth); } } davHeader += QLatin1String("\r\n"); break; case DAV_PROPPATCH: hasDavData = true; break; case DAV_MKCOL: break; case DAV_COPY: case DAV_MOVE: davHeader = QLatin1String("Destination: ") + m_request.davData.desturl + // infinity depth means copy recursively // (optional for copy -> but is the desired action) QLatin1String("\r\nDepth: infinity\r\nOverwrite: ") + QLatin1Char(m_request.davData.overwrite ? 'T' : 'F') + QLatin1String("\r\n"); break; case DAV_LOCK: davHeader = QStringLiteral("Timeout: "); { uint timeout = 0; if (hasMetaData(QStringLiteral("davTimeout"))) { timeout = metaData(QStringLiteral("davTimeout")).toUInt(); } if (timeout == 0) { davHeader += QLatin1String("Infinite"); } else { davHeader += QLatin1String("Seconds-") + QString::number(timeout); } } davHeader += QLatin1String("\r\n"); hasDavData = true; break; case DAV_UNLOCK: davHeader = QLatin1String("Lock-token: ") + metaData(QStringLiteral("davLockToken")) + QLatin1String("\r\n"); break; case DAV_SEARCH: case DAV_REPORT: hasDavData = true; /* fall through */ case DAV_SUBSCRIBE: case DAV_UNSUBSCRIBE: case DAV_POLL: break; default: error(ERR_UNSUPPORTED_ACTION, QString()); return false; } // DAV_POLL; DAV_NOTIFY header += formatRequestUri() + QLatin1String(" HTTP/1.1\r\n"); /* start header */ /* support for virtual hosts and required by HTTP 1.1 */ header += QLatin1String("Host: ") + m_request.encoded_hostname; if (m_request.url.port(defaultPort()) != defaultPort()) { header += QLatin1Char(':') + QString::number(m_request.url.port()); } header += QLatin1String("\r\n"); // Support old HTTP/1.0 style keep-alive header for compatibility // purposes as well as performance improvements while giving end // users the ability to disable this feature for proxy servers that // don't support it, e.g. junkbuster proxy server. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { header += QLatin1String("Proxy-Connection: "); } else { header += QLatin1String("Connection: "); } if (m_request.isKeepAlive) { header += QLatin1String("keep-alive\r\n"); } else { header += QLatin1String("close\r\n"); } if (!m_request.userAgent.isEmpty()) { header += QLatin1String("User-Agent: ") + m_request.userAgent + QLatin1String("\r\n"); } if (!m_request.referrer.isEmpty()) { // Don't try to correct spelling! header += QLatin1String("Referer: ") + m_request.referrer + QLatin1String("\r\n"); } if (m_request.endoffset > m_request.offset) { header += QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1Char('-') + KIO::number(m_request.endoffset) + QLatin1String("\r\n"); qCDebug(KIO_HTTP) << "kio_http : Range =" << KIO::number(m_request.offset) << "-" << KIO::number(m_request.endoffset); } else if (m_request.offset > 0 && m_request.endoffset == 0) { header += QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1String("-\r\n"); qCDebug(KIO_HTTP) << "kio_http: Range =" << KIO::number(m_request.offset); } if (!m_request.cacheTag.useCache || m_request.cacheTag.policy == CC_Reload) { /* No caching for reload */ header += QLatin1String("Pragma: no-cache\r\n"); /* for HTTP/1.0 caches */ header += QLatin1String("Cache-control: no-cache\r\n"); /* for HTTP >=1.1 caches */ } else if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { qCDebug(KIO_HTTP) << "needs validation, performing conditional get."; /* conditional get */ if (!m_request.cacheTag.etag.isEmpty()) { header += QLatin1String("If-None-Match: ") + m_request.cacheTag.etag + QLatin1String("\r\n"); } if (m_request.cacheTag.lastModifiedDate.isValid()) { const QString httpDate = formatHttpDate(m_request.cacheTag.lastModifiedDate); header += QLatin1String("If-Modified-Since: ") + httpDate + QLatin1String("\r\n"); setMetaData(QStringLiteral("modified"), httpDate); } } header += QLatin1String("Accept: "); const QString acceptHeader = metaData(QStringLiteral("accept")); if (!acceptHeader.isEmpty()) { header += acceptHeader; } else { header += QLatin1String(DEFAULT_ACCEPT_HEADER); } header += QLatin1String("\r\n"); if (m_request.allowTransferCompression) { header += QLatin1String("Accept-Encoding: gzip, deflate, x-gzip, x-deflate\r\n"); } if (!m_request.charsets.isEmpty()) { header += QLatin1String("Accept-Charset: ") + m_request.charsets + QLatin1String("\r\n"); } if (!m_request.languages.isEmpty()) { header += QLatin1String("Accept-Language: ") + m_request.languages + QLatin1String("\r\n"); } QString cookieStr; const QString cookieMode = metaData(QStringLiteral("cookies")).toLower(); if (cookieMode == QLatin1String("none")) { m_request.cookieMode = HTTPRequest::CookiesNone; } else if (cookieMode == QLatin1String("manual")) { m_request.cookieMode = HTTPRequest::CookiesManual; cookieStr = metaData(QStringLiteral("setcookies")); } else { m_request.cookieMode = HTTPRequest::CookiesAuto; if (m_request.useCookieJar) { cookieStr = findCookies(m_request.url.toString()); } } if (!cookieStr.isEmpty()) { header += cookieStr + QLatin1String("\r\n"); } const QString customHeader = metaData(QStringLiteral("customHTTPHeader")); if (!customHeader.isEmpty()) { header += sanitizeCustomHTTPHeader(customHeader) + QLatin1String("\r\n"); } const QString contentType = metaData(QStringLiteral("content-type")); if (!contentType.isEmpty()) { if (!contentType.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) { header += QLatin1String("Content-Type: "); } header += contentType + QLatin1String("\r\n"); } // DoNotTrack feature... if (config()->readEntry("DoNotTrack", false)) { header += QLatin1String("DNT: 1\r\n"); } // Remember that at least one failed (with 401 or 407) request/response // roundtrip is necessary for the server to tell us that it requires // authentication. However, we proactively add authentication headers if when // we have cached credentials to avoid the extra roundtrip where possible. header += authenticationHeader(); if (m_protocol == "webdav" || m_protocol == "webdavs") { header += davProcessLocks(); // add extra webdav headers, if supplied davHeader += metaData(QStringLiteral("davHeader")); // Set content type of webdav data if (hasDavData) { davHeader += QStringLiteral("Content-Type: text/xml; charset=utf-8\r\n"); } // add extra header elements for WebDAV header += davHeader; } } qCDebug(KIO_HTTP) << "============ Sending Header:"; Q_FOREACH (const QString &s, header.split(QStringLiteral("\r\n"), QString::SkipEmptyParts)) { qCDebug(KIO_HTTP) << s; } // End the header iff there is no payload data. If we do have payload data // sendBody() will add another field to the header, Content-Length. if (!hasBodyData && !hasDavData) { header += QStringLiteral("\r\n"); } // Now that we have our formatted header, let's send it! // Clear out per-connection settings... resetConnectionSettings(); // Send the data to the remote machine... const QByteArray headerBytes = header.toLatin1(); ssize_t written = write(headerBytes.constData(), headerBytes.length()); bool sendOk = (written == (ssize_t) headerBytes.length()); if (!sendOk) { qCDebug(KIO_HTTP) << "Connection broken! (" << m_request.url.host() << ")" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "sendOk == false. Connection broken !" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } else { qCDebug(KIO_HTTP) << "sent it!"; } bool res = true; if (hasBodyData || hasDavData) { res = sendBody(); } infoMessage(i18n("%1 contacted. Waiting for reply...", m_request.url.host())); return res; } void HTTPProtocol::forwardHttpResponseHeader(bool forwardImmediately) { // Send the response header if it was requested... if (!config()->readEntry("PropagateHttpHeader", false)) { return; } setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); if (forwardImmediately) { sendMetaData(); } } bool HTTPProtocol::parseHeaderFromCache() { qCDebug(KIO_HTTP); if (!cacheFileReadTextHeader2()) { return false; } Q_FOREACH (const QString &str, m_responseHeaders) { const QString header = str.trimmed(); if (header.startsWith(QLatin1String("content-type:"), Qt::CaseInsensitive)) { int pos = header.indexOf(QLatin1String("charset="), Qt::CaseInsensitive); if (pos != -1) { const QString charset = header.mid(pos + 8).toLower(); m_request.cacheTag.charset = charset; setMetaData(QStringLiteral("charset"), charset); } } else if (header.startsWith(QLatin1String("content-language:"), Qt::CaseInsensitive)) { const QString language = header.mid(17).trimmed().toLower(); setMetaData(QStringLiteral("content-language"), language); } else if (header.startsWith(QLatin1String("content-disposition:"), Qt::CaseInsensitive)) { parseContentDisposition(header.mid(20).toLower()); } } if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), formatHttpDate(m_request.cacheTag.lastModifiedDate)); } // this header comes from the cache, so the response must have been cacheable :) setCacheabilityMetadata(true); qCDebug(KIO_HTTP) << "Emitting mimeType" << m_mimeType; forwardHttpResponseHeader(false); mimeType(m_mimeType); // IMPORTANT: Do not remove the call below or the http response headers will // not be available to the application if this slave is put on hold. forwardHttpResponseHeader(); return true; } void HTTPProtocol::fixupResponseMimetype() { if (m_mimeType.isEmpty()) { return; } qCDebug(KIO_HTTP) << "before fixup" << m_mimeType; // Convert some common mimetypes to standard mimetypes if (m_mimeType == QLatin1String("application/x-targz")) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("image/x-png")) { m_mimeType = QStringLiteral("image/png"); } else if (m_mimeType == QLatin1String("audio/x-mp3") || m_mimeType == QLatin1String("audio/x-mpeg") || m_mimeType == QLatin1String("audio/mp3")) { m_mimeType = QStringLiteral("audio/mpeg"); } else if (m_mimeType == QLatin1String("audio/microsoft-wave")) { m_mimeType = QStringLiteral("audio/x-wav"); } else if (m_mimeType == QLatin1String("image/x-ms-bmp")) { m_mimeType = QStringLiteral("image/bmp"); } // Crypto ones.... else if (m_mimeType == QLatin1String("application/pkix-cert") || m_mimeType == QLatin1String("application/binary-certificate")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } // Prefer application/x-compressed-tar or x-gzpostscript over application/x-gzip. else if (m_mimeType == QLatin1String("application/x-gzip")) { if ((m_request.url.path().endsWith(QLatin1String(".tar.gz"))) || (m_request.url.path().endsWith(QLatin1String(".tar")))) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } if ((m_request.url.path().endsWith(QLatin1String(".ps.gz")))) { m_mimeType = QStringLiteral("application/x-gzpostscript"); } } // Prefer application/x-xz-compressed-tar over application/x-xz for LMZA compressed // tar files. Arch Linux AUR servers notoriously send the wrong mimetype for this. else if (m_mimeType == QLatin1String("application/x-xz")) { if (m_request.url.path().endsWith(QLatin1String(".tar.xz")) || m_request.url.path().endsWith(QLatin1String(".txz"))) { m_mimeType = QStringLiteral("application/x-xz-compressed-tar"); } } // Some webservers say "text/plain" when they mean "application/x-bzip" else if ((m_mimeType == QLatin1String("text/plain")) || (m_mimeType == QLatin1String("application/octet-stream"))) { const QString ext = QFileInfo(m_request.url.path()).suffix().toUpper(); if (ext == QLatin1String("BZ2")) { m_mimeType = QStringLiteral("application/x-bzip"); } else if (ext == QLatin1String("PEM")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } else if (ext == QLatin1String("SWF")) { m_mimeType = QStringLiteral("application/x-shockwave-flash"); } else if (ext == QLatin1String("PLS")) { m_mimeType = QStringLiteral("audio/x-scpls"); } else if (ext == QLatin1String("WMV")) { m_mimeType = QStringLiteral("video/x-ms-wmv"); } else if (ext == QLatin1String("WEBM")) { m_mimeType = QStringLiteral("video/webm"); } else if (ext == QLatin1String("DEB")) { m_mimeType = QStringLiteral("application/x-deb"); } } qCDebug(KIO_HTTP) << "after fixup" << m_mimeType; } void HTTPProtocol::fixupResponseContentEncoding() { // WABA: Correct for tgz files with a gzip-encoding. // They really shouldn't put gzip in the Content-Encoding field! // Web-servers really shouldn't do this: They let Content-Size refer // to the size of the tgz file, not to the size of the tar file, // while the Content-Type refers to "tar" instead of "tgz". if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("gzip")) { if (m_mimeType == QLatin1String("application/x-tar")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("application/postscript")) { // LEONB: Adding another exception for psgz files. // Could we use the mimelnk files instead of hardcoding all this? m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzpostscript"); } else if ((m_request.allowTransferCompression && m_mimeType == QLatin1String("text/html")) || (m_request.allowTransferCompression && m_mimeType != QLatin1String("application/x-compressed-tar") && m_mimeType != QLatin1String("application/x-tgz") && // deprecated name m_mimeType != QLatin1String("application/x-targz") && // deprecated name m_mimeType != QLatin1String("application/x-gzip"))) { // Unzip! } else { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzip"); } } // We can't handle "bzip2" encoding (yet). So if we get something with // bzip2 encoding, we change the mimetype to "application/x-bzip". // Note for future changes: some web-servers send both "bzip2" as // encoding and "application/x-bzip[2]" as mimetype. That is wrong. // currently that doesn't bother us, because we remove the encoding // and set the mimetype to x-bzip anyway. if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("bzip2")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-bzip"); } } #ifdef Q_CC_MSVC // strncasecmp does not exist on windows, have to use _strnicmp static inline int strncasecmp(const char *c1, const char* c2, size_t max) { return _strnicmp(c1, c2, max); } #endif //Return true if the term was found, false otherwise. Advance *pos. //If (*pos + strlen(term) >= end) just advance *pos to end and return false. //This means that users should always search for the shortest terms first. static bool consume(const char input[], int *pos, int end, const char *term) { // note: gcc/g++ is quite good at optimizing away redundant strlen()s int idx = *pos; if (idx + (int)strlen(term) >= end) { *pos = end; return false; } if (strncasecmp(&input[idx], term, strlen(term)) == 0) { *pos = idx + strlen(term); return true; } return false; } /** * This function will read in the return header from the server. It will * not read in the body of the return message. It will also not transmit * the header to our client as the client doesn't need to know the gory * details of HTTP headers. */ bool HTTPProtocol::readResponseHeader() { resetResponseParsing(); if (m_request.cacheTag.ioMode == ReadFromCache && m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached) { // parseHeaderFromCache replaces this method in case of cached content return parseHeaderFromCache(); } try_again: qCDebug(KIO_HTTP); bool upgradeRequired = false; // Server demands that we upgrade to something // This is also true if we ask to upgrade and // the server accepts, since we are now // committed to doing so bool noHeadersFound = false; m_request.cacheTag.charset.clear(); m_responseHeaders.clear(); static const int maxHeaderSize = 128 * 1024; char buffer[maxHeaderSize]; bool cont = false; bool bCanResume = false; if (!isConnected()) { qCDebug(KIO_HTTP) << "No connection."; return false; // Reestablish connection and try again } #if 0 // NOTE: This is unnecessary since TCPSlaveBase::read does the same exact // thing. Plus, if we are unable to read from the socket we need to resend // the request as done below, not error out! Do not assume remote server // will honor persistent connections!! if (!waitForResponse(m_remoteRespTimeout)) { qCDebug(KIO_HTTP) << "Got socket error:" << socket()->errorString(); // No response error error(ERR_SERVER_TIMEOUT, m_request.url.host()); return false; } #endif int bufPos = 0; bool foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 1); if (!foundDelimiter && bufPos < maxHeaderSize) { qCDebug(KIO_HTTP) << "EOF while waiting for header start."; if (m_request.isKeepAlive && m_iEOFRetryCount < 2) { m_iEOFRetryCount++; httpCloseConnection(); // Try to reestablish connection. return false; // Reestablish connection and try again. } if (m_request.method == HTTP_HEAD) { // HACK // Some web-servers fail to respond properly to a HEAD request. // We compensate for their failure to properly implement the HTTP standard // by assuming that they will be sending html. qCDebug(KIO_HTTP) << "HEAD -> returned mimetype:" << DEFAULT_MIME_TYPE; mimeType(QStringLiteral(DEFAULT_MIME_TYPE)); return true; } qCDebug(KIO_HTTP) << "Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } if (!foundDelimiter) { //### buffer too small for first line of header(!) Q_ASSERT(0); } qCDebug(KIO_HTTP) << "============ Received Status Response:"; qCDebug(KIO_HTTP) << QByteArray(buffer, bufPos).trimmed(); HTTP_REV httpRev = HTTP_None; int idx = 0; if (idx != bufPos && buffer[idx] == '<') { qCDebug(KIO_HTTP) << "No valid HTTP header found! Document starts with XML/HTML tag"; // document starts with a tag, assume HTML instead of text/plain m_mimeType = QStringLiteral("text/html"); m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; // put string back unread(buffer, bufPos); goto endParsing; } // "HTTP/1.1" or similar if (consume(buffer, &idx, bufPos, "ICY ")) { httpRev = SHOUTCAST; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "HTTP/")) { if (consume(buffer, &idx, bufPos, "1.0")) { httpRev = HTTP_10; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "1.1")) { httpRev = HTTP_11; } } if (httpRev == HTTP_None && bufPos != 0) { // Remote server does not seem to speak HTTP at all // Put the crap back into the buffer and hope for the best qCDebug(KIO_HTTP) << "DO NOT WANT." << bufPos; unread(buffer, bufPos); if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; goto endParsing; } // response code //### maybe wrong if we need several iterations for this response... //### also, do multiple iterations (cf. try_again) to parse one header work w/ pipelining? if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } skipSpace(buffer, &idx, bufPos); //TODO saner handling of invalid response code strings if (idx != bufPos) { m_request.responseCode = atoi(&buffer[idx]); } else { m_request.responseCode = 200; } // move idx to start of (yet to be fetched) next line, skipping the "OK" idx = bufPos; // (don't bother parsing the "OK", what do we do if it isn't there anyway?) // immediately act on most response codes... // Protect users against bogus username intended to fool them into visiting // sites they had no intention of visiting. if (isPotentialSpoofingAttack(m_request, config())) { qCDebug(KIO_HTTP) << "**** POTENTIAL ADDRESS SPOOFING:" << m_request.url; const int result = messageBox(WarningYesNo, i18nc("@info Security check on url being accessed", "

You are about to log in to the site \"%1\" " "with the username \"%2\", but the website " "does not require authentication. " "This may be an attempt to trick you.

" "

Is \"%1\" the site you want to visit?

", m_request.url.host(), m_request.url.userName()), i18nc("@title:window", "Confirm Website Access")); if (result == SlaveBase::No) { error(ERR_USER_CANCELED, m_request.url.toDisplayString()); return false; } setMetaData(QStringLiteral("{internal~currenthost}LastSpoofedUserName"), m_request.url.userName()); } if (m_request.responseCode != 200 && m_request.responseCode != 304) { m_request.cacheTag.ioMode = NoCache; if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { // Server side errors if (m_request.method == HTTP_HEAD) { ; // Ignore error } else { if (!sendErrorPageNotification()) { error(ERR_INTERNAL_SERVER, m_request.url.toDisplayString()); return false; } } } else if (m_request.responseCode == 416) { // Range not supported m_request.offset = 0; return false; // Try again. } else if (m_request.responseCode == 426) { // Upgrade Required upgradeRequired = true; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { // Any other client errors // Tell that we will only get an error page here. if (!sendErrorPageNotification()) { if (m_request.responseCode == 403) { error(ERR_ACCESS_DENIED, m_request.url.toDisplayString()); } else { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } } } else if (m_request.responseCode >= 301 && m_request.responseCode <= 308) { // NOTE: According to RFC 2616 (section 10.3.[2-4,8]), 301 and 302 // redirects for a POST operation should not be converted to a GET // request. That should only be done for a 303 response. However, // because almost all other client implementations do exactly that // in violation of the spec, many servers have simply adapted to // this way of doing things! Thus, we are forced to do the same // thing here. Otherwise, we loose compatibility and might not be // able to correctly retrieve sites that redirect. switch (m_request.responseCode) { case 301: // Moved Permanently setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); // fall through case 302: // Found if (m_request.sentMethodString == "POST") { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 303: // See Other if (m_request.method != HTTP_HEAD) { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 308: // Permanent Redirect setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); break; default: break; } } else if (m_request.responseCode == 204) { // No content // error(ERR_NO_CONTENT, i18n("Data have been successfully sent.")); // Short circuit and do nothing! // The original handling here was wrong, this is not an error: eg. in the // example of a 204 No Content response to a PUT completing. // return false; } else if (m_request.responseCode == 206) { if (m_request.offset) { bCanResume = true; } } else if (m_request.responseCode == 102) { // Processing (for WebDAV) /*** * This status code is given when the server expects the * command to take significant time to complete. So, inform * the user. */ infoMessage(i18n("Server processing request, please wait...")); cont = true; } else if (m_request.responseCode == 100) { // We got 'Continue' - ignore it cont = true; } } // (m_request.responseCode != 200 && m_request.responseCode != 304) endParsing: bool authRequiresAnotherRoundtrip = false; // Skip the whole header parsing if we got no HTTP headers at all if (!noHeadersFound) { // Auth handling const bool wasAuthError = isAuthenticationRequired(m_request.prevResponseCode); const bool isAuthError = isAuthenticationRequired(m_request.responseCode); const bool sameAuthError = (m_request.responseCode == m_request.prevResponseCode); qCDebug(KIO_HTTP) << "wasAuthError=" << wasAuthError << "isAuthError=" << isAuthError << "sameAuthError=" << sameAuthError; // Not the same authorization error as before and no generic error? // -> save the successful credentials. if (wasAuthError && (m_request.responseCode < 400 || (isAuthError && !sameAuthError))) { saveAuthenticationData(); } // done with the first line; now tokenize the other lines // TODO review use of STRTOLL vs. QByteArray::toInt() foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 2); qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).trimmed(); // Use this to see newlines: //qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).replace("\r", "\\r").replace("\n", "\\n\n"); Q_ASSERT(foundDelimiter); //NOTE because tokenizer will overwrite newlines in case of line continuations in the header // unread(buffer, bufSize) will not generally work anymore. we don't need it either. // either we have a http response line -> try to parse the header, fail if it doesn't work // or we have garbage -> fail. HeaderTokenizer tokenizer(buffer); tokenizer.tokenize(idx, sizeof(buffer)); // Note that not receiving "accept-ranges" means that all bets are off // wrt the server supporting ranges. TokenIterator tIt = tokenizer.iterator("accept-ranges"); if (tIt.hasNext() && tIt.next().toLower().startsWith("none")) { // krazy:exclude=strings bCanResume = false; } tIt = tokenizer.iterator("keep-alive"); while (tIt.hasNext()) { QByteArray ka = tIt.next().trimmed().toLower(); if (ka.startsWith("timeout=")) { // krazy:exclude=strings int ka_timeout = ka.mid(qstrlen("timeout=")).trimmed().toInt(); if (ka_timeout > 0) { m_request.keepAliveTimeout = ka_timeout; } if (httpRev == HTTP_10) { m_request.isKeepAlive = true; } break; // we want to fetch ka timeout only } } // get the size of our data tIt = tokenizer.iterator("content-length"); if (tIt.hasNext()) { m_iSize = STRTOLL(tIt.next().constData(), nullptr, 10); } tIt = tokenizer.iterator("content-location"); if (tIt.hasNext()) { setMetaData(QStringLiteral("content-location"), toQString(tIt.next().trimmed())); } // which type of data do we have? QString mediaValue; QString mediaAttribute; tIt = tokenizer.iterator("content-type"); if (tIt.hasNext()) { QList l = tIt.next().split(';'); if (!l.isEmpty()) { // Assign the mime-type. m_mimeType = toQString(l.first().trimmed().toLower()); if (m_mimeType.startsWith(QLatin1Char('"'))) { m_mimeType.remove(0, 1); } if (m_mimeType.endsWith(QLatin1Char('"'))) { m_mimeType.chop(1); } qCDebug(KIO_HTTP) << "Content-type:" << m_mimeType; l.removeFirst(); } // If we still have text, then it means we have a mime-type with a // parameter (eg: charset=iso-8851) ; so let's get that... Q_FOREACH (const QByteArray &statement, l) { const int index = statement.indexOf('='); if (index <= 0) { mediaAttribute = toQString(statement.mid(0, index)); } else { mediaAttribute = toQString(statement.mid(0, index)); mediaValue = toQString(statement.mid(index + 1)); } mediaAttribute = mediaAttribute.trimmed(); mediaValue = mediaValue.trimmed(); bool quoted = false; if (mediaValue.startsWith(QLatin1Char('"'))) { quoted = true; mediaValue.remove(0, 1); } if (mediaValue.endsWith(QLatin1Char('"'))) { mediaValue.chop(1); } qCDebug(KIO_HTTP) << "Encoding-type:" << mediaAttribute << "=" << mediaValue; if (mediaAttribute == QLatin1String("charset")) { mediaValue = mediaValue.toLower(); m_request.cacheTag.charset = mediaValue; setMetaData(QStringLiteral("charset"), mediaValue); } else { setMetaData(QLatin1String("media-") + mediaAttribute, mediaValue); if (quoted) { setMetaData(QLatin1String("media-") + mediaAttribute + QLatin1String("-kio-quoted"), QStringLiteral("true")); } } } } // content? tIt = tokenizer.iterator("content-encoding"); while (tIt.hasNext()) { // This is so wrong !! No wonder kio_http is stripping the // gzip encoding from downloaded files. This solves multiple // bug reports and caitoo's problem with downloads when such a // header is encountered... // A quote from RFC 2616: // " When present, its (Content-Encoding) value indicates what additional // content have been applied to the entity body, and thus what decoding // mechanism must be applied to obtain the media-type referenced by the // Content-Type header field. Content-Encoding is primarily used to allow // a document to be compressed without loosing the identity of its underlying // media type. Simply put if it is specified, this is the actual mime-type // we should use when we pull the resource !!! addEncoding(toQString(tIt.next()), m_contentEncodings); } // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183 tIt = tokenizer.iterator("content-disposition"); if (tIt.hasNext()) { parseContentDisposition(toQString(tIt.next())); } tIt = tokenizer.iterator("content-language"); if (tIt.hasNext()) { QString language = toQString(tIt.next().trimmed()); if (!language.isEmpty()) { setMetaData(QStringLiteral("content-language"), language); } } tIt = tokenizer.iterator("proxy-connection"); if (tIt.hasNext() && isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QByteArray pc = tIt.next().toLower(); if (pc.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (pc.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } tIt = tokenizer.iterator("link"); if (tIt.hasNext()) { // We only support Link: ; rel="type" so far QStringList link = toQString(tIt.next()).split(QLatin1Char(';'), QString::SkipEmptyParts); if (link.count() == 2) { QString rel = link[1].trimmed(); if (rel.startsWith(QLatin1String("rel=\""))) { rel = rel.mid(5, rel.length() - 6); if (rel.toLower() == QLatin1String("pageservices")) { //### the remove() part looks fishy! QString url = link[0].remove(QRegExp(QStringLiteral("[<>]"))).trimmed(); setMetaData(QStringLiteral("PageServices"), url); } } } } tIt = tokenizer.iterator("p3p"); if (tIt.hasNext()) { // P3P privacy policy information QStringList policyrefs, compact; while (tIt.hasNext()) { QStringList policy = toQString(tIt.next().simplified()) .split(QLatin1Char('='), QString::SkipEmptyParts); if (policy.count() == 2) { if (policy[0].toLower() == QLatin1String("policyref")) { policyrefs << policy[1].remove(QRegExp(QStringLiteral("[\")\']"))).trimmed(); } else if (policy[0].toLower() == QLatin1String("cp")) { // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with // other metadata sent in strings. This could be a bit more // efficient but I'm going for correctness right now. const QString s = policy[1].remove(QRegExp(QStringLiteral("[\")\']"))); const QStringList cps = s.split(QLatin1Char(' '), QString::SkipEmptyParts); compact << cps; } } } if (!policyrefs.isEmpty()) { setMetaData(QStringLiteral("PrivacyPolicy"), policyrefs.join(QLatin1Char('\n'))); } if (!compact.isEmpty()) { setMetaData(QStringLiteral("PrivacyCompactPolicy"), compact.join(QLatin1Char('\n'))); } } // continue only if we know that we're at least HTTP/1.0 if (httpRev == HTTP_11 || httpRev == HTTP_10) { // let them tell us if we should stay alive or not tIt = tokenizer.iterator("connection"); while (tIt.hasNext()) { QByteArray connection = tIt.next().toLower(); if (!(isHttpProxy(m_request.proxyUrl) && !isAutoSsl())) { if (connection.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (connection.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } if (connection.startsWith("upgrade")) { // krazy:exclude=strings if (m_request.responseCode == 101) { // Ok, an upgrade was accepted, now we must do it upgradeRequired = true; } else if (upgradeRequired) { // 426 // Nothing to do since we did it above already } } } // what kind of encoding do we have? transfer? tIt = tokenizer.iterator("transfer-encoding"); while (tIt.hasNext()) { // If multiple encodings have been applied to an entity, the // transfer-codings MUST be listed in the order in which they // were applied. addEncoding(toQString(tIt.next().trimmed()), m_transferEncodings); } // md5 signature tIt = tokenizer.iterator("content-md5"); if (tIt.hasNext()) { m_contentMD5 = toQString(tIt.next().trimmed()); } // *** Responses to the HTTP OPTIONS method follow // WebDAV capabilities tIt = tokenizer.iterator("dav"); while (tIt.hasNext()) { m_davCapabilities << toQString(tIt.next()); } // *** Responses to the HTTP OPTIONS method finished } // Now process the HTTP/1.1 upgrade QStringList upgradeOffers; tIt = tokenizer.iterator("upgrade"); if (tIt.hasNext()) { // Now we have to check to see what is offered for the upgrade QString offered = toQString(tIt.next()); upgradeOffers = offered.split(QRegExp(QStringLiteral("[ \n,\r\t]")), QString::SkipEmptyParts); } Q_FOREACH (const QString &opt, upgradeOffers) { if (opt == QLatin1String("TLS/1.0")) { if (!startSsl() && upgradeRequired) { error(ERR_UPGRADE_REQUIRED, opt); return false; } } else if (opt == QLatin1String("HTTP/1.1")) { httpRev = HTTP_11; } else if (upgradeRequired) { // we are told to do an upgrade we don't understand error(ERR_UPGRADE_REQUIRED, opt); return false; } } // Harvest cookies (mmm, cookie fields!) QByteArray cookieStr; // In case we get a cookie. tIt = tokenizer.iterator("set-cookie"); while (tIt.hasNext()) { cookieStr += "Set-Cookie: " + tIt.next() + '\n'; } if (!cookieStr.isEmpty()) { if ((m_request.cookieMode == HTTPRequest::CookiesAuto) && m_request.useCookieJar) { // Give cookies to the cookiejar. const QString domain = config()->readEntry("cross-domain"); if (!domain.isEmpty() && isCrossDomainRequest(m_request.url.host(), domain)) { cookieStr = "Cross-Domain\n" + cookieStr; } addCookies(m_request.url.toString(), cookieStr); } else if (m_request.cookieMode == HTTPRequest::CookiesManual) { // Pass cookie to application setMetaData(QStringLiteral("setcookies"), QString::fromUtf8(cookieStr)); // ## is encoding ok? } } // We need to reread the header if we got a '100 Continue' or '102 Processing' // This may be a non keepalive connection so we handle this kind of loop internally if (cont) { qCDebug(KIO_HTTP) << "cont; returning to mark try_again"; goto try_again; } if (!m_isChunked && (m_iSize == NO_SIZE) && m_request.isKeepAlive && canHaveResponseBody(m_request.responseCode, m_request.method)) { qCDebug(KIO_HTTP) << "Ignoring keep-alive: otherwise unable to determine response body length."; m_request.isKeepAlive = false; } // TODO cache the proxy auth data (not doing this means a small performance regression for now) // we may need to send (Proxy or WWW) authorization data if ((!m_request.doNotWWWAuthenticate && m_request.responseCode == 401) || (!m_request.doNotProxyAuthenticate && m_request.responseCode == 407)) { authRequiresAnotherRoundtrip = handleAuthenticationHeader(&tokenizer); if (m_kioError) { // If error is set, then handleAuthenticationHeader failed. return false; } } else { authRequiresAnotherRoundtrip = false; } QString locationStr; // In fact we should do redirection only if we have a redirection response code (300 range) tIt = tokenizer.iterator("location"); if (tIt.hasNext() && m_request.responseCode > 299 && m_request.responseCode < 400) { locationStr = QString::fromUtf8(tIt.next().trimmed()); } // We need to do a redirect if (!locationStr.isEmpty()) { QUrl u = m_request.url.resolved(QUrl(locationStr)); if (!u.isValid()) { error(ERR_MALFORMED_URL, u.toDisplayString()); return false; } // preserve #ref: (bug 124654) // if we were at http://host/resource1#ref, we sent a GET for "/resource1" // if we got redirected to http://host/resource2, then we have to re-add // the fragment: // http to https redirection included if (m_request.url.hasFragment() && !u.hasFragment() && (m_request.url.host() == u.host()) && (m_request.url.scheme() == u.scheme() || (m_request.url.scheme() == QLatin1String("http") && u.scheme() == QLatin1String("https")))) { u.setFragment(m_request.url.fragment()); } m_isRedirection = true; if (!m_request.id.isEmpty()) { sendMetaData(); } // If we're redirected to a http:// url, remember that we're doing webdav... if (m_protocol == "webdav" || m_protocol == "webdavs") { if (u.scheme() == QLatin1String("http")) { u.setScheme(QStringLiteral("webdav")); } else if (u.scheme() == QLatin1String("https")) { u.setScheme(QStringLiteral("webdavs")); } m_request.redirectUrl = u; } qCDebug(KIO_HTTP) << "Re-directing from" << m_request.url << "to" << u; redirection(u); // It would be hard to cache the redirection response correctly. The possible benefit // is small (if at all, assuming fast disk and slow network), so don't do it. cacheFileClose(); setCacheabilityMetadata(false); } // Inform the job that we can indeed resume... if (bCanResume && m_request.offset) { //TODO turn off caching??? canResume(); } else { m_request.offset = 0; } // Correct a few common wrong content encodings fixupResponseContentEncoding(); // Correct some common incorrect pseudo-mimetypes fixupResponseMimetype(); // parse everything related to expire and other dates, and cache directives; also switch // between cache reading and writing depending on cache validation result. cacheParseResponseHeader(tokenizer); } if (m_request.cacheTag.ioMode == ReadFromCache) { if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { qCDebug(KIO_HTTP) << "Reading resource from cache even though the cache plan is not " "UseCached; the server is probably sending wrong expiry information."; } // parseHeaderFromCache replaces this method in case of cached content return parseHeaderFromCache(); } if (config()->readEntry("PropagateHttpHeader", false) || m_request.cacheTag.ioMode == WriteToCache) { // store header lines if they will be used; note that the tokenizer removing // line continuation special cases is probably more good than bad. int nextLinePos = 0; int prevLinePos = 0; bool haveMore = true; while (haveMore) { haveMore = nextLine(buffer, &nextLinePos, bufPos); int prevLineEnd = nextLinePos; while (buffer[prevLineEnd - 1] == '\r' || buffer[prevLineEnd - 1] == '\n') { prevLineEnd--; } m_responseHeaders.append(QString::fromLatin1(&buffer[prevLinePos], prevLineEnd - prevLinePos)); prevLinePos = nextLinePos; } // IMPORTANT: Do not remove this line because forwardHttpResponseHeader // is called below. This line is here to ensure the response headers are // available to the client before it receives mimetype information. // The support for putting ioslaves on hold in the KIO-QNAM integration // will break if this line is removed. setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); } // Let the app know about the mime-type iff this is not a redirection and // the mime-type string is not empty. if (!m_isRedirection && m_request.responseCode != 204 && (!m_mimeType.isEmpty() || m_request.method == HTTP_HEAD) && !m_kioError && (m_isLoadingErrorPage || !authRequiresAnotherRoundtrip)) { qCDebug(KIO_HTTP) << "Emitting mimetype " << m_mimeType; mimeType(m_mimeType); } // IMPORTANT: Do not move the function call below before doing any // redirection. Otherwise it might mess up some sites, see BR# 150904. forwardHttpResponseHeader(); if (m_request.method == HTTP_HEAD) { return true; } return !authRequiresAnotherRoundtrip; // return true if no more credentials need to be sent } void HTTPProtocol::parseContentDisposition(const QString &disposition) { const QMap parameters = contentDispositionParser(disposition); QMap::const_iterator i = parameters.constBegin(); while (i != parameters.constEnd()) { setMetaData(QLatin1String("content-disposition-") + i.key(), i.value()); qCDebug(KIO_HTTP) << "Content-Disposition:" << i.key() << "=" << i.value(); ++i; } } void HTTPProtocol::addEncoding(const QString &_encoding, QStringList &encs) { QString encoding = _encoding.trimmed().toLower(); // Identity is the same as no encoding if (encoding == QLatin1String("identity")) { return; } else if (encoding == QLatin1String("8bit")) { // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de return; } else if (encoding == QLatin1String("chunked")) { m_isChunked = true; // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints? //if ( m_cmd != CMD_COPY ) m_iSize = NO_SIZE; } else if ((encoding == QLatin1String("x-gzip")) || (encoding == QLatin1String("gzip"))) { encs.append(QStringLiteral("gzip")); } else if ((encoding == QLatin1String("x-bzip2")) || (encoding == QLatin1String("bzip2"))) { encs.append(QStringLiteral("bzip2")); // Not yet supported! } else if ((encoding == QLatin1String("x-deflate")) || (encoding == QLatin1String("deflate"))) { encs.append(QStringLiteral("deflate")); } else { qCDebug(KIO_HTTP) << "Unknown encoding encountered. " << "Please write code. Encoding =" << encoding; } } void HTTPProtocol::cacheParseResponseHeader(const HeaderTokenizer &tokenizer) { if (!m_request.cacheTag.useCache) { return; } // might have to add more response codes if (m_request.responseCode != 200 && m_request.responseCode != 304) { return; } m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); const QDateTime currentDate = QDateTime::currentDateTime(); bool mayCache = m_request.cacheTag.ioMode != NoCache; TokenIterator tIt = tokenizer.iterator("last-modified"); if (tIt.hasNext()) { m_request.cacheTag.lastModifiedDate = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); //### might be good to canonicalize the date by using QDateTime::toString() if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), toQString(tIt.current())); } } // determine from available information when the response was served by the origin server { QDateTime dateHeader; tIt = tokenizer.iterator("date"); if (tIt.hasNext()) { dateHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); // -1 on error } qint64 ageHeader = 0; tIt = tokenizer.iterator("age"); if (tIt.hasNext()) { ageHeader = tIt.next().toLongLong(); // 0 on error } if (dateHeader.isValid()) { m_request.cacheTag.servedDate = dateHeader; } else if (ageHeader) { m_request.cacheTag.servedDate = currentDate.addSecs(-ageHeader); } else { m_request.cacheTag.servedDate = currentDate; } } bool hasCacheDirective = false; // determine when the response "expires", i.e. becomes stale and needs revalidation { // (we also parse other cache directives here) qint64 maxAgeHeader = 0; tIt = tokenizer.iterator("cache-control"); while (tIt.hasNext()) { QByteArray cacheStr = tIt.next().toLower(); if (cacheStr.startsWith("no-cache") || cacheStr.startsWith("no-store")) { // krazy:exclude=strings // Don't put in cache mayCache = false; hasCacheDirective = true; } else if (cacheStr.startsWith("max-age=")) { // krazy:exclude=strings QByteArray ba = cacheStr.mid(qstrlen("max-age=")).trimmed(); bool ok = false; maxAgeHeader = ba.toLongLong(&ok); if (ok) { hasCacheDirective = true; } } } QDateTime expiresHeader; tIt = tokenizer.iterator("expires"); if (tIt.hasNext()) { expiresHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); qCDebug(KIO_HTTP) << "parsed expire date from 'expires' header:" << tIt.current(); } if (maxAgeHeader) { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(maxAgeHeader); } else if (expiresHeader.isValid()) { m_request.cacheTag.expireDate = expiresHeader; } else { // heuristic expiration date if (m_request.cacheTag.lastModifiedDate.isValid()) { // expAge is following the RFC 2616 suggestion for heuristic expiration qint64 expAge = (m_request.cacheTag.lastModifiedDate.secsTo(m_request.cacheTag.servedDate)) / 10; // not in the RFC: make sure not to have a huge heuristic cache lifetime expAge = qMin(expAge, qint64(3600 * 24)); m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(expAge); } else { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(DEFAULT_CACHE_EXPIRE); } } // make sure that no future clock monkey business causes the cache entry to un-expire if (m_request.cacheTag.expireDate < currentDate) { m_request.cacheTag.expireDate.setMSecsSinceEpoch(0); // January 1, 1970 :) } } tIt = tokenizer.iterator("etag"); if (tIt.hasNext()) { QString prevEtag = m_request.cacheTag.etag; m_request.cacheTag.etag = toQString(tIt.next()); if (m_request.cacheTag.etag != prevEtag && m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "304 Not Modified but new entity tag - I don't think this is legal HTTP."; } } // whoops.. we received a warning tIt = tokenizer.iterator("warning"); if (tIt.hasNext()) { //Don't use warning() here, no need to bother the user. //Those warnings are mostly about caches. infoMessage(toQString(tIt.next())); } // Cache management (HTTP 1.0) tIt = tokenizer.iterator("pragma"); while (tIt.hasNext()) { if (tIt.next().toLower().startsWith("no-cache")) { // krazy:exclude=strings mayCache = false; hasCacheDirective = true; } } // The deprecated Refresh Response tIt = tokenizer.iterator("refresh"); if (tIt.hasNext()) { mayCache = false; setMetaData(QStringLiteral("http-refresh"), toQString(tIt.next().trimmed())); } // We don't cache certain text objects if (m_mimeType.startsWith(QLatin1String("text/")) && (m_mimeType != QLatin1String("text/css")) && (m_mimeType != QLatin1String("text/x-javascript")) && !hasCacheDirective) { // Do not cache secure pages or pages // originating from password protected sites // unless the webserver explicitly allows it. if (isUsingSsl() || m_wwwAuth) { mayCache = false; } } // note that we've updated cacheTag, so the plan() is with current data if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { qCDebug(KIO_HTTP) << "Cache needs validation"; if (m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "...was revalidated by response code but not by updated expire times. " "We're going to set the expire date to 60 seconds in the future..."; m_request.cacheTag.expireDate = currentDate.addSecs(60); if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { // "apparently" because we /could/ have made an error ourselves, but the errors I // witnessed were all the server's fault. qCDebug(KIO_HTTP) << "this proxy or server apparently sends bogus expiry information."; } } } // validation handling if (mayCache && m_request.responseCode == 200 && !m_mimeType.isEmpty()) { qCDebug(KIO_HTTP) << "Cache, adding" << m_request.url; // ioMode can still be ReadFromCache here if we're performing a conditional get // aka validation m_request.cacheTag.ioMode = WriteToCache; if (!cacheFileOpenWrite()) { qCDebug(KIO_HTTP) << "Error creating cache entry for" << m_request.url << "!"; } m_maxCacheSize = config()->readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE); } else if (m_request.responseCode == 304 && m_request.cacheTag.file) { if (!mayCache) { qCDebug(KIO_HTTP) << "This webserver is confused about the cacheability of the data it sends."; } // the cache file should still be open for reading, see satisfyRequestFromCache(). Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); } else { cacheFileClose(); } setCacheabilityMetadata(mayCache); } void HTTPProtocol::setCacheabilityMetadata(bool cachingAllowed) { if (!cachingAllowed) { setMetaData(QStringLiteral("no-cache"), QStringLiteral("true")); setMetaData(QStringLiteral("expire-date"), QStringLiteral("1")); // Expired } else { QString tmp; tmp.setNum(m_request.cacheTag.expireDate.toTime_t()); setMetaData(QStringLiteral("expire-date"), tmp); // slightly changed semantics from old creationDate, probably more correct now tmp.setNum(m_request.cacheTag.servedDate.toTime_t()); setMetaData(QStringLiteral("cache-creation-date"), tmp); } } bool HTTPProtocol::sendCachedBody() { infoMessage(i18n("Sending data to %1", m_request.url.host())); const qint64 size = m_POSTbuf->size(); const QByteArray cLength = "Content-Length: " + QByteArray::number(size) + "\r\n\r\n"; //qDebug() << "sending cached data (size=" << size << ")"; // Send the content length... bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size()); if (!sendOk) { qCDebug(KIO_HTTP) << "Connection broken when sending " << "content length: (" << m_request.url.host() << ")"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } totalSize(size); // Make sure the read head is at the beginning... m_POSTbuf->reset(); KIO::filesize_t totalBytesSent = 0; // Send the data... while (!m_POSTbuf->atEnd()) { const QByteArray buffer = m_POSTbuf->read(65536); const ssize_t bytesSent = write(buffer.data(), buffer.size()); if (bytesSent != static_cast(buffer.size())) { qCDebug(KIO_HTTP) << "Connection broken when sending message body: (" << m_request.url.host() << ")"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } totalBytesSent += bytesSent; processedSize(totalBytesSent); } return true; } bool HTTPProtocol::sendBody() { // If we have cached data, the it is either a repost or a DAV request so send // the cached data... if (m_POSTbuf) { return sendCachedBody(); } if (m_iPostDataSize == NO_SIZE) { // Try the old approach of retrieving content data from the job // before giving up. if (retrieveAllData()) { return sendCachedBody(); } error(ERR_POST_NO_SIZE, m_request.url.host()); return false; } qCDebug(KIO_HTTP) << "sending data (size=" << m_iPostDataSize << ")"; infoMessage(i18n("Sending data to %1", m_request.url.host())); const QByteArray cLength = "Content-Length: " + QByteArray::number(m_iPostDataSize) + "\r\n\r\n"; qCDebug(KIO_HTTP) << cLength.trimmed(); // Send the content length... bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size()); if (!sendOk) { // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "Connection broken while sending POST content size to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // Send the amount totalSize(m_iPostDataSize); // If content-length is 0, then do nothing but simply return true. if (m_iPostDataSize == 0) { return true; } sendOk = true; KIO::filesize_t bytesSent = 0; while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); // On done... if (bytesRead == 0) { sendOk = (bytesSent == m_iPostDataSize); break; } // On error return false... if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); sendOk = false; break; } // Cache the POST data in case of a repost request. cachePostData(buffer); // This will only happen if transmitting the data fails, so we will simply // cache the content locally for the potential re-transmit... if (!sendOk) { continue; } if (write(buffer.data(), bytesRead) == static_cast(bytesRead)) { bytesSent += bytesRead; processedSize(bytesSent); // Send update status... continue; } qCDebug(KIO_HTTP) << "Connection broken while sending POST content to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); sendOk = false; } return sendOk; } void HTTPProtocol::httpClose(bool keepAlive) { qCDebug(KIO_HTTP) << "keepAlive =" << keepAlive; cacheFileClose(); // Only allow persistent connections for GET requests. // NOTE: we might even want to narrow this down to non-form // based submit requests which will require a meta-data from // khtml. if (keepAlive) { if (!m_request.keepAliveTimeout) { m_request.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; } else if (m_request.keepAliveTimeout > 2 * DEFAULT_KEEP_ALIVE_TIMEOUT) { m_request.keepAliveTimeout = 2 * DEFAULT_KEEP_ALIVE_TIMEOUT; } qCDebug(KIO_HTTP) << "keep alive (" << m_request.keepAliveTimeout << ")"; QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << int(99); // special: Close connection setTimeoutSpecialCommand(m_request.keepAliveTimeout, data); return; } httpCloseConnection(); } void HTTPProtocol::closeConnection() { qCDebug(KIO_HTTP); httpCloseConnection(); } void HTTPProtocol::httpCloseConnection() { qCDebug(KIO_HTTP); m_server.clear(); disconnectFromHost(); clearUnreadBuffer(); setTimeoutSpecialCommand(-1); // Cancel any connection timeout } void HTTPProtocol::slave_status() { qCDebug(KIO_HTTP); if (!isConnected()) { httpCloseConnection(); } slaveStatus(m_server.url.host(), isConnected()); } void HTTPProtocol::mimetype(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_HEAD; m_request.cacheTag.policy = CC_Cache; if (proceedUntilResponseHeader()) { httpClose(m_request.isKeepAlive); finished(); } qCDebug(KIO_HTTP) << m_mimeType; } void HTTPProtocol::special(const QByteArray &data) { qCDebug(KIO_HTTP); int tmp; QDataStream stream(data); stream >> tmp; switch (tmp) { case 1: { // HTTP POST QUrl url; qint64 size; stream >> url >> size; post(url, size); break; } case 2: { // cache_update QUrl url; bool no_cache; qint64 expireDate; stream >> url >> no_cache >> expireDate; if (no_cache) { QString filename = cacheFilePathFromUrl(url); // there is a tiny risk of deleting the wrong file due to hash collisions here. // this is an unimportant performance issue. // FIXME on Windows we may be unable to delete the file if open QFile::remove(filename); finished(); break; } // let's be paranoid and inefficient here... HTTPRequest savedRequest = m_request; m_request.url = url; if (cacheFileOpenRead()) { m_request.cacheTag.expireDate.setTime_t(expireDate); cacheFileClose(); // this sends an update command to the cache cleaner process } m_request = savedRequest; finished(); break; } case 5: { // WebDAV lock QUrl url; QString scope, type, owner; stream >> url >> scope >> type >> owner; davLock(url, scope, type, owner); break; } case 6: { // WebDAV unlock QUrl url; stream >> url; davUnlock(url); break; } case 7: { // Generic WebDAV QUrl url; int method; qint64 size; stream >> url >> method >> size; davGeneric(url, (KIO::HTTP_METHOD) method, size); break; } case 99: { // Close Connection httpCloseConnection(); break; } default: // Some command we don't understand. // Just ignore it, it may come from some future version of KDE. break; } } /** * Read a chunk from the data stream. */ int HTTPProtocol::readChunked() { if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) { // discard CRLF from previous chunk, if any, and read size of next chunk int bufPos = 0; m_receiveBuf.resize(4096); bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); if (foundCrLf && bufPos == 2) { // The previous read gave us the CRLF from the previous chunk. As bufPos includes // the trailing CRLF it has to be > 2 to possibly include the next chunksize. bufPos = 0; foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); } if (!foundCrLf) { qCDebug(KIO_HTTP) << "Failed to read chunk header."; return -1; } Q_ASSERT(bufPos > 2); long long nextChunkSize = STRTOLL(m_receiveBuf.data(), nullptr, 16); if (nextChunkSize < 0) { qCDebug(KIO_HTTP) << "Negative chunk size"; return -1; } m_iBytesLeft = nextChunkSize; qCDebug(KIO_HTTP) << "Chunk size =" << m_iBytesLeft << "bytes"; if (m_iBytesLeft == 0) { // Last chunk; read and discard chunk trailer. // The last trailer line ends with CRLF and is followed by another CRLF // so we have CRLFCRLF like at the end of a standard HTTP header. // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes. //NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over. char trash[4096]; trash[0] = m_receiveBuf.constData()[bufPos - 2]; trash[1] = m_receiveBuf.constData()[bufPos - 1]; int trashBufPos = 2; bool done = false; while (!done && !m_isEOF) { if (trashBufPos > 3) { // shift everything but the last three bytes out of the buffer for (int i = 0; i < 3; i++) { trash[i] = trash[trashBufPos - 3 + i]; } trashBufPos = 3; } done = readDelimitedText(trash, &trashBufPos, 4096, 2); } if (m_isEOF && !done) { qCDebug(KIO_HTTP) << "Failed to read chunk trailer."; return -1; } return 0; } } int bytesReceived = readLimited(); if (!m_iBytesLeft) { m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk } return bytesReceived; } int HTTPProtocol::readLimited() { if (!m_iBytesLeft) { return 0; } m_receiveBuf.resize(4096); int bytesToReceive; if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size())) { bytesToReceive = m_receiveBuf.size(); } else { bytesToReceive = m_iBytesLeft; } const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false); if (bytesReceived <= 0) { return -1; // Error: connection lost } m_iBytesLeft -= bytesReceived; return bytesReceived; } int HTTPProtocol::readUnlimited() { if (m_request.isKeepAlive) { qCDebug(KIO_HTTP) << "Unbounded datastream on a Keep-alive connection!"; m_request.isKeepAlive = false; } m_receiveBuf.resize(4096); int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size()); if (result > 0) { return result; } m_isEOF = true; m_iBytesLeft = 0; return 0; } void HTTPProtocol::slotData(const QByteArray &_d) { if (!_d.size()) { m_isEOD = true; return; } if (m_iContentLeft != NO_SIZE) { if (m_iContentLeft >= KIO::filesize_t(_d.size())) { m_iContentLeft -= _d.size(); } else { m_iContentLeft = NO_SIZE; } } QByteArray d = _d; if (!m_dataInternal) { // If a broken server does not send the mime-type, // we try to id it from the content before dealing // with the content itself. if (m_mimeType.isEmpty() && !m_isRedirection && !(m_request.responseCode >= 300 && m_request.responseCode <= 399)) { qCDebug(KIO_HTTP) << "Determining mime-type from content..."; int old_size = m_mimeTypeBuffer.size(); m_mimeTypeBuffer.resize(old_size + d.size()); memcpy(m_mimeTypeBuffer.data() + old_size, d.data(), d.size()); if ((m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) && (m_mimeTypeBuffer.size() < 1024)) { m_cpMimeBuffer = true; return; // Do not send up the data since we do not yet know its mimetype! } qCDebug(KIO_HTTP) << "Mimetype buffer size:" << m_mimeTypeBuffer.size(); QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(m_request.url.adjusted(QUrl::StripTrailingSlash).path(), m_mimeTypeBuffer); if (mime.isValid() && !mime.isDefault()) { m_mimeType = mime.name(); qCDebug(KIO_HTTP) << "Mimetype from content:" << m_mimeType; } if (m_mimeType.isEmpty()) { m_mimeType = QStringLiteral(DEFAULT_MIME_TYPE); qCDebug(KIO_HTTP) << "Using default mimetype:" << m_mimeType; } //### we could also open the cache file here if (m_cpMimeBuffer) { d.resize(0); d.resize(m_mimeTypeBuffer.size()); memcpy(d.data(), m_mimeTypeBuffer.data(), d.size()); } mimeType(m_mimeType); m_mimeTypeBuffer.resize(0); } //qDebug() << "Sending data of size" << d.size(); data(d); if (m_request.cacheTag.ioMode == WriteToCache) { cacheFileWritePayload(d); } } else { uint old_size = m_webDavDataBuf.size(); m_webDavDataBuf.resize(old_size + d.size()); memcpy(m_webDavDataBuf.data() + old_size, d.data(), d.size()); } } /** * This function is our "receive" function. It is responsible for * downloading the message (not the header) from the HTTP server. It * is called either as a response to a client's KIOJob::dataEnd() * (meaning that the client is done sending data) or by 'sendQuery()' * (if we are in the process of a PUT/POST request). It can also be * called by a webDAV function, to receive stat/list/property/etc. * data; in this case the data is stored in m_webDavDataBuf. */ bool HTTPProtocol::readBody(bool dataInternal /* = false */) { // special case for reading cached body since we also do it in this function. oh well. if (!canHaveResponseBody(m_request.responseCode, m_request.method) && !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 && m_request.method != HTTP_HEAD)) { return true; } m_isEOD = false; // Note that when dataInternal is true, we are going to: // 1) save the body data to a member variable, m_webDavDataBuf // 2) _not_ advertise the data, speed, size, etc., through the // corresponding functions. // This is used for returning data to WebDAV. m_dataInternal = dataInternal; if (dataInternal) { m_webDavDataBuf.clear(); } // Check if we need to decode the data. // If we are in copy mode, then use only transfer decoding. bool useMD5 = !m_contentMD5.isEmpty(); // Deal with the size of the file. KIO::filesize_t sz = m_request.offset; if (sz) { m_iSize += sz; } if (!m_isRedirection) { // Update the application with total size except when // it is compressed, or when the data is to be handled // internally (webDAV). If compressed we have to wait // until we uncompress to find out the actual data size if (!dataInternal) { if ((m_iSize > 0) && (m_iSize != NO_SIZE)) { totalSize(m_iSize); infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize), m_request.url.host())); } else { totalSize(0); } } if (m_request.cacheTag.ioMode == ReadFromCache) { qCDebug(KIO_HTTP) << "reading data from cache..."; m_iContentLeft = NO_SIZE; QByteArray d; while (true) { d = cacheFileReadPayload(MAX_IPC_SIZE); if (d.isEmpty()) { break; } slotData(d); sz += d.size(); if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); if (!dataInternal) { data(QByteArray()); } return true; } } if (m_iSize != NO_SIZE) { m_iBytesLeft = m_iSize - sz; } else { m_iBytesLeft = NO_SIZE; } m_iContentLeft = m_iBytesLeft; if (m_isChunked) { m_iBytesLeft = NO_SIZE; } qCDebug(KIO_HTTP) << KIO::number(m_iBytesLeft) << "bytes left."; // Main incoming loop... Gather everything while we can... m_cpMimeBuffer = false; m_mimeTypeBuffer.resize(0); HTTPFilterChain chain; // redirection ignores the body if (!m_isRedirection) { QObject::connect(&chain, &HTTPFilterBase::output, this, &HTTPProtocol::slotData); } QObject::connect(&chain, &HTTPFilterBase::error, this, &HTTPProtocol::slotFilterError); // decode all of the transfer encodings while (!m_transferEncodings.isEmpty()) { QString enc = m_transferEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } // From HTTP 1.1 Draft 6: // The MD5 digest is computed based on the content of the entity-body, // including any content-coding that has been applied, but not including // any transfer-encoding applied to the message-body. If the message is // received with a transfer-encoding, that encoding MUST be removed // prior to checking the Content-MD5 value against the received entity. HTTPFilterMD5 *md5Filter = nullptr; if (useMD5) { md5Filter = new HTTPFilterMD5; chain.addFilter(md5Filter); } // now decode all of the content encodings // -- Why ?? We are not // -- a proxy server, be a client side implementation!! The applications // -- are capable of determining how to extract the encoded implementation. // WB: That's a misunderstanding. We are free to remove the encoding. // WB: Some braindead www-servers however, give .tgz files an encoding // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar" // WB: They shouldn't do that. We can work around that though... while (!m_contentEncodings.isEmpty()) { QString enc = m_contentEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } while (!m_isEOF) { int bytesReceived; if (m_isChunked) { bytesReceived = readChunked(); } else if (m_iSize != NO_SIZE) { bytesReceived = readLimited(); } else { bytesReceived = readUnlimited(); } // make sure that this wasn't an error, first qCDebug(KIO_HTTP) << "bytesReceived:" << bytesReceived << " m_iSize:" << (int)m_iSize << " Chunked:" << m_isChunked << " BytesLeft:"<< (int)m_iBytesLeft; if (bytesReceived == -1) { if (m_iContentLeft == 0) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; break; } // Oh well... log an error and bug out qCDebug(KIO_HTTP) << "bytesReceived==-1 sz=" << (int)sz << " Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // I guess that nbytes == 0 isn't an error.. but we certainly // won't work with it! if (bytesReceived > 0) { // Important: truncate the buffer to the actual size received! // Otherwise garbage will be passed to the app m_receiveBuf.truncate(bytesReceived); chain.slotInput(m_receiveBuf); if (m_kioError) { return false; } sz += bytesReceived; if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); // res if (m_iBytesLeft && m_isEOD && !m_isChunked) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; } if (m_iBytesLeft == 0) { qCDebug(KIO_HTTP) << "EOD received! Left ="<< KIO::number(m_iBytesLeft); break; } } chain.slotInput(QByteArray()); // Flush chain. if (useMD5) { QString calculatedMD5 = md5Filter->md5(); if (m_contentMD5 != calculatedMD5) qCWarning(KIO_HTTP) << "MD5 checksum MISMATCH! Expected:" << calculatedMD5 << ", Got:" << m_contentMD5; } // Close cache entry if (m_iBytesLeft == 0) { cacheFileClose(); // no-op if not necessary } if (!dataInternal && sz <= 1) { if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { error(ERR_INTERNAL_SERVER, m_request.url.host()); return false; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { error(ERR_DOES_NOT_EXIST, m_request.url.host()); return false; } } if (!dataInternal && !m_isRedirection) { data(QByteArray()); } return true; } void HTTPProtocol::slotFilterError(const QString &text) { error(KIO::ERR_SLAVE_DEFINED, text); } void HTTPProtocol::error(int _err, const QString &_text) { // Close the connection only on connection errors. Otherwise, honor the // keep alive flag. if (_err == ERR_CONNECTION_BROKEN || _err == ERR_CANNOT_CONNECT) { httpClose(false); } else { httpClose(m_request.isKeepAlive); } if (!m_request.id.isEmpty()) { forwardHttpResponseHeader(); sendMetaData(); } // It's over, we don't need it anymore clearPostDataBuffer(); SlaveBase::error(_err, _text); m_kioError = _err; } void HTTPProtocol::addCookies(const QString &url, const QByteArray &cookieHeader) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); (void)kcookiejar.call(QDBus::NoBlock, QStringLiteral("addCookies"), url, cookieHeader, windowId); } QString HTTPProtocol::findCookies(const QString &url) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); QDBusReply reply = kcookiejar.call(QStringLiteral("findCookies"), url, windowId); if (!reply.isValid()) { qCWarning(KIO_HTTP) << "Can't communicate with kded_kcookiejar!"; return QString(); } return reply; } /******************************* CACHING CODE ****************************/ HTTPProtocol::CacheTag::CachePlan HTTPProtocol::CacheTag::plan(int maxCacheAge) const { //notable omission: we're not checking cache file presence or integrity switch (policy) { case KIO::CC_Refresh: // Conditional GET requires the presence of either an ETag or // last modified date. if (lastModifiedDate.isValid() || !etag.isEmpty()) { return ValidateCached; } break; case KIO::CC_Reload: return IgnoreCached; case KIO::CC_CacheOnly: case KIO::CC_Cache: return UseCached; default: break; } Q_ASSERT((policy == CC_Verify || policy == CC_Refresh)); QDateTime currentDate = QDateTime::currentDateTime(); if ((servedDate.isValid() && (currentDate > servedDate.addSecs(maxCacheAge))) || (expireDate.isValid() && (currentDate > expireDate))) { return ValidateCached; } return UseCached; } // !START SYNC! // The following code should be kept in sync // with the code in http_cache_cleaner.cpp // we use QDataStream; this is just an illustration struct BinaryCacheFileHeader { quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment qint32 useCount; qint64 servedDate; qint64 lastModifiedDate; qint64 expireDate; qint32 bytesCached; // packed size should be 36 bytes; we explicitly set it here to make sure that no compiler // padding ruins it. We write the fields to disk without any padding. static const int size = 36; }; enum CacheCleanerCommandCode { InvalidCommand = 0, CreateFileNotificationCommand, UpdateFileCommand }; // illustration for cache cleaner update "commands" struct CacheCleanerCommand { BinaryCacheFileHeader header; quint32 commandCode; // filename in ASCII, binary isn't worth the coding and decoding quint8 filename[s_hashedUrlNibbles]; }; QByteArray HTTPProtocol::CacheTag::serialize() const { QByteArray ret; QDataStream stream(&ret, QIODevice::WriteOnly); stream << quint8('A'); stream << quint8('\n'); stream << quint8(0); stream << quint8(0); stream << fileUseCount; stream << servedDate.toMSecsSinceEpoch() / 1000; stream << lastModifiedDate.toMSecsSinceEpoch() / 1000; stream << expireDate.toMSecsSinceEpoch() / 1000; stream << bytesCached; Q_ASSERT(ret.size() == BinaryCacheFileHeader::size); return ret; } static bool compareByte(QDataStream *stream, quint8 value) { quint8 byte; *stream >> byte; return byte == value; } // If starting a new file cacheFileWriteVariableSizeHeader() must have been called *before* // calling this! This is to fill in the headerEnd field. // If the file is not new headerEnd has already been read from the file and in fact the variable // size header *may* not be rewritten because a size change would mess up the file layout. bool HTTPProtocol::CacheTag::deserialize(const QByteArray &d) { if (d.size() != BinaryCacheFileHeader::size) { return false; } QDataStream stream(d); stream.setVersion(QDataStream::Qt_4_5); bool ok = true; ok = ok && compareByte(&stream, 'A'); ok = ok && compareByte(&stream, '\n'); ok = ok && compareByte(&stream, 0); ok = ok && compareByte(&stream, 0); if (!ok) { return false; } stream >> fileUseCount; qint64 servedDateMs; stream >> servedDateMs; servedDate = QDateTime::fromMSecsSinceEpoch(servedDateMs * 1000); qint64 lastModifiedDateMs; stream >> lastModifiedDateMs; lastModifiedDate = QDateTime::fromMSecsSinceEpoch(lastModifiedDateMs * 1000); qint64 expireDateMs; stream >> expireDateMs; expireDate = QDateTime::fromMSecsSinceEpoch(expireDateMs * 1000); stream >> bytesCached; return true; } /* Text part of the header, directly following the binary first part: URL\n etag\n mimetype\n header line\n header line\n ... \n */ static QUrl storableUrl(const QUrl &url) { QUrl ret(url); ret.setPassword(QString()); ret.setFragment(QString()); return ret; } static void writeLine(QIODevice *dev, const QByteArray &line) { static const char linefeed = '\n'; dev->write(line); dev->write(&linefeed, 1); } void HTTPProtocol::cacheFileWriteTextHeader() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() & QIODevice::WriteOnly); file->seek(BinaryCacheFileHeader::size); writeLine(file, storableUrl(m_request.url).toEncoded()); writeLine(file, m_request.cacheTag.etag.toLatin1()); writeLine(file, m_mimeType.toLatin1()); writeLine(file, m_responseHeaders.join(QLatin1Char('\n')).toLatin1()); // join("\n") adds no \n to the end, but writeLine() does. // Add another newline to mark the end of text. writeLine(file, QByteArray()); } static bool readLineChecked(QIODevice *dev, QByteArray *line) { *line = dev->readLine(MAX_IPC_SIZE); // if nothing read or the line didn't fit into 8192 bytes(!) if (line->isEmpty() || !line->endsWith('\n')) { return false; } // we don't actually want the newline! line->chop(1); return true; } bool HTTPProtocol::cacheFileReadTextHeader1(const QUrl &desiredUrl) { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); QByteArray readBuf; bool ok = readLineChecked(file, &readBuf); if (storableUrl(desiredUrl).toEncoded() != readBuf) { qCDebug(KIO_HTTP) << "You have witnessed a very improbable hash collision!"; return false; } ok = ok && readLineChecked(file, &readBuf); m_request.cacheTag.etag = toQString(readBuf); return ok; } bool HTTPProtocol::cacheFileReadTextHeader2() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); bool ok = true; QByteArray readBuf; #ifndef NDEBUG // we assume that the URL and etag have already been read qint64 oldPos = file->pos(); file->seek(BinaryCacheFileHeader::size); ok = ok && readLineChecked(file, &readBuf); ok = ok && readLineChecked(file, &readBuf); Q_ASSERT(file->pos() == oldPos); #endif ok = ok && readLineChecked(file, &readBuf); m_mimeType = toQString(readBuf); m_responseHeaders.clear(); // read as long as no error and no empty line found while (true) { ok = ok && readLineChecked(file, &readBuf); if (ok && !readBuf.isEmpty()) { m_responseHeaders.append(toQString(readBuf)); } else { break; } } return ok; // it may still be false ;) } static QString filenameFromUrl(const QUrl &url) { QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(storableUrl(url).toEncoded()); return toQString(hash.result().toHex()); } QString HTTPProtocol::cacheFilePathFromUrl(const QUrl &url) const { QString filePath = m_strCacheDir; if (!filePath.endsWith(QLatin1Char('/'))) { filePath.append(QLatin1Char('/')); } filePath.append(filenameFromUrl(url)); return filePath; } bool HTTPProtocol::cacheFileOpenRead() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); QFile *&file = m_request.cacheTag.file; if (file) { qCDebug(KIO_HTTP) << "File unexpectedly open; old file is" << file->fileName() << "new name is" << filename; Q_ASSERT(file->fileName() == filename); } Q_ASSERT(!file); file = new QFile(filename); if (file->open(QIODevice::ReadOnly)) { QByteArray header = file->read(BinaryCacheFileHeader::size); if (!m_request.cacheTag.deserialize(header)) { qCDebug(KIO_HTTP) << "Cache file header is invalid."; file->close(); } } if (file->isOpen() && !cacheFileReadTextHeader1(m_request.url)) { file->close(); } if (!file->isOpen()) { cacheFileClose(); return false; } return true; } bool HTTPProtocol::cacheFileOpenWrite() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); // if we open a cache file for writing while we have a file open for reading we must have // found out that the old cached content is obsolete, so delete the file. QFile *&file = m_request.cacheTag.file; if (file) { // ensure that the file is in a known state - either open for reading or null Q_ASSERT(!qobject_cast(file)); Q_ASSERT((file->openMode() & QIODevice::WriteOnly) == 0); Q_ASSERT(file->fileName() == filename); qCDebug(KIO_HTTP) << "deleting expired cache entry and recreating."; file->remove(); delete file; file = nullptr; } // note that QTemporaryFile will automatically append random chars to filename file = new QTemporaryFile(filename); file->open(QIODevice::WriteOnly); // if we have started a new file we have not initialized some variables from disk data. m_request.cacheTag.fileUseCount = 0; // the file has not been *read* yet m_request.cacheTag.bytesCached = 0; if ((file->openMode() & QIODevice::WriteOnly) == 0) { qCDebug(KIO_HTTP) << "Could not open file for writing: QTemporaryFile(" << filename << ")" << "due to error" << file->error(); cacheFileClose(); return false; } return true; } static QByteArray makeCacheCleanerCommand(const HTTPProtocol::CacheTag &cacheTag, CacheCleanerCommandCode cmd) { QByteArray ret = cacheTag.serialize(); QDataStream stream(&ret, QIODevice::ReadWrite); stream.setVersion(QDataStream::Qt_4_5); stream.skipRawData(BinaryCacheFileHeader::size); // append the command code stream << quint32(cmd); // append the filename QString fileName = cacheTag.file->fileName(); int basenameStart = fileName.lastIndexOf(QLatin1Char('/')) + 1; const QByteArray baseName = fileName.midRef(basenameStart, s_hashedUrlNibbles).toLatin1(); stream.writeRawData(baseName.constData(), baseName.size()); Q_ASSERT(ret.size() == BinaryCacheFileHeader::size + sizeof(quint32) + s_hashedUrlNibbles); return ret; } //### not yet 100% sure when and when not to call this void HTTPProtocol::cacheFileClose() { qCDebug(KIO_HTTP); QFile *&file = m_request.cacheTag.file; if (!file) { return; } m_request.cacheTag.ioMode = NoCache; QByteArray ccCommand; QTemporaryFile *tempFile = qobject_cast(file); if (file->openMode() & QIODevice::WriteOnly) { Q_ASSERT(tempFile); if (m_request.cacheTag.bytesCached && !m_kioError) { QByteArray header = m_request.cacheTag.serialize(); tempFile->seek(0); tempFile->write(header); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, CreateFileNotificationCommand); QString oldName = tempFile->fileName(); QString newName = oldName; int basenameStart = newName.lastIndexOf(QLatin1Char('/')) + 1; // remove the randomized name part added by QTemporaryFile newName.chop(newName.length() - basenameStart - s_hashedUrlNibbles); qCDebug(KIO_HTTP) << "Renaming temporary file" << oldName << "to" << newName; // on windows open files can't be renamed tempFile->setAutoRemove(false); delete tempFile; file = nullptr; if (!QFile::rename(oldName, newName)) { // ### currently this hides a minor bug when force-reloading a resource. We // should not even open a new file for writing in that case. qCDebug(KIO_HTTP) << "Renaming temporary file failed, deleting it instead."; QFile::remove(oldName); ccCommand.clear(); // we have nothing of value to tell the cache cleaner } } else { // oh, we've never written payload data to the cache file. // the temporary file is closed and removed and no proper cache entry is created. } } else if (file->openMode() == QIODevice::ReadOnly) { Q_ASSERT(!tempFile); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, UpdateFileCommand); } delete file; file = nullptr; if (!ccCommand.isEmpty()) { sendCacheCleanerCommand(ccCommand); } } void HTTPProtocol::sendCacheCleanerCommand(const QByteArray &command) { qCDebug(KIO_HTTP); if (!qEnvironmentVariableIsEmpty("KIO_DISABLE_CACHE_CLEANER")) // for autotests return; Q_ASSERT(command.size() == BinaryCacheFileHeader::size + s_hashedUrlNibbles + sizeof(quint32)); if (m_cacheCleanerConnection.state() != QLocalSocket::ConnectedState) { QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + QLatin1String("kio_http_cache_cleaner"); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState) { // An error happened. // Most likely the cache cleaner is not running, let's start it. // search paths const QStringList searchPaths = QStringList() << QCoreApplication::applicationDirPath() // then look where our application binary is located << QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath) // look where libexec path is (can be set in qt.conf) << QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5); // look at our installation location const QString exe = QStandardPaths::findExecutable(QStringLiteral("kio_http_cache_cleaner"), searchPaths); if (exe.isEmpty()) { qCWarning(KIO_HTTP) << "kio_http_cache_cleaner not found in" << searchPaths; return; } qCDebug(KIO_HTTP) << "starting" << exe; QProcess::startDetached(exe, QStringList()); for (int i = 0 ; i < 30 && m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState; ++i) { // Server is not listening yet; let's hope it does so under 3 seconds QThread::msleep(100); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() != QLocalSocket::UnconnectedState) { break; // connecting or connected, sounds good } } } if (!m_cacheCleanerConnection.waitForConnected(1500)) { // updating the stats is not vital, so we just give up. qCDebug(KIO_HTTP) << "Could not connect to cache cleaner, not updating stats of this cache file."; return; } qCDebug(KIO_HTTP) << "Successfully connected to cache cleaner."; } Q_ASSERT(m_cacheCleanerConnection.state() == QLocalSocket::ConnectedState); m_cacheCleanerConnection.write(command); m_cacheCleanerConnection.flush(); } QByteArray HTTPProtocol::cacheFileReadPayload(int maxLength) { Q_ASSERT(m_request.cacheTag.file); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); QByteArray ret = m_request.cacheTag.file->read(maxLength); if (ret.isEmpty()) { cacheFileClose(); } return ret; } void HTTPProtocol::cacheFileWritePayload(const QByteArray &d) { if (!m_request.cacheTag.file) { return; } // If the file being downloaded is so big that it exceeds the max cache size, // do not cache it! See BR# 244215. NOTE: this can be improved upon in the // future... if (m_iSize >= KIO::filesize_t(m_maxCacheSize * 1024)) { qCDebug(KIO_HTTP) << "Caching disabled because content size is too big."; cacheFileClose(); return; } Q_ASSERT(m_request.cacheTag.ioMode == WriteToCache); Q_ASSERT(m_request.cacheTag.file->openMode() & QIODevice::WriteOnly); if (d.isEmpty()) { cacheFileClose(); } //TODO: abort if file grows too big! // write the variable length text header as soon as we start writing to the file if (!m_request.cacheTag.bytesCached) { cacheFileWriteTextHeader(); } m_request.cacheTag.bytesCached += d.size(); m_request.cacheTag.file->write(d); } void HTTPProtocol::cachePostData(const QByteArray &data) { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(qMax(m_iPostDataSize, static_cast(data.size()))); if (!m_POSTbuf) { return; } } m_POSTbuf->write(data.constData(), data.size()); } void HTTPProtocol::clearPostDataBuffer() { if (!m_POSTbuf) { return; } delete m_POSTbuf; m_POSTbuf = nullptr; } bool HTTPProtocol::retrieveAllData() { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(s_MaxInMemPostBufSize + 1); } if (!m_POSTbuf) { error(ERR_OUT_OF_MEMORY, m_request.url.host()); return false; } while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); return false; } if (bytesRead == 0) { break; } m_POSTbuf->write(buffer.constData(), buffer.size()); } return true; } // The above code should be kept in sync // with the code in http_cache_cleaner.cpp // !END SYNC! //************************** AUTHENTICATION CODE ********************/ QString HTTPProtocol::authenticationHeader() { QByteArray ret; // If the internal meta-data "cached-www-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_wwwAuth && config()->readEntry("cached-www-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.url; authinfo.realmValue = config()->readEntry("www-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); const bool useCachedAuth = (m_request.responseCode == 401 || !config()->readEntry("no-preemptive-auth-reuse", false)); if (useCachedAuth && checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("www-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_wwwAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_wwwAuth) { qCDebug(KIO_HTTP) << "creating www authentication header from cached info"; m_wwwAuth->setChallenge(cachedChallenge, m_request.url, m_request.sentMethodString); m_wwwAuth->generateResponse(authinfo.username, authinfo.password); } } } } // If the internal meta-data "cached-proxy-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_proxyAuth && config()->readEntry("cached-proxy-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.proxyUrl; authinfo.realmValue = config()->readEntry("proxy-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); if (checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("proxy-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_proxyAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_proxyAuth) { qCDebug(KIO_HTTP) << "creating proxy authentication header from cached info"; m_proxyAuth->setChallenge(cachedChallenge, m_request.proxyUrl, m_request.sentMethodString); m_proxyAuth->generateResponse(authinfo.username, authinfo.password); } } } } // the authentication classes don't know if they are for proxy or webserver authentication... if (m_wwwAuth && !m_wwwAuth->isError()) { ret += "Authorization: " + m_wwwAuth->headerFragment(); } if (m_proxyAuth && !m_proxyAuth->isError()) { ret += "Proxy-Authorization: " + m_proxyAuth->headerFragment(); } return toQString(ret); // ## encoding ok? } static QString protocolForProxyType(QNetworkProxy::ProxyType type) { switch (type) { case QNetworkProxy::DefaultProxy: break; case QNetworkProxy::Socks5Proxy: return QStringLiteral("socks"); case QNetworkProxy::NoProxy: break; case QNetworkProxy::HttpProxy: case QNetworkProxy::HttpCachingProxy: case QNetworkProxy::FtpCachingProxy: break; } return QStringLiteral("http"); } void HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator) { qCDebug(KIO_HTTP) << "realm:" << authenticator->realm() << "user:" << authenticator->user(); // Set the proxy URL... m_request.proxyUrl.setScheme(protocolForProxyType(proxy.type())); m_request.proxyUrl.setUserName(proxy.user()); m_request.proxyUrl.setHost(proxy.hostName()); m_request.proxyUrl.setPort(proxy.port()); AuthInfo info; info.url = m_request.proxyUrl; info.realmValue = authenticator->realm(); info.username = authenticator->user(); info.verifyPath = info.realmValue.isEmpty(); const bool haveCachedCredentials = checkCachedAuthentication(info); const bool retryAuth = (m_socketProxyAuth != nullptr); // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, // and it was not successful. see below and saveProxyAuthenticationForSocket(). if (!haveCachedCredentials || retryAuth) { // Save authentication info if the connection succeeds. We need to disconnect // this after saving the auth data (or an error) so we won't save garbage afterwards! connect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); //### fillPromptInfo(&info); info.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); info.keepPassword = true; info.commentLabel = i18n("Proxy:"); info.comment = i18n("%1 at %2", info.realmValue.toHtmlEscaped(), m_request.proxyUrl.host()); const QString errMsg((retryAuth ? i18n("Proxy Authentication Failed.") : QString())); const int errorCode = openPasswordDialogV2(info, errMsg); if (errorCode) { qCDebug(KIO_HTTP) << "proxy auth cancelled by user, or communication error"; error(errorCode, QString()); delete m_proxyAuth; m_proxyAuth = nullptr; return; } } authenticator->setUser(info.username); authenticator->setPassword(info.password); authenticator->setOption(QStringLiteral("keepalive"), info.keepPassword); if (m_socketProxyAuth) { *m_socketProxyAuth = *authenticator; } else { m_socketProxyAuth = new QAuthenticator(*authenticator); } if (!m_request.proxyUrl.userName().isEmpty()) { m_request.proxyUrl.setUserName(info.username); } } void HTTPProtocol::saveProxyAuthenticationForSocket() { qCDebug(KIO_HTTP) << "Saving authenticator"; disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); Q_ASSERT(m_socketProxyAuth); if (m_socketProxyAuth) { qCDebug(KIO_HTTP) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); KIO::AuthInfo a; a.verifyPath = true; a.url = m_request.proxyUrl; a.realmValue = m_socketProxyAuth->realm(); a.username = m_socketProxyAuth->user(); a.password = m_socketProxyAuth->password(); a.keepPassword = m_socketProxyAuth->option(QStringLiteral("keepalive")).toBool(); cacheAuthentication(a); } delete m_socketProxyAuth; m_socketProxyAuth = nullptr; } void HTTPProtocol::saveAuthenticationData() { KIO::AuthInfo authinfo; bool alreadyCached = false; KAbstractHttpAuthentication *auth = nullptr; switch (m_request.prevResponseCode) { case 401: auth = m_wwwAuth; alreadyCached = config()->readEntry("cached-www-auth", false); break; case 407: auth = m_proxyAuth; alreadyCached = config()->readEntry("cached-proxy-auth", false); break; default: Q_ASSERT(false); // should never happen! } // Prevent recaching of the same credentials over and over again. if (auth && (!auth->realm().isEmpty() || !alreadyCached)) { auth->fillKioAuthInfo(&authinfo); if (auth == m_wwwAuth) { setMetaData(QStringLiteral("{internal~currenthost}cached-www-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo); } } else { setMetaData(QStringLiteral("{internal~allhosts}cached-proxy-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo); } } qCDebug(KIO_HTTP) << "Cache authentication info ?" << authinfo.keepPassword; if (authinfo.keepPassword) { cacheAuthentication(authinfo); qCDebug(KIO_HTTP) << "Cached authentication for" << m_request.url; } } // Update our server connection state which includes www and proxy username and password. m_server.updateCredentials(m_request); } bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer *tokenizer) { KIO::AuthInfo authinfo; QList authTokens; KAbstractHttpAuthentication **auth; QList *blacklistedAuthTokens; TriedCredentials *triedCredentials; if (m_request.responseCode == 401) { auth = &m_wwwAuth; blacklistedAuthTokens = &m_blacklistedWwwAuthMethods; triedCredentials = &m_triedWwwCredentials; authTokens = tokenizer->iterator("www-authenticate").all(); authinfo.url = m_request.url; authinfo.username = m_server.url.userName(); authinfo.prompt = i18n("You need to supply a username and a " "password to access this site."); authinfo.commentLabel = i18n("Site:"); } else { // make sure that the 407 header hasn't escaped a lower layer when it shouldn't. // this may break proxy chains which were never tested anyway, and AFAIK they are // rare to nonexistent in the wild. Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy); auth = &m_proxyAuth; blacklistedAuthTokens = &m_blacklistedProxyAuthMethods; triedCredentials = &m_triedProxyCredentials; authTokens = tokenizer->iterator("proxy-authenticate").all(); authinfo.url = m_request.proxyUrl; authinfo.username = m_request.proxyUrl.userName(); authinfo.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); authinfo.commentLabel = i18n("Proxy:"); } bool authRequiresAnotherRoundtrip = false; // Workaround brain dead server responses that violate the spec and // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate // header fields. See bug 215736... if (!authTokens.isEmpty()) { QString errorMsg; authRequiresAnotherRoundtrip = true; if (m_request.responseCode == m_request.prevResponseCode && *auth) { // Authentication attempt failed. Retry... if ((*auth)->wasFinalStage()) { errorMsg = (m_request.responseCode == 401 ? i18n("Authentication Failed.") : i18n("Proxy Authentication Failed.")); // The authentication failed in its final stage. If the chosen method didn't use a password or // if it failed with both the supplied and prompted password then blacklist this method and try // again with another one if possible. if (!(*auth)->needCredentials() || *triedCredentials > JobCredentials) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; blacklistedAuthTokens->append(scheme); } delete *auth; *auth = nullptr; } else { // Create authentication header // WORKAROUND: The following piece of code prevents brain dead IIS // servers that send back multiple "WWW-Authenticate" headers from // screwing up our authentication logic during the challenge // phase (Type 2) of NTLM authentication. QMutableListIterator it(authTokens); const QByteArray authScheme((*auth)->scheme().trimmed()); while (it.hasNext()) { if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) { it.remove(); } } } } QList::iterator it = authTokens.begin(); while (it != authTokens.end()) { QByteArray scheme = *it; // Separate the method name from any additional parameters (for ex. nonce or realm). int index = it->indexOf(' '); if (index > 0) { scheme.truncate(index); } if (blacklistedAuthTokens->contains(scheme)) { it = authTokens.erase(it); } else { it++; } } try_next_auth_scheme: QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens); if (*auth) { const QByteArray authScheme((*auth)->scheme().trimmed()); if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) { // huh, the strongest authentication scheme offered has changed. delete *auth; *auth = nullptr; } } if (!(*auth)) { *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config()); } if (*auth) { qCDebug(KIO_HTTP) << "Trying authentication scheme:" << (*auth)->scheme(); // remove trailing space from the method string, or digest auth will fail (*auth)->setChallenge(bestOffer, authinfo.url, m_request.sentMethodString); QString username, password; bool generateAuthHeader = true; if ((*auth)->needCredentials()) { // use credentials supplied by the application if available if (!m_request.url.userName().isEmpty() && !m_request.url.password().isEmpty() && *triedCredentials == NoCredentials) { username = m_request.url.userName(); password = m_request.url.password(); // don't try this password any more *triedCredentials = JobCredentials; } else { // try to get credentials from kpasswdserver's cache, then try asking the user. authinfo.verifyPath = false; // we have realm, no path based checking please! authinfo.realmValue = (*auth)->realm(); if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching()) { authinfo.realmValue = QLatin1String((*auth)->scheme()); } // Save the current authinfo url because it can be modified by the call to // checkCachedAuthentication. That way we can restore it if the call // modified it. const QUrl reqUrl = authinfo.url; if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) { // Reset url to the saved url... authinfo.url = reqUrl; authinfo.keepPassword = true; authinfo.comment = i18n("%1 at %2", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host()); const int errorCode = openPasswordDialogV2(authinfo, errorMsg); if (errorCode) { generateAuthHeader = false; authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_ACCESS_DENIED, reqUrl.host()); } qCDebug(KIO_HTTP) << "looks like the user canceled the authentication dialog"; delete *auth; *auth = nullptr; } *triedCredentials = UserInputCredentials; } else { *triedCredentials = CachedCredentials; } username = authinfo.username; password = authinfo.password; } } if (generateAuthHeader) { (*auth)->generateResponse(username, password); (*auth)->setCachePasswordEnabled(authinfo.keepPassword); qCDebug(KIO_HTTP) << "isError=" << (*auth)->isError() << "needCredentials=" << (*auth)->needCredentials() << "forceKeepAlive=" << (*auth)->forceKeepAlive() << "forceDisconnect=" << (*auth)->forceDisconnect(); if ((*auth)->isError()) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; authTokens.removeOne(scheme); blacklistedAuthTokens->append(scheme); if (!authTokens.isEmpty()) { goto try_next_auth_scheme; } else { if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed.")); } authRequiresAnotherRoundtrip = false; } //### return false; ? } else if ((*auth)->forceKeepAlive()) { //### think this through for proxied / not proxied m_request.isKeepAlive = true; } else if ((*auth)->forceDisconnect()) { //### think this through for proxied / not proxied m_request.isKeepAlive = false; httpCloseConnection(); } } } else { authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method.")); } } } return authRequiresAnotherRoundtrip; } void HTTPProtocol::copyPut(const QUrl& src, const QUrl& dest, JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest)) { return; } resetSessionSettings(); if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { return; } } m_POSTbuf = new QFile (src.toLocalFile()); if (!m_POSTbuf->open(QFile::ReadOnly)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, QString()); return; } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } bool HTTPProtocol::davStatDestination() { const QByteArray request ("" "" "" "" "" "" ""); davSetRequest(request); // WebDAV Stat or List... m_request.method = DAV_PROPFIND; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = 0; proceedUntilResponseContent(true); if (!m_request.isKeepAlive) { httpCloseConnection(); // close connection if server requested it. m_request.isKeepAlive = true; // reset the keep alive flag. } if (m_request.responseCode == 207) { error(ERR_FILE_ALREADY_EXIST, QString()); return false; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; return true; } void HTTPProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url); } void HTTPProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; default: SlaveBase::virtual_hook(id, data); } } // needed for JSON file embedding #include "http.moc" diff --git a/src/ioslaves/http/kcookiejar/kcookiejar.cpp b/src/ioslaves/http/kcookiejar/kcookiejar.cpp index d151f6c4..4a436c58 100644 --- a/src/ioslaves/http/kcookiejar/kcookiejar.cpp +++ b/src/ioslaves/http/kcookiejar/kcookiejar.cpp @@ -1,1612 +1,1612 @@ /* This file is part of the KDE File Manager Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) Copyright (C) 2000,2001 Dawit Alemayehu (adawit@kde.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //---------------------------------------------------------------------------- // // KDE File Manager -- HTTP Cookies // // The cookie protocol is a mess. RFC2109 is a joke since nobody seems to // use it. Apart from that it is badly written. // We try to implement Netscape Cookies and try to behave us according to // RFC2109 as much as we can. // // We assume cookies do not contain any spaces (Netscape spec.) // According to RFC2109 this is allowed though. // #include "kcookiejar.h" #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(KIO_COOKIEJAR, "kf5.kio.cookiejar") // BR87227 // Waba: Should the number of cookies be limited? // I am not convinced of the need of such limit // Mozilla seems to limit to 20 cookies / domain // but it is unclear which policy it uses to expire // cookies when it exceeds that amount #undef MAX_COOKIE_LIMIT #define MAX_COOKIES_PER_HOST 25 #define READ_BUFFER_SIZE 8192 #define IP_ADDRESS_EXPRESSION "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" // Note with respect to QLatin1String( ).... // Cookies are stored as 8 bit data and passed to kio_http as Latin1 // regardless of their actual encoding. #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) static QString removeWeekday(const QString &value) { const int index = value.indexOf(QL1C(' ')); if (index > -1) { int pos = 0; - const QString weekday = value.left(index); + const QStringRef weekday = value.leftRef(index); const QLocale cLocale = QLocale::c(); for (int i = 1; i < 8; ++i) { // No need to check for long names since the short names are // prefixes of the long names if (weekday.startsWith(cLocale.dayName(i, QLocale::ShortFormat), Qt::CaseInsensitive)) { pos = index + 1; break; } } if (pos > 0) { return value.mid(pos); } } return value; } static QDateTime parseDate(const QString &_value) { // Handle sites sending invalid weekday as part of the date. #298660 const QString value(removeWeekday(_value)); // Check if expiration date matches RFC dates as specified under // RFC 2616 sec 3.3.1 & RFC 6265 sec 4.1.1 QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); if (!dt.isValid()) { static const char *const date_formats[] = { // Other formats documented in RFC 2616 sec 3.3.1 // Note: the RFC says timezone information MUST be "GMT", hence the hardcoded timezone string "MMM dd HH:mm:ss yyyy", /* ANSI C's asctime() format (#145244): Jan 01 00:00:00 1970 GMT */ "dd-MMM-yy HH:mm:ss 'GMT'", /* RFC 850 date: 06-Dec-39 00:30:42 GMT */ // Non-standard formats "MMM dd yyyy HH:mm:ss", /* A variation on ANSI C format seen @ amazon.com: Jan 01 1970 00:00:00 GMT */ "dd-MMM-yyyy HH:mm:ss 'GMT'", /* cookies.test: Y2K38 problem: 06-Dec-2039 00:30:42 GMT */ "MMM dd HH:mm:ss yyyy 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 07:00:00 2020 GMT */ "MMM dd yyyy HH:mm:ss 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 2020 07:00:00 GMT */ nullptr }; // Only English month names are allowed, thus use the C locale. const QLocale cLocale = QLocale::c(); for (int i = 0; date_formats[i]; ++i) { dt = cLocale.toDateTime(value, QL1S(date_formats[i])); if (dt.isValid()) { dt.setTimeSpec(Qt::UTC); break; } } } return dt.toUTC(); // Per RFC 2616 sec 3.3.1 always convert to UTC. } static qint64 toEpochSecs(const QDateTime &dt) { return (dt.toMSecsSinceEpoch() / 1000); // convert to seconds... } static qint64 epoch() { return toEpochSecs(QDateTime::currentDateTimeUtc()); } QString KCookieJar::adviceToStr(KCookieAdvice _advice) { switch (_advice) { case KCookieAccept: return QStringLiteral("Accept"); case KCookieAcceptForSession: return QStringLiteral("AcceptForSession"); case KCookieReject: return QStringLiteral("Reject"); case KCookieAsk: return QStringLiteral("Ask"); default: return QStringLiteral("Dunno"); } } KCookieAdvice KCookieJar::strToAdvice(const QString &_str) { if (_str.isEmpty()) { return KCookieDunno; } QString advice = _str.toLower(); if (advice == QL1S("accept")) { return KCookieAccept; } else if (advice == QL1S("acceptforsession")) { return KCookieAcceptForSession; } else if (advice == QL1S("reject")) { return KCookieReject; } else if (advice == QL1S("ask")) { return KCookieAsk; } return KCookieDunno; } // KHttpCookie /////////////////////////////////////////////////////////////////////////// // // Cookie constructor // KHttpCookie::KHttpCookie(const QString &_host, const QString &_domain, const QString &_path, const QString &_name, const QString &_value, qint64 _expireDate, int _protocolVersion, bool _secure, bool _httpOnly, bool _explicitPath) : mHost(_host), mDomain(_domain), mPath(_path.isEmpty() ? QString() : _path), mName(_name), mValue(_value), mExpireDate(_expireDate), mProtocolVersion(_protocolVersion), mSecure(_secure), mCrossDomain(false), mHttpOnly(_httpOnly), mExplicitPath(_explicitPath), mUserSelectedAdvice(KCookieDunno) { } // // Checks if a cookie has been expired // bool KHttpCookie::isExpired(qint64 currentDate) const { if (currentDate == -1) { currentDate = epoch(); } return (mExpireDate != 0) && (mExpireDate < currentDate); } // // Returns a string for a HTTP-header // QString KHttpCookie::cookieStr(bool useDOMFormat) const { QString result; if (useDOMFormat || (mProtocolVersion == 0)) { if (mName.isEmpty()) { result = mValue; } else { result = mName + QL1C('=') + mValue; } } else { result = mName + QL1C('=') + mValue; if (mExplicitPath) { result += QL1S("; $Path=\"") + mPath + QL1C('"'); } if (!mDomain.isEmpty()) { result += QL1S("; $Domain=\"") + mDomain + QL1C('"'); } if (!mPorts.isEmpty()) { if (mPorts.length() == 2 && mPorts.at(0) == -1) { result += QL1S("; $Port"); } else { QString portNums; Q_FOREACH (int port, mPorts) { portNums += QString::number(port) + QL1C(' '); } result += QL1S("; $Port=\"") + portNums.trimmed() + QL1C('"'); } } } return result; } // // Returns whether this cookie should be send to this location. bool KHttpCookie::match(const QString &fqdn, const QStringList &domains, const QString &path, int port) const { // Cookie domain match check if (mDomain.isEmpty()) { if (fqdn != mHost) { return false; } } else if (!domains.contains(mDomain)) { if (mDomain[0] == QL1C('.')) { return false; } // Maybe the domain needs an extra dot. const QString domain = QL1C('.') + mDomain; if (!domains.contains(domain)) if (fqdn != mDomain) { return false; } } else if (mProtocolVersion != 0 && port != -1 && !mPorts.isEmpty() && !mPorts.contains(port)) { return false; } // Cookie path match check if (mPath.isEmpty()) { return true; } // According to the netscape spec http://www.acme.com/foobar, // http://www.acme.com/foo.bar and http://www.acme.com/foo/bar // should all match http://www.acme.com/foo... // We only match http://www.acme.com/foo/bar if (path.startsWith(mPath) && ( (path.length() == mPath.length()) || // Paths are exact match mPath.endsWith(QL1C('/')) || // mPath ended with a slash (path[mPath.length()] == QL1C('/')) // A slash follows )) { return true; // Path of URL starts with cookie-path } return false; } // KCookieJar /////////////////////////////////////////////////////////////////////////// // // Constructs a new cookie jar // // One jar should be enough for all cookies. // KCookieJar::KCookieJar() { m_globalAdvice = KCookieDunno; m_configChanged = false; m_cookiesChanged = false; KConfig cfg(QStringLiteral("kf5/kcookiejar/domain_info"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation); KConfigGroup group(&cfg, QString()); m_gTLDs = QSet::fromList(group.readEntry("gTLDs", QStringList())); m_twoLevelTLD = QSet::fromList(group.readEntry("twoLevelTLD", QStringList())); } // // Destructs the cookie jar // // Poor little cookies, they will all be eaten by the cookie monster! // KCookieJar::~KCookieJar() { qDeleteAll(m_cookieDomains); // Not much to do here } // cookiePtr is modified: the window ids of the existing cookie in the list are added to it static void removeDuplicateFromList(KHttpCookieList *list, KHttpCookie &cookiePtr, bool nameMatchOnly = false, bool updateWindowId = false) { QString domain1 = cookiePtr.domain(); if (domain1.isEmpty()) { domain1 = cookiePtr.host(); } QMutableListIterator cookieIterator(*list); while (cookieIterator.hasNext()) { const KHttpCookie &cookie = cookieIterator.next(); QString domain2 = cookie.domain(); if (domain2.isEmpty()) { domain2 = cookie.host(); } if (cookiePtr.name() == cookie.name() && (nameMatchOnly || (domain1 == domain2 && cookiePtr.path() == cookie.path()))) { if (updateWindowId) { Q_FOREACH (WId windowId, cookie.windowIds()) { if (windowId && (!cookiePtr.windowIds().contains(windowId))) { cookiePtr.windowIds().append(windowId); } } } cookieIterator.remove(); break; } } } // // Looks for cookies in the cookie jar which are appropriate for _url. // Returned is a string containing all appropriate cookies in a format // which can be added to a HTTP-header without any additional processing. // QString KCookieJar::findCookies(const QString &_url, bool useDOMFormat, WId windowId, KHttpCookieList *pendingCookies) { QString cookieStr, fqdn, path; QStringList domains; int port = -1; if (!parseUrl(_url, fqdn, path, &port)) { return cookieStr; } const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) || _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive)); if (port == -1) { port = (secureRequest ? 443 : 80); } extractDomains(fqdn, domains); KHttpCookieList allCookies; for (QStringList::ConstIterator it = domains.constBegin(), itEnd = domains.constEnd();; ++it) { KHttpCookieList *cookieList = nullptr; if (it == itEnd) { cookieList = pendingCookies; // Add pending cookies pendingCookies = nullptr; if (!cookieList) { break; } } else { if ((*it).isNull()) { cookieList = m_cookieDomains.value(QL1S("")); } else { cookieList = m_cookieDomains.value(*it); } if (!cookieList) { continue; // No cookies for this domain } } QMutableListIterator cookieIt(*cookieList); while (cookieIt.hasNext()) { KHttpCookie &cookie = cookieIt.next(); if (cookieAdvice(cookie) == KCookieReject) { continue; } if (!cookie.match(fqdn, domains, path, port)) { continue; } if (cookie.isSecure() && !secureRequest) { continue; } if (cookie.isHttpOnly() && useDOMFormat) { continue; } // Do not send expired cookies. if (cookie.isExpired()) { // NOTE: there is no need to delete the cookie here because the // cookieserver will invoke its saveCookieJar function as a result // of the state change below. This will then result in the cookie // being deleting at that point. m_cookiesChanged = true; continue; } if (windowId && (cookie.windowIds().indexOf(windowId) == -1)) { cookie.windowIds().append(windowId); } if (it == itEnd) { // Only needed when processing pending cookies removeDuplicateFromList(&allCookies, cookie); } allCookies.append(cookie); } if (it == itEnd) { break; // Finished. } } int protVersion = 0; Q_FOREACH (const KHttpCookie &cookie, allCookies) { if (cookie.protocolVersion() > protVersion) { protVersion = cookie.protocolVersion(); } } if (!allCookies.isEmpty()) { if (!useDOMFormat) { cookieStr = QStringLiteral("Cookie: "); } if (protVersion > 0) { cookieStr = cookieStr + QStringLiteral("$Version=") + QString::number(protVersion) + QStringLiteral("; "); } Q_FOREACH (const KHttpCookie &cookie, allCookies) { cookieStr = cookieStr + cookie.cookieStr(useDOMFormat) + QStringLiteral("; "); } cookieStr.chop(2); // Remove the trailing '; ' } return cookieStr; } // // This function parses a string like 'my_name="my_value";' and returns // 'my_name' in Name and 'my_value' in Value. // // A pointer to the end of the parsed part is returned. // This pointer points either to: // '\0' - The end of the string has reached. // ';' - Another my_name="my_value" pair follows // ',' - Another cookie follows // '\n' - Another header follows static const char *parseNameValue(const char *header, QString &Name, QString &Value, bool keepQuotes = false, bool rfcQuotes = false) { const char *s = header; // Parse 'my_name' part for (; (*s != '='); s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { // No '=' sign -> use string as the value, name is empty // (behavior found in Mozilla and IE) Name = QL1S(""); Value = QL1S(header); Value.truncate(s - header); Value = Value.trimmed(); return s; } } Name = QL1S(header); Name.truncate(s - header); Name = Name.trimmed(); // *s == '=' s++; // Skip any whitespace for (; (*s == ' ') || (*s == '\t'); s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { // End of Name Value = QLatin1String(""); return s; } } if ((rfcQuotes || !keepQuotes) && (*s == '\"')) { // Parse '"my_value"' part (quoted value) if (keepQuotes) { header = s++; } else { header = ++s; // skip " } for (; (*s != '\"'); s++) { if ((*s == '\0') || (*s == '\n')) { // End of Name Value = QL1S(header); Value.truncate(s - header); return s; } } Value = QL1S(header); // *s == '\"'; if (keepQuotes) { Value.truncate(++s - header); } else { Value.truncate(s++ - header); } // Skip any remaining garbage for (;; s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { break; } } } else { // Parse 'my_value' part (unquoted value) header = s; while ((*s != '\0') && (*s != ';') && (*s != '\n')) { s++; } // End of Name Value = QL1S(header); Value.truncate(s - header); Value = Value.trimmed(); } return s; } void KCookieJar::stripDomain(const QString &_fqdn, QString &_domain) const { QStringList domains; extractDomains(_fqdn, domains); if (domains.count() > 3) { _domain = domains[3]; } else if (domains.count() > 0) { _domain = domains[0]; } else { _domain = QL1S(""); } } QString KCookieJar::stripDomain(const KHttpCookie &cookie) const { QString domain; // We file the cookie under this domain. if (cookie.domain().isEmpty()) { stripDomain(cookie.host(), domain); } else { domain = cookie.domain(); } return domain; } bool KCookieJar::parseUrl(const QString &_url, QString &_fqdn, QString &_path, int *port) { QUrl kurl(_url); if (!kurl.isValid() || kurl.scheme().isEmpty()) { return false; } _fqdn = kurl.host().toLower(); // Cookie spoofing protection. Since there is no way a path separator, // a space or the escape encoding character is allowed in the hostname // according to RFC 2396, reject attempts to include such things there! if (_fqdn.contains(QL1C('/')) || _fqdn.contains(QL1C('%'))) { return false; // deny everything!! } // Set the port number from the protocol when one is found... if (port) { *port = kurl.port(); } _path = kurl.path(); if (_path.isEmpty()) { _path = QStringLiteral("/"); } return true; } // not static because it uses m_twoLevelTLD void KCookieJar::extractDomains(const QString &_fqdn, QStringList &_domains) const { if (_fqdn.isEmpty()) { _domains.append(QStringLiteral("localhost")); return; } // Return numeric IPv6 addresses as is... if (_fqdn[0] == QL1C('[')) { _domains.append(_fqdn); return; } // Return numeric IPv4 addresses as is... if (_fqdn[0] >= QL1C('0') && _fqdn[0] <= QL1C('9') && _fqdn.indexOf(QRegExp(QStringLiteral(IP_ADDRESS_EXPRESSION))) > -1) { _domains.append(_fqdn); return; } // Always add the FQDN at the start of the list for // hostname == cookie-domainname checks! _domains.append(_fqdn); _domains.append(QL1C('.') + _fqdn); QStringList partList = _fqdn.split(QL1C('.'), QString::SkipEmptyParts); if (partList.count()) { partList.erase(partList.begin()); // Remove hostname } while (partList.count()) { if (partList.count() == 1) { break; // We only have a TLD left. } if ((partList.count() == 2) && m_twoLevelTLD.contains(partList[1].toLower())) { // This domain uses two-level TLDs in the form xxxx.yy break; } if ((partList.count() == 2) && (partList[1].length() == 2)) { // If this is a TLD, we should stop. (e.g. co.uk) // We assume this is a TLD if it ends with .xx.yy or .x.yy if (partList[0].length() <= 2) { break; // This is a TLD. } // Catch some TLDs that we miss with the previous check // e.g. com.au, org.uk, mil.co if (m_gTLDs.contains(partList[0].toLower())) { break; } } QString domain = partList.join(QLatin1Char('.')); _domains.append(domain); _domains.append(QL1C('.') + domain); partList.erase(partList.begin()); // Remove part } } // // This function parses cookie_headers and returns a linked list of // KHttpCookie objects for all cookies found in cookie_headers. // If no cookies could be found 0 is returned. // // cookie_headers should be a concatenation of all lines of a HTTP-header // which start with "Set-Cookie". The lines should be separated by '\n's. // KHttpCookieList KCookieJar::makeCookies(const QString &_url, const QByteArray &cookie_headers, WId windowId) { QString fqdn, path; if (!parseUrl(_url, fqdn, path)) { return KHttpCookieList(); // Error parsing _url } QString Name, Value; KHttpCookieList cookieList, cookieList2; bool isRFC2965 = false; bool crossDomain = false; const char *cookieStr = cookie_headers.constData(); QString defaultPath; const int index = path.lastIndexOf(QL1C('/')); if (index > 0) { defaultPath = path.left(index); } // Check for cross-domain flag from kio_http if (qstrncmp(cookieStr, "Cross-Domain\n", 13) == 0) { cookieStr += 13; crossDomain = true; } // The hard stuff :) for (;;) { // check for "Set-Cookie" if (qstrnicmp(cookieStr, "Set-Cookie:", 11) == 0) { cookieStr = parseNameValue(cookieStr + 11, Name, Value, true); // Host = FQDN // Default domain = "" // Default path according to rfc2109 KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookie.mCrossDomain = crossDomain; // Insert cookie in chain cookieList.append(cookie); } else if (qstrnicmp(cookieStr, "Set-Cookie2:", 12) == 0) { // Attempt to follow rfc2965 isRFC2965 = true; cookieStr = parseNameValue(cookieStr + 12, Name, Value, true, true); // Host = FQDN // Default domain = "" // Default path according to rfc2965 KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookie.mCrossDomain = crossDomain; // Insert cookie in chain cookieList2.append(cookie); } else { // This is not the start of a cookie header, skip till next line. while (*cookieStr && *cookieStr != '\n') { cookieStr++; } if (*cookieStr == '\n') { cookieStr++; } if (!*cookieStr) { break; // End of cookie_headers } else { continue; // end of this header, continue with next. } } while ((*cookieStr == ';') || (*cookieStr == ' ')) { cookieStr++; // Name-Value pair follows cookieStr = parseNameValue(cookieStr, Name, Value); KHttpCookie &lastCookie = (isRFC2965 ? cookieList2.last() : cookieList.last()); if (Name.compare(QL1S("domain"), Qt::CaseInsensitive) == 0) { QString dom = Value.toLower(); // RFC2965 3.2.2: If an explicitly specified value does not // start with a dot, the user agent supplies a leading dot if (dom.length() > 0 && dom[0] != QL1C('.')) { dom.prepend(QL1C('.')); } // remove a trailing dot if (dom.length() > 2 && dom[dom.length() - 1] == QL1C('.')) { dom = dom.left(dom.length() - 1); } if (dom.count(QL1C('.')) > 1 || dom == QLatin1String(".local")) { lastCookie.mDomain = dom; } } else if (Name.compare(QL1S("max-age"), Qt::CaseInsensitive) == 0) { int max_age = Value.toInt(); if (max_age == 0) { lastCookie.mExpireDate = 1; } else { lastCookie.mExpireDate = toEpochSecs(QDateTime::currentDateTimeUtc().addSecs(max_age)); } } else if (Name.compare(QL1S("expires"), Qt::CaseInsensitive) == 0) { const QDateTime dt = parseDate(Value); if (dt.isValid()) { lastCookie.mExpireDate = toEpochSecs(dt); if (lastCookie.mExpireDate == 0) { lastCookie.mExpireDate = 1; } } } else if (Name.compare(QL1S("path"), Qt::CaseInsensitive) == 0) { if (Value.isEmpty()) { lastCookie.mPath.clear(); // Catch "" <> QString() } else { lastCookie.mPath = QUrl::fromPercentEncoding(Value.toLatin1()); } lastCookie.mExplicitPath = true; } else if (Name.compare(QL1S("version"), Qt::CaseInsensitive) == 0) { lastCookie.mProtocolVersion = Value.toInt(); } else if (Name.compare(QL1S("secure"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("secure"), Qt::CaseInsensitive) == 0)) { lastCookie.mSecure = true; } else if (Name.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0)) { lastCookie.mHttpOnly = true; } else if (isRFC2965 && (Name.compare(QL1S("port"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("port"), Qt::CaseInsensitive) == 0))) { // Based on the port selection rule of RFC 2965 section 3.3.4... if (Name.isEmpty()) { // We intentionally append a -1 first in order to distinguish // between only a 'Port' vs a 'Port="80 443"' in the sent cookie. lastCookie.mPorts.append(-1); const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) || _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive)); if (secureRequest) { lastCookie.mPorts.append(443); } else { lastCookie.mPorts.append(80); } } else { bool ok; const QStringList portNums = Value.split(QL1C(' '), QString::SkipEmptyParts); Q_FOREACH (const QString &portNum, portNums) { const int port = portNum.toInt(&ok); if (ok) { lastCookie.mPorts.append(port); } } } } } if (*cookieStr == '\0') { break; // End of header } // Skip ';' or '\n' cookieStr++; } // RFC2965 cookies come last so that they override netscape cookies. while (!cookieList2.isEmpty()) { KHttpCookie &lastCookie = cookieList2.first(); removeDuplicateFromList(&cookieList, lastCookie, true); cookieList.append(lastCookie); cookieList2.removeFirst(); } return cookieList; } /** * Parses cookie_domstr and returns a linked list of KHttpCookie objects. * cookie_domstr should be a semicolon-delimited list of "name=value" * pairs. Any whitespace before "name" or around '=' is discarded. * If no cookies are found, 0 is returned. */ KHttpCookieList KCookieJar::makeDOMCookies(const QString &_url, const QByteArray &cookie_domstring, WId windowId) { // A lot copied from above KHttpCookieList cookieList; const char *cookieStr = cookie_domstring.data(); QString fqdn; QString path; if (!parseUrl(_url, fqdn, path)) { // Error parsing _url return KHttpCookieList(); } QString Name; QString Value; // This time it's easy while (*cookieStr) { cookieStr = parseNameValue(cookieStr, Name, Value); // Host = FQDN // Default domain = "" // Default path = "" KHttpCookie cookie(fqdn, QString(), QString(), Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookieList.append(cookie); if (*cookieStr != '\0') { cookieStr++; // Skip ';' or '\n' } } return cookieList; } // KHttpCookieList sorting /////////////////////////////////////////////////////////////////////////// // We want the longest path first static bool compareCookies(const KHttpCookie &item1, const KHttpCookie &item2) { return item1.path().length() > item2.path().length(); } #ifdef MAX_COOKIE_LIMIT static void makeRoom(KHttpCookieList *cookieList, KHttpCookiePtr &cookiePtr) { // Too many cookies: throw one away, try to be somewhat clever KHttpCookiePtr lastCookie = 0; for (KHttpCookiePtr cookie = cookieList->first(); cookie; cookie = cookieList->next()) { if (compareCookies(cookie, cookiePtr)) { break; } lastCookie = cookie; } if (!lastCookie) { lastCookie = cookieList->first(); } cookieList->removeRef(lastCookie); } #endif // // This function hands a KHttpCookie object over to the cookie jar. // void KCookieJar::addCookie(KHttpCookie &cookie) { QStringList domains; // We always need to do this to make sure that the // that cookies of type hostname == cookie-domainname // are properly removed and/or updated as necessary! extractDomains(cookie.host(), domains); // If the cookie specifies a domain, check whether it is valid. Otherwise, // accept the cookie anyways but removes the domain="" value to prevent // cross-site cookie injection. if (!cookie.domain().isEmpty()) { if (!domains.contains(cookie.domain()) && !cookie.domain().endsWith(QL1C('.') + cookie.host())) { cookie.fixDomain(QString()); } } QStringListIterator it(domains); while (it.hasNext()) { const QString &key = it.next(); KHttpCookieList *list; if (key.isNull()) { list = m_cookieDomains.value(QL1S("")); } else { list = m_cookieDomains.value(key); } if (list) { removeDuplicateFromList(list, cookie, false, true); } } const QString domain = stripDomain(cookie); KHttpCookieList *cookieList; if (domain.isNull()) { cookieList = m_cookieDomains.value(QL1S("")); } else { cookieList = m_cookieDomains.value(domain); } if (!cookieList) { // Make a new cookie list cookieList = new KHttpCookieList(); // All cookies whose domain is not already // known to us should be added with KCookieDunno. // KCookieDunno means that we use the global policy. cookieList->setAdvice(KCookieDunno); m_cookieDomains.insert(domain, cookieList); // Update the list of domains m_domainList.append(domain); } // Add the cookie to the cookie list // The cookie list is sorted 'longest path first' if (!cookie.isExpired()) { #ifdef MAX_COOKIE_LIMIT if (cookieList->count() >= MAX_COOKIES_PER_HOST) { makeRoom(cookieList, cookie); // Delete a cookie } #endif cookieList->push_back(cookie); // Use a stable sort so that unit tests are reliable. // In practice it doesn't matter though. qStableSort(cookieList->begin(), cookieList->end(), compareCookies); m_cookiesChanged = true; } } // // This function advises whether a single KHttpCookie object should // be added to the cookie jar. // KCookieAdvice KCookieJar::cookieAdvice(const KHttpCookie &cookie) const { if (m_rejectCrossDomainCookies && cookie.isCrossDomain()) { return KCookieReject; } if (cookie.getUserSelectedAdvice() != KCookieDunno) { return cookie.getUserSelectedAdvice(); } if (m_autoAcceptSessionCookies && cookie.expireDate() == 0) { return KCookieAccept; } QStringList domains; extractDomains(cookie.host(), domains); KCookieAdvice advice = KCookieDunno; QStringListIterator it(domains); while (advice == KCookieDunno && it.hasNext()) { const QString &domain = it.next(); if (domain.startsWith(QL1C('.')) || cookie.host() == domain) { KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { advice = cookieList->getAdvice(); } } } if (advice == KCookieDunno) { advice = m_globalAdvice; } return advice; } // // This function tells whether a single KHttpCookie object should // be considered persistent. Persistent cookies do not get deleted // at the end of the session and are saved on disk. // bool KCookieJar::cookieIsPersistent(const KHttpCookie &cookie) const { if (cookie.expireDate() == 0) { return false; } KCookieAdvice advice = cookieAdvice(cookie); if (advice == KCookieReject || advice == KCookieAcceptForSession) { return false; } return true; } // // This function gets the advice for all cookies originating from // _domain. // KCookieAdvice KCookieJar::getDomainAdvice(const QString &_domain) const { KHttpCookieList *cookieList = m_cookieDomains.value(_domain); KCookieAdvice advice; if (cookieList) { advice = cookieList->getAdvice(); } else { advice = KCookieDunno; } return advice; } // // This function sets the advice for all cookies originating from // _domain. // void KCookieJar::setDomainAdvice(const QString &_domain, KCookieAdvice _advice) { QString domain(_domain); KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { if (cookieList->getAdvice() != _advice) { m_configChanged = true; // domain is already known cookieList->setAdvice(_advice); } if ((cookieList->isEmpty()) && (_advice == KCookieDunno)) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } } else { // domain is not yet known if (_advice != KCookieDunno) { // We should create a domain entry m_configChanged = true; // Make a new cookie list cookieList = new KHttpCookieList(); cookieList->setAdvice(_advice); m_cookieDomains.insert(domain, cookieList); // Update the list of domains m_domainList.append(domain); } } } // // This function sets the advice for all cookies originating from // the same domain as _cookie // void KCookieJar::setDomainAdvice(const KHttpCookie &cookie, KCookieAdvice _advice) { QString domain; stripDomain(cookie.host(), domain); // We file the cookie under this domain. setDomainAdvice(domain, _advice); } // // This function sets the global advice for cookies // void KCookieJar::setGlobalAdvice(KCookieAdvice _advice) { if (m_globalAdvice != _advice) { m_configChanged = true; } m_globalAdvice = _advice; } // // Get a list of all domains known to the cookie jar. // const QStringList &KCookieJar::getDomainList() { return m_domainList; } // // Get a list of all cookies in the cookie jar originating from _domain. // KHttpCookieList *KCookieJar::getCookieList(const QString &_domain, const QString &_fqdn) { QString domain; if (_domain.isEmpty()) { stripDomain(_fqdn, domain); } else { domain = _domain; } return m_cookieDomains.value(domain); } // // Eat a cookie out of the jar. // cookieIterator should be one of the cookies returned by getCookieList() // void KCookieJar::eatCookie(const KHttpCookieList::iterator &cookieIterator) { const KHttpCookie &cookie = *cookieIterator; const QString domain = stripDomain(cookie); // We file the cookie under this domain. KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { // This deletes cookie! cookieList->erase(cookieIterator); if ((cookieList->isEmpty()) && (cookieList->getAdvice() == KCookieDunno)) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } } } void KCookieJar::eatCookiesForDomain(const QString &domain) { KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (!cookieList || cookieList->isEmpty()) { return; } cookieList->clear(); if (cookieList->getAdvice() == KCookieDunno) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } m_cookiesChanged = true; } void KCookieJar::eatSessionCookies(long windowId) { if (!windowId) { return; } Q_FOREACH (const QString &domain, m_domainList) { eatSessionCookies(domain, windowId, false); } } void KCookieJar::eatAllCookies() { Q_FOREACH (const QString &domain, m_domainList) { eatCookiesForDomain(domain); // This might remove domain from m_domainList! } } void KCookieJar::eatSessionCookies(const QString &fqdn, WId windowId, bool isFQDN) { KHttpCookieList *cookieList; if (!isFQDN) { cookieList = m_cookieDomains.value(fqdn); } else { QString domain; stripDomain(fqdn, domain); cookieList = m_cookieDomains.value(domain); } if (cookieList) { QMutableListIterator cookieIterator(*cookieList); while (cookieIterator.hasNext()) { KHttpCookie &cookie = cookieIterator.next(); if (cookieIsPersistent(cookie)) { continue; } QList &ids = cookie.windowIds(); #ifndef NDEBUG if (ids.contains(windowId)) { if (ids.count() > 1) { qCDebug(KIO_COOKIEJAR) << "removing window id" << windowId << "from session cookie"; } else { qCDebug(KIO_COOKIEJAR) << "deleting session cookie"; } } #endif if (!ids.removeAll(windowId) || !ids.isEmpty()) { continue; } cookieIterator.remove(); } } } static QString hostWithPort(const KHttpCookie *cookie) { const QList &ports = cookie->ports(); if (ports.isEmpty()) { return cookie->host(); } QStringList portList; Q_FOREACH (int port, ports) { portList << QString::number(port); } return (cookie->host() + QL1C(':') + portList.join(QLatin1Char(','))); } // // Saves all cookies to the file '_filename'. // On success 'true' is returned. // On failure 'false' is returned. bool KCookieJar::saveCookies(const QString &_filename) { QSaveFile cookieFile(_filename); if (!cookieFile.open(QIODevice::WriteOnly)) { return false; } QTextStream ts(&cookieFile); ts << "# KDE Cookie File v2\n#\n"; QString s; s.sprintf("%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n", "# Host", "Domain", "Path", "Exp.date", "Prot", "Name", "Sec", "Value"); ts << s.toLatin1().constData(); QStringListIterator it(m_domainList); while (it.hasNext()) { const QString &domain = it.next(); bool domainPrinted = false; KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (!cookieList) { continue; } QMutableListIterator cookieIterator(*cookieList); while (cookieIterator.hasNext()) { const KHttpCookie &cookie = cookieIterator.next(); if (cookie.isExpired()) { // Delete expired cookies cookieIterator.remove(); continue; } if (cookieIsPersistent(cookie)) { // Only save cookies that are not "session-only cookies" if (!domainPrinted) { domainPrinted = true; ts << '[' << domain.toLocal8Bit().data() << "]\n"; } // Store persistent cookies const QString path = QL1C('"') + cookie.path() + QL1C('"'); const QString domain = QL1C('"') + cookie.domain() + QL1C('"'); const QString host = hostWithPort(&cookie); // TODO: replace with direct QTextStream output ? s.sprintf("%-20s %-20s %-12s %10lld %3d %-20s %-4i %s\n", host.toLatin1().constData(), domain.toLatin1().constData(), path.toLatin1().constData(), cookie.expireDate(), cookie.protocolVersion(), cookie.name().isEmpty() ? cookie.value().toLatin1().constData() : cookie.name().toLatin1().constData(), (cookie.isSecure() ? 1 : 0) + (cookie.isHttpOnly() ? 2 : 0) + (cookie.hasExplicitPath() ? 4 : 0) + (cookie.name().isEmpty() ? 8 : 0), cookie.value().toLatin1().constData()); ts << s.toLatin1().constData(); } } } if (cookieFile.commit()) { QFile::setPermissions(_filename, QFile::ReadUser | QFile::WriteUser); return true; } return false; } static const char *parseField(char *&buffer, bool keepQuotes = false) { char *result; if (!keepQuotes && (*buffer == '\"')) { // Find terminating " buffer++; result = buffer; while ((*buffer != '\"') && (*buffer)) { buffer++; } } else { // Find first white space result = buffer; while ((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer)) { buffer++; } } if (!*buffer) { return result; // } *buffer++ = '\0'; // Skip white-space while ((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n')) { buffer++; } return result; } static QString extractHostAndPorts(const QString &str, QList *ports = nullptr) { if (str.isEmpty()) { return str; } const int index = str.indexOf(QL1C(':')); if (index == -1) { return str; } const QString host = str.left(index); if (ports) { bool ok; QStringList portList = str.mid(index + 1).split(QL1C(',')); Q_FOREACH (const QString &portStr, portList) { const int portNum = portStr.toInt(&ok); if (ok) { ports->append(portNum); } } } return host; } // // Reloads all cookies from the file '_filename'. // On success 'true' is returned. // On failure 'false' is returned. bool KCookieJar::loadCookies(const QString &_filename) { QFile cookieFile(_filename); if (!cookieFile.open(QIODevice::ReadOnly)) { return false; } int version = 1; bool success = false; char *buffer = new char[READ_BUFFER_SIZE]; qint64 len = cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1); if (len != -1) { if (qstrcmp(buffer, "# KDE Cookie File\n") == 0) { success = true; } else if (qstrcmp(buffer, "# KDE Cookie File v") > 0) { bool ok = false; const int verNum = QByteArray(buffer + 19, len - 19).trimmed().toInt(&ok); if (ok) { version = verNum; success = true; } } } if (success) { const qint64 currentTime = epoch(); QList ports; while (cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1) != -1) { char *line = buffer; // Skip lines which begin with '#' or '[' if ((line[0] == '#') || (line[0] == '[')) { continue; } const QString host = extractHostAndPorts(QL1S(parseField(line)), &ports); const QString domain = QL1S(parseField(line)); if (host.isEmpty() && domain.isEmpty()) { continue; } const QString path = QL1S(parseField(line)); const QString expStr = QL1S(parseField(line)); if (expStr.isEmpty()) { continue; } const qint64 expDate = expStr.toLongLong(); const QString verStr = QL1S(parseField(line)); if (verStr.isEmpty()) { continue; } int protVer = verStr.toInt(); QString name = QL1S(parseField(line)); bool keepQuotes = false; bool secure = false; bool httpOnly = false; bool explicitPath = false; const char *value = nullptr; if ((version == 2) || (protVer >= 200)) { if (protVer >= 200) { protVer -= 200; } int i = atoi(parseField(line)); secure = i & 1; httpOnly = i & 2; explicitPath = i & 4; if (i & 8) { name = QLatin1String(""); } line[strlen(line) - 1] = '\0'; // Strip LF. value = line; } else { if (protVer >= 100) { protVer -= 100; keepQuotes = true; } value = parseField(line, keepQuotes); secure = QByteArray(parseField(line)).toShort(); } // Expired or parse error if (!value || expDate == 0 || expDate < currentTime) { continue; } KHttpCookie cookie(host, domain, path, name, QString::fromUtf8(value), expDate, protVer, secure, httpOnly, explicitPath); if (ports.count()) { cookie.mPorts = ports; } addCookie(cookie); } } delete [] buffer; m_cookiesChanged = false; return success; } // // Save the cookie configuration // void KCookieJar::saveConfig(KConfig *_config) { if (!m_configChanged) { return; } KConfigGroup dlgGroup(_config, "Cookie Dialog"); dlgGroup.writeEntry("PreferredPolicy", static_cast(m_preferredPolicy)); dlgGroup.writeEntry("ShowCookieDetails", m_showCookieDetails); KConfigGroup policyGroup(_config, "Cookie Policy"); policyGroup.writeEntry("CookieGlobalAdvice", adviceToStr(m_globalAdvice)); QStringList domainSettings; QStringListIterator it(m_domainList); while (it.hasNext()) { const QString &domain = it.next(); KCookieAdvice advice = getDomainAdvice(domain); if (advice != KCookieDunno) { const QString value = domain + QL1C(':') + adviceToStr(advice); domainSettings.append(value); } } policyGroup.writeEntry("CookieDomainAdvice", domainSettings); _config->sync(); m_configChanged = false; } // // Load the cookie configuration // void KCookieJar::loadConfig(KConfig *_config, bool reparse) { if (reparse) { _config->reparseConfiguration(); } KConfigGroup dlgGroup(_config, "Cookie Dialog"); m_showCookieDetails = dlgGroup.readEntry("ShowCookieDetails", false); m_preferredPolicy = static_cast(dlgGroup.readEntry("PreferredPolicy", 0)); KConfigGroup policyGroup(_config, "Cookie Policy"); const QStringList domainSettings = policyGroup.readEntry("CookieDomainAdvice", QStringList()); // Warning: those default values are duplicated in the kcm (kio/kcookiespolicies.cpp) m_rejectCrossDomainCookies = policyGroup.readEntry("RejectCrossDomainCookies", true); m_autoAcceptSessionCookies = policyGroup.readEntry("AcceptSessionCookies", true); m_globalAdvice = strToAdvice(policyGroup.readEntry("CookieGlobalAdvice", QStringLiteral("Accept"))); // Reset current domain settings first. Q_FOREACH (const QString &domain, m_domainList) { setDomainAdvice(domain, KCookieDunno); } // Now apply the domain settings read from config file... for (QStringList::ConstIterator it = domainSettings.constBegin(), itEnd = domainSettings.constEnd(); it != itEnd; ++it) { const QString &value = *it; const int sepPos = value.lastIndexOf(QL1C(':')); if (sepPos <= 0) { continue; } const QString domain(value.left(sepPos)); KCookieAdvice advice = strToAdvice(value.mid(sepPos + 1)); setDomainAdvice(domain, advice); } } QDebug operator<<(QDebug dbg, const KHttpCookie &cookie) { dbg.nospace() << cookie.cookieStr(false); return dbg.space(); } QDebug operator<<(QDebug dbg, const KHttpCookieList &list) { Q_FOREACH (const KHttpCookie &cookie, list) { dbg << cookie; } return dbg; } diff --git a/src/ioslaves/http/parsinghelpers.cpp b/src/ioslaves/http/parsinghelpers.cpp index 5f74769b..2fa29294 100644 --- a/src/ioslaves/http/parsinghelpers.cpp +++ b/src/ioslaves/http/parsinghelpers.cpp @@ -1,600 +1,600 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Andreas Hartmetz Copyright (C) 2010,2011 Rolf Eike Beer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include // Advance *pos beyond spaces / tabs static void skipSpace(const char input[], int *pos, int end) { int idx = *pos; while (idx < end && (input[idx] == ' ' || input[idx] == '\t')) { idx++; } *pos = idx; return; } // Advance *pos to start of next line while being forgiving about line endings. // Return false if the end of the header has been reached, true otherwise. static bool nextLine(const char input[], int *pos, int end) { int idx = *pos; while (idx < end && input[idx] != '\r' && input[idx] != '\n') { idx++; } int rCount = 0; int nCount = 0; while (idx < end && qMax(rCount, nCount) < 2 && (input[idx] == '\r' || input[idx] == '\n')) { input[idx] == '\r' ? rCount++ : nCount++; idx++; } if (idx < end && qMax(rCount, nCount) == 2 && qMin(rCount, nCount) == 1) { // if just one of the others is missing eat it too. // this ensures that conforming headers using the proper // \r\n sequence (and also \n\r) will be parsed correctly. if ((rCount == 1 && input[idx] == '\r') || (nCount == 1 && input[idx] == '\n')) { idx++; } } *pos = idx; return idx < end && rCount < 2 && nCount < 2; } // QByteArray::fromPercentEncoding() does not notify us about encoding errors so we need // to check here if this is valid at all. static bool isValidPercentEncoding(const QByteArray &data) { int i = 0; const int last = data.length() - 1; const char *d = data.constData(); while ((i = data.indexOf('%', i)) != -1) { if (i >= last - 2) { return false; } if (! isxdigit(d[i + 1])) { return false; } if (! isxdigit(d[i + 2])) { return false; } i++; } return true; } QByteArray TokenIterator::next() { QPair token = m_tokens[m_currentToken++]; //fromRawData brings some speed advantage but also the requirement to keep the text buffer //around. this together with implicit sharing (you don't know where copies end up) //is dangerous! //return QByteArray::fromRawData(&m_buffer[token.first], token.second - token.first); return QByteArray(&m_buffer[token.first], token.second - token.first); } QByteArray TokenIterator::current() const { QPair token = m_tokens[m_currentToken - 1]; //return QByteArray::fromRawData(&m_buffer[token.first], token.second - token.first); return QByteArray(&m_buffer[token.first], token.second - token.first); } QList TokenIterator::all() const { QList ret; for (int i = 0; i < m_tokens.count(); i++) { QPair token = m_tokens[i]; ret.append(QByteArray(&m_buffer[token.first], token.second - token.first)); } return ret; } HeaderTokenizer::HeaderTokenizer(char *buffer) : m_buffer(buffer) { // add information about available headers and whether they have one or multiple, // comma-separated values. //The following response header fields are from RFC 2616 unless otherwise specified. //Hint: search the web for e.g. 'http "accept-ranges header"' to find information about //a header field. static const HeaderFieldTemplate headerFieldTemplates[] = { {"accept-ranges", false}, {"age", false}, {"cache-control", true}, {"connection", true}, {"content-disposition", false}, //is multi-valued in a way, but with ";" separator! {"content-encoding", true}, {"content-language", true}, {"content-length", false}, {"content-location", false}, {"content-md5", false}, {"content-type", false}, {"date", false}, {"dav", true}, //RFC 2518 {"etag", false}, {"expires", false}, {"keep-alive", true}, //RFC 2068 {"last-modified", false}, {"link", false}, //RFC 2068, multi-valued with ";" separator {"location", false}, {"p3p", true}, // http://www.w3.org/TR/P3P/ {"pragma", true}, {"proxy-authenticate", false}, //complicated multi-valuedness: quoted commas don't separate //multiple values. we handle this at a higher level. {"proxy-connection", true}, //inofficial but well-known; to avoid misunderstandings //when using "connection" when talking to a proxy. {"refresh", false}, //not sure, only found some mailing list posts mentioning it {"set-cookie", false}, //RFC 2109; the multi-valuedness seems to be usually achieved //by sending several instances of this field as opposed to //usually comma-separated lists with maybe multiple instances. {"transfer-encoding", true}, {"upgrade", true}, {"warning", true}, {"www-authenticate", false} //see proxy-authenticate }; for (uint i = 0; i < sizeof(headerFieldTemplates) / sizeof(HeaderFieldTemplate); i++) { const HeaderFieldTemplate &ft = headerFieldTemplates[i]; insert(QByteArray(ft.name), HeaderField(ft.isMultiValued)); } } int HeaderTokenizer::tokenize(int begin, int end) { char *buf = m_buffer; //keep line length in check :/ int idx = begin; int startIdx = begin; //multi-purpose start of current token bool multiValuedEndedWithComma = false; //did the last multi-valued line end with a comma? QByteArray headerKey; do { if (buf[idx] == ' ' || buf [idx] == '\t') { // line continuation; preserve startIdx except (see below) if (headerKey.isEmpty()) { continue; } // turn CR/LF into spaces for later parsing convenience int backIdx = idx - 1; while (backIdx >= begin && (buf[backIdx] == '\r' || buf[backIdx] == '\n')) { buf[backIdx--] = ' '; } // multiple values, comma-separated: add new value or continue previous? if (operator[](headerKey).isMultiValued) { if (multiValuedEndedWithComma) { // start new value; this is almost like no line continuation skipSpace(buf, &idx, end); startIdx = idx; } else { // continue previous value; this is tricky. unit tests to the rescue! if (operator[](headerKey).beginEnd.last().first == startIdx) { // remove entry, it will be re-added because already idx != startIdx operator[](headerKey).beginEnd.removeLast(); } else { // no comma, no entry: the prev line was whitespace only - start new value skipSpace(buf, &idx, end); startIdx = idx; } } } } else { // new field startIdx = idx; // also make sure that there is at least one char after the colon while (idx < (end - 1) && buf[idx] != ':' && buf[idx] != '\r' && buf[idx] != '\n') { buf[idx] = tolower(buf[idx]); idx++; } if (buf[idx] != ':') { //malformed line: no colon headerKey.clear(); continue; } headerKey = QByteArray(&buf[startIdx], idx - startIdx); if (!contains(headerKey)) { //we don't recognize this header line headerKey.clear(); continue; } // skip colon & leading whitespace idx++; skipSpace(buf, &idx, end); startIdx = idx; } // we have the name/key of the field, now parse the value if (!operator[](headerKey).isMultiValued) { // scan to end of line while (idx < end && buf[idx] != '\r' && buf[idx] != '\n') { idx++; } if (!operator[](headerKey).beginEnd.isEmpty()) { // there already is an entry; are we just in a line continuation? if (operator[](headerKey).beginEnd.last().first == startIdx) { // line continuation: delete previous entry and later insert a new, longer one. operator[](headerKey).beginEnd.removeLast(); } } operator[](headerKey).beginEnd.append(QPair(startIdx, idx)); } else { // comma-separated list while (true) { //skip one value while (idx < end && buf[idx] != '\r' && buf[idx] != '\n' && buf[idx] != ',') { idx++; } if (idx != startIdx) { operator[](headerKey).beginEnd.append(QPair(startIdx, idx)); } multiValuedEndedWithComma = buf[idx] == ','; //skip comma(s) and leading whitespace, if any respectively while (idx < end && buf[idx] == ',') { idx++; } skipSpace(buf, &idx, end); //next value or end-of-line / end of header? if (buf[idx] >= end || buf[idx] == '\r' || buf[idx] == '\n') { break; } //next value startIdx = idx; } } } while (nextLine(buf, &idx, end)); return idx; } TokenIterator HeaderTokenizer::iterator(const char *key) const { QByteArray keyBa = QByteArray::fromRawData(key, strlen(key)); if (contains(keyBa)) { return TokenIterator(value(keyBa).beginEnd, m_buffer); } else { return TokenIterator(m_nullTokens, m_buffer); } } static void skipLWS(const QString &str, int &pos) { while (pos < str.length() && (str[pos] == QLatin1Char(' ') || str[pos] == QLatin1Char('\t'))) { ++pos; } } // keep the common ending, this allows the compiler to join them static const char typeSpecials[] = "{}*'%()<>@,;:\\\"/[]?="; static const char attrSpecials[] = "'%()<>@,;:\\\"/[]?="; static const char valueSpecials[] = "()<>@,;:\\\"/[]?="; static bool specialChar(const QChar &ch, const char *specials) { // WORKAROUND: According to RFC 2616, any character other than ascii // characters should NOT be allowed in unquoted content-disposition file // names. However, since none of the major browsers follow this rule, we do // the same thing here and allow all printable unicode characters. See // https://bugs.kde.org/show_bug.cgi?id=261223 for the details. if (!ch.isPrint()) { return true; } for (int i = qstrlen(specials) - 1; i >= 0; i--) { if (ch == QLatin1Char(specials[i])) { return true; } } return false; } /** * read and parse the input until the given terminator * @param str input string to parse * @param term terminator * @param pos position marker in the input string * @param specials characters forbidden in this section * @return the next section or an empty string if it was invalid * * Extracts token-like input until terminator char or EOL. * Also skips over the terminator. * * pos is correctly incremented even if this functions returns * an empty string so this can be used to skip over invalid * parts and continue. */ static QString extractUntil(const QString &str, QChar term, int &pos, const char *specials) { QString out; skipLWS(str, pos); bool valid = true; while (pos < str.length() && (str[pos] != term)) { out += str[pos]; valid = (valid && !specialChar(str[pos], specials)); ++pos; } if (pos < str.length()) { // Stopped due to finding term ++pos; } if (!valid) { return QString(); } // Remove trailing linear whitespace... while (out.endsWith(QLatin1Char(' ')) || out.endsWith(QLatin1Char('\t'))) { out.chop(1); } if (out.contains(QLatin1Char(' '))) { out.clear(); } return out; } // As above, but also handles quotes.. // pos is set to -1 on parse error static QString extractMaybeQuotedUntil(const QString &str, int &pos) { const QChar term = QLatin1Char(';'); skipLWS(str, pos); // Are we quoted? if (pos < str.length() && str[pos] == QLatin1Char('"')) { QString out; // Skip the quote... ++pos; // when quoted we also need an end-quote bool endquote = false; // Parse until trailing quote... while (pos < str.length()) { if (str[pos] == QLatin1Char('\\') && pos + 1 < str.length()) { // quoted-pair = "\" CHAR out += str[pos + 1]; pos += 2; // Skip both... } else if (str[pos] == QLatin1Char('"')) { ++pos; endquote = true; break; } else if (!str[pos].isPrint()) { // Don't allow CTL's RFC 2616 sec 2.2 break; } else { out += str[pos]; ++pos; } } if (!endquote) { pos = -1; return QString(); } // Skip until term.. while (pos < str.length() && (str[pos] != term)) { if ((str[pos] != QLatin1Char(' ')) && (str[pos] != QLatin1Char('\t'))) { pos = -1; return QString(); } ++pos; } if (pos < str.length()) { // Stopped due to finding term ++pos; } return out; } else { return extractUntil(str, term, pos, valueSpecials); } } static QMap contentDispositionParserInternal(const QString &disposition) { // qDebug() << "disposition: " << disposition; int pos = 0; const QString strDisposition = extractUntil(disposition, QLatin1Char(';'), pos, typeSpecials).toLower(); QMap parameters; QMap contparams; // all parameters that contain continuations QMap encparams; // all parameters that have character encoding // the type is invalid, the complete header is junk if (strDisposition.isEmpty()) { return parameters; } parameters.insert(QStringLiteral("type"), strDisposition); while (pos < disposition.length()) { QString key = extractUntil(disposition, QLatin1Char('='), pos, attrSpecials).toLower(); if (key.isEmpty()) { // parse error in this key: do not parse more, but add up // everything we already got // qDebug() << "parse error in key, abort parsing"; break; } QString val; if (key.endsWith(QLatin1Char('*'))) { val = extractUntil(disposition, QLatin1Char(';'), pos, valueSpecials); } else { val = extractMaybeQuotedUntil(disposition, pos); } if (val.isEmpty()) { if (pos == -1) { // qDebug() << "parse error in value, abort parsing"; break; } continue; } const int spos = key.indexOf(QLatin1Char('*')); if (spos == key.length() - 1) { key.chop(1); encparams.insert(key, val); } else if (spos >= 0) { contparams.insert(key, val); } else if (parameters.contains(key)) { // qDebug() << "duplicate key" << key << "found, ignoring everything more"; parameters.remove(key); return parameters; } else { parameters.insert(key, val); } } QMap::iterator i = contparams.begin(); while (i != contparams.end()) { QString key = i.key(); int spos = key.indexOf(QLatin1Char('*')); bool hasencoding = false; if (key.at(spos + 1) != QLatin1Char('0')) { ++i; continue; } // no leading zeros allowed, so delete the junk int klen = key.length(); if (klen > spos + 2) { // nothing but continuations and encodings may insert * into parameter name if ((klen > spos + 3) || ((klen == spos + 3) && (key.at(spos + 2) != QLatin1Char('*')))) { // qDebug() << "removing invalid key " << key << "with val" << i.value() << key.at(spos + 2); i = contparams.erase(i); continue; } hasencoding = true; } int seqnum = 1; QMap::iterator partsi; // we do not need to care about encoding specifications: only the first // part is allowed to have one QString val = i.value(); key.chop(hasencoding ? 2 : 1); while ((partsi = contparams.find(key + QString::number(seqnum))) != contparams.end()) { val += partsi.value(); contparams.erase(partsi); } i = contparams.erase(i); key.chop(1); if (hasencoding) { encparams.insert(key, val); } else { if (parameters.contains(key)) { // qDebug() << "duplicate key" << key << "found, ignoring everything more"; parameters.remove(key); return parameters; } parameters.insert(key, val); } } for (QMap::iterator i = encparams.begin(); i != encparams.end(); ++i) { QString val = i.value(); // RfC 2231 encoded character set in filename int spos = val.indexOf(QLatin1Char('\'')); if (spos == -1) { continue; } int npos = val.indexOf(QLatin1Char('\''), spos + 1); if (npos == -1) { continue; } - const QString charset = val.left(spos); + const QStringRef charset = val.leftRef(spos); const QByteArray encodedVal = val.midRef(npos + 1).toLatin1(); if (! isValidPercentEncoding(encodedVal)) { continue; } const QByteArray rawval = QByteArray::fromPercentEncoding(encodedVal); if (charset.isEmpty() || (charset == QLatin1String("us-ascii"))) { bool valid = true; for (int j = rawval.length() - 1; (j >= 0) && valid; j--) { valid = (rawval.at(j) >= 32); } if (!valid) { continue; } val = QString::fromLatin1(rawval.constData()); } else { QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1()); if (!codec) { continue; } val = codec->toUnicode(rawval); } parameters.insert(i.key(), val); } return parameters; } static QMap contentDispositionParser(const QString &disposition) { QMap parameters = contentDispositionParserInternal(disposition); const QLatin1String fn("filename"); if (parameters.contains(fn)) { // Content-Disposition is not allowed to dictate directory // path, thus we extract the filename only. const QString val = QDir::toNativeSeparators(parameters[fn]); int slpos = val.lastIndexOf(QDir::separator()); if (slpos > -1) { parameters.insert(fn, val.mid(slpos + 1)); } } return parameters; } diff --git a/src/widgets/accessmanager.cpp b/src/widgets/accessmanager.cpp index 602c015f..aaf2740f 100644 --- a/src/widgets/accessmanager.cpp +++ b/src/widgets/accessmanager.cpp @@ -1,571 +1,571 @@ /* * This file is part of the KDE project. * * Copyright (C) 2009 - 2012 Dawit Alemayehu * Copyright (C) 2008 - 2009 Urs Wolfer * Copyright (C) 2007 Trolltech ASA * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "accessmanager.h" #include "accessmanagerreply_p.h" #include "job.h" #include "kjobwidgets.h" #include "scheduler.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) static const QNetworkRequest::Attribute gSynchronousNetworkRequestAttribute = QNetworkRequest::SynchronousRequestAttribute; static qint64 sizeFromRequest(const QNetworkRequest &req) { const QVariant size = req.header(QNetworkRequest::ContentLengthHeader); if (!size.isValid()) { return -1; } bool ok = false; const qlonglong value = size.toLongLong(&ok); return (ok ? value : -1); } namespace KIO { class Q_DECL_HIDDEN AccessManager::AccessManagerPrivate { public: AccessManagerPrivate() : externalContentAllowed(true), emitReadyReadOnMetaDataChange(false), window(nullptr) {} void setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData); bool externalContentAllowed; bool emitReadyReadOnMetaDataChange; KIO::MetaData requestMetaData; KIO::MetaData sessionMetaData; QPointer window; }; namespace Integration { class Q_DECL_HIDDEN CookieJar::CookieJarPrivate { public: CookieJarPrivate() : windowId((WId) - 1), isEnabled(true), isStorageDisabled(false) {} WId windowId; bool isEnabled; bool isStorageDisabled; }; } } using namespace KIO; AccessManager::AccessManager(QObject *parent) : QNetworkAccessManager(parent), d(new AccessManager::AccessManagerPrivate()) { // KDE Cookiejar (KCookieJar) integration... setCookieJar(new KIO::Integration::CookieJar); } AccessManager::~AccessManager() { delete d; } void AccessManager::setExternalContentAllowed(bool allowed) { d->externalContentAllowed = allowed; } bool AccessManager::isExternalContentAllowed() const { return d->externalContentAllowed; } #ifndef KIOWIDGETS_NO_DEPRECATED void AccessManager::setCookieJarWindowId(WId id) { QWidget *window = QWidget::find(id); if (!window) { return; } KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { jar->setWindowId(id); } d->window = window->isWindow() ? window : window->window(); } #endif void AccessManager::setWindow(QWidget *widget) { if (!widget) { return; } d->window = widget->isWindow() ? widget : widget->window(); if (!d->window) { return; } KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { jar->setWindowId(d->window->winId()); } } #ifndef KIOWIDGETS_NO_DEPRECATED WId AccessManager::cookieJarWindowid() const { KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { return jar->windowId(); } return 0; } #endif QWidget *AccessManager::window() const { return d->window; } KIO::MetaData &AccessManager::requestMetaData() { return d->requestMetaData; } KIO::MetaData &AccessManager::sessionMetaData() { return d->sessionMetaData; } void AccessManager::putReplyOnHold(QNetworkReply *reply) { KDEPrivate::AccessManagerReply *r = qobject_cast(reply); if (!r) { return; } r->putOnHold(); } void AccessManager::setEmitReadyReadOnMetaDataChange(bool enable) { d->emitReadyReadOnMetaDataChange = enable; } QNetworkReply *AccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData) { const QUrl reqUrl(req.url()); if (!d->externalContentAllowed && !KDEPrivate::AccessManagerReply::isLocalRequest(reqUrl) && reqUrl.scheme() != QL1S("data")) { //qDebug() << "Blocked: " << reqUrl; return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ContentAccessDenied, i18n("Blocked request."), this); } // Check if the internal ignore content disposition header is set. const bool ignoreContentDisposition = req.hasRawHeader("x-kdewebkit-ignore-disposition"); // Retrieve the KIO meta data... KIO::MetaData metaData; d->setMetaDataForRequest(req, metaData); KIO::SimpleJob *kioJob = nullptr; switch (op) { case HeadOperation: { //qDebug() << "HeadOperation:" << reqUrl; kioJob = KIO::mimetype(reqUrl, KIO::HideProgressInfo); break; } case GetOperation: { //qDebug() << "GetOperation:" << reqUrl; if (!reqUrl.path().isEmpty() || reqUrl.host().isEmpty()) { kioJob = KIO::storedGet(reqUrl, KIO::NoReload, KIO::HideProgressInfo); } else { kioJob = KIO::stat(reqUrl, KIO::HideProgressInfo); } // WORKAROUND: Avoid the brain damaged stuff QtWebKit does when a POST // operation is redirected! See BR# 268694. metaData.remove(QStringLiteral("content-type")); // Remove the content-type from a GET/HEAD request! break; } case PutOperation: { //qDebug() << "PutOperation:" << reqUrl; if (outgoingData) { Q_ASSERT(outgoingData->isReadable()); StoredTransferJob* storedJob = KIO::storedPut(outgoingData, reqUrl, -1, KIO::HideProgressInfo); storedJob->setAsyncDataEnabled(outgoingData->isSequential()); QVariant len = req.header(QNetworkRequest::ContentLengthHeader); if (len.isValid()) { storedJob->setTotalSize(len.toInt()); } kioJob = storedJob; } else { kioJob = KIO::put(reqUrl, -1, KIO::HideProgressInfo); } break; } case PostOperation: { kioJob = KIO::storedHttpPost(outgoingData, reqUrl, sizeFromRequest(req), KIO::HideProgressInfo); if (!metaData.contains(QStringLiteral("content-type"))) { const QVariant header = req.header(QNetworkRequest::ContentTypeHeader); if (header.isValid()) { metaData.insert(QStringLiteral("content-type"), (QStringLiteral("Content-Type: ") + header.toString())); } else { metaData.insert(QStringLiteral("content-type"), QStringLiteral("Content-Type: application/x-www-form-urlencoded")); } } break; } case DeleteOperation: { //qDebug() << "DeleteOperation:" << reqUrl; kioJob = KIO::http_delete(reqUrl, KIO::HideProgressInfo); break; } case CustomOperation: { const QByteArray &method = req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray(); //qDebug() << "CustomOperation:" << reqUrl << "method:" << method << "outgoing data:" << outgoingData; if (method.isEmpty()) { return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ProtocolUnknownError, i18n("Unknown HTTP verb."), this); } const qint64 size = sizeFromRequest(req); if (size > 0) { kioJob = KIO::http_post(reqUrl, outgoingData, size, KIO::HideProgressInfo); } else { kioJob = KIO::get(reqUrl, KIO::NoReload, KIO::HideProgressInfo); } metaData.insert(QStringLiteral("CustomHTTPMethod"), QString::fromUtf8(method)); break; } default: { qCWarning(KIO_WIDGETS) << "Unsupported KIO operation requested! Deferring to QNetworkAccessManager..."; return QNetworkAccessManager::createRequest(op, req, outgoingData); } } // Set the job priority switch (req.priority()) { case QNetworkRequest::HighPriority: KIO::Scheduler::setJobPriority(kioJob, -5); break; case QNetworkRequest::LowPriority: KIO::Scheduler::setJobPriority(kioJob, 5); break; default: break; } KDEPrivate::AccessManagerReply *reply; /* NOTE: Here we attempt to handle synchronous XHR requests. Unfortunately, due to the fact that QNAM is both synchronous and multi-thread while KIO is completely the opposite (asynchronous and not thread safe), the code below might cause crashes like the one reported in bug# 287778 (nested event loops are inherently dangerous). Unfortunately, all attempts to address the crash has so far failed due to the many regressions they caused, e.g. bug# 231932 and 297954. Hence, until a solution is found, we have to live with the side effects of creating nested event loops. */ if (req.attribute(gSynchronousNetworkRequestAttribute).toBool()) { KJobWidgets::setWindow(kioJob, d->window); kioJob->setRedirectionHandlingEnabled(true); if (kioJob->exec()) { QByteArray data; if (StoredTransferJob *storedJob = qobject_cast< KIO::StoredTransferJob * >(kioJob)) { data = storedJob->data(); } reply = new KDEPrivate::AccessManagerReply(op, req, data, kioJob->url(), kioJob->metaData(), this); //qDebug() << "Synchronous XHR:" << reply << reqUrl; } else { qCWarning(KIO_WIDGETS) << "Failed to create a synchronous XHR for" << reqUrl; qCWarning(KIO_WIDGETS) << "REASON:" << kioJob->errorString(); reply = new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::UnknownNetworkError, kioJob->errorText(), this); } } else { // Set the window on the KIO ui delegate if (d->window) { KJobWidgets::setWindow(kioJob, d->window); } // Disable internal automatic redirection handling kioJob->setRedirectionHandlingEnabled(false); // Set the job priority switch (req.priority()) { case QNetworkRequest::HighPriority: KIO::Scheduler::setJobPriority(kioJob, -5); break; case QNetworkRequest::LowPriority: KIO::Scheduler::setJobPriority(kioJob, 5); break; default: break; } // Set the meta data for this job... kioJob->setMetaData(metaData); // Create the reply... reply = new KDEPrivate::AccessManagerReply(op, req, kioJob, d->emitReadyReadOnMetaDataChange, this); //qDebug() << reply << reqUrl; } if (ignoreContentDisposition && reply) { //qDebug() << "Content-Disposition WILL BE IGNORED!"; reply->setIgnoreContentDisposition(ignoreContentDisposition); } return reply; } static inline void moveMetaData(KIO::MetaData &metaData, const QString &metaDataKey, QNetworkRequest &request, const QByteArray &requestKey) { if (request.hasRawHeader(requestKey)) { metaData.insert(metaDataKey, QString::fromUtf8(request.rawHeader(requestKey))); request.setRawHeader(requestKey, QByteArray()); } } void AccessManager::AccessManagerPrivate::setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData) { // Add any meta data specified within request... QVariant userMetaData = request.attribute(static_cast(MetaData)); if (userMetaData.isValid() && userMetaData.type() == QVariant::Map) { metaData += userMetaData.toMap(); } metaData.insert(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true")); moveMetaData(metaData, QStringLiteral("UserAgent"), request, QByteArrayLiteral("User-Agent")); moveMetaData(metaData, QStringLiteral("accept"), request, QByteArrayLiteral("Accept")); moveMetaData(metaData, QStringLiteral("Charsets"), request, QByteArrayLiteral("Accept-Charset")); moveMetaData(metaData, QStringLiteral("Languages"), request, QByteArrayLiteral("Accept-Language")); moveMetaData(metaData, QStringLiteral("referrer"), request, QByteArrayLiteral("Referer")); //Don't try to correct spelling! moveMetaData(metaData, QStringLiteral("content-type"), request, QByteArrayLiteral("Content-Type")); if (request.attribute(QNetworkRequest::AuthenticationReuseAttribute) == QNetworkRequest::Manual) { metaData.insert(QStringLiteral("no-preemptive-auth-reuse"), QStringLiteral("true")); } request.setRawHeader("Content-Length", QByteArray()); request.setRawHeader("Connection", QByteArray()); request.setRawHeader("If-None-Match", QByteArray()); request.setRawHeader("If-Modified-Since", QByteArray()); request.setRawHeader("x-kdewebkit-ignore-disposition", QByteArray()); QStringList customHeaders; Q_FOREACH (const QByteArray &key, request.rawHeaderList()) { const QByteArray value = request.rawHeader(key); if (value.length()) { customHeaders << (QString::fromUtf8(key) + QLatin1String(": ") + QString::fromUtf8(value)); } } if (!customHeaders.isEmpty()) { metaData.insert(QStringLiteral("customHTTPHeader"), customHeaders.join(QStringLiteral("\r\n"))); } // Append per request meta data, if any... if (!requestMetaData.isEmpty()) { metaData += requestMetaData; // Clear per request meta data... requestMetaData.clear(); } // Append per session meta data, if any... if (!sessionMetaData.isEmpty()) { metaData += sessionMetaData; } } using namespace KIO::Integration; static QSsl::SslProtocol qSslProtocolFromString(const QString &str) { if (str.compare(QStringLiteral("SSLv3"), Qt::CaseInsensitive) == 0) { return QSsl::SslV3; } if (str.compare(QStringLiteral("SSLv2"), Qt::CaseInsensitive) == 0) { return QSsl::SslV2; } if (str.compare(QStringLiteral("TLSv1"), Qt::CaseInsensitive) == 0) { return QSsl::TlsV1_0; } return QSsl::AnyProtocol; } bool KIO::Integration::sslConfigFromMetaData(const KIO::MetaData &metadata, QSslConfiguration &sslconfig) { bool success = false; if (metadata.value(QStringLiteral("ssl_in_use")) == QStringLiteral("TRUE")) { const QSsl::SslProtocol sslProto = qSslProtocolFromString(metadata.value(QStringLiteral("ssl_protocol_version"))); QList cipherList; cipherList << QSslCipher(metadata.value(QStringLiteral("ssl_cipher_name")), sslProto); sslconfig.setCaCertificates(QSslCertificate::fromData(metadata.value(QStringLiteral("ssl_peer_chain")).toUtf8())); sslconfig.setCiphers(cipherList); sslconfig.setProtocol(sslProto); success = sslconfig.isNull(); } return success; } CookieJar::CookieJar(QObject *parent) : QNetworkCookieJar(parent), d(new CookieJar::CookieJarPrivate) { reparseConfiguration(); } CookieJar::~CookieJar() { delete d; } WId CookieJar::windowId() const { return d->windowId; } bool CookieJar::isCookieStorageDisabled() const { return d->isStorageDisabled; } QList CookieJar::cookiesForUrl(const QUrl &url) const { QList cookieList; if (!d->isEnabled) { return cookieList; } QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); QDBusReply reply = kcookiejar.call(QStringLiteral("findDOMCookies"), url.toString(QUrl::RemoveUserInfo), (qlonglong)d->windowId); if (!reply.isValid()) { qCWarning(KIO_WIDGETS) << "Unable to communicate with the cookiejar!"; return cookieList; } const QString cookieStr = reply.value(); const QStringList cookies = cookieStr.split(QStringLiteral("; "), QString::SkipEmptyParts); Q_FOREACH (const QString &cookie, cookies) { const int index = cookie.indexOf(QL1C('=')); - const QString name = cookie.left(index); + const QStringRef name = cookie.leftRef(index); const QString value = cookie.right((cookie.length() - index - 1)); cookieList << QNetworkCookie(name.toUtf8(), value.toUtf8()); //qDebug() << "cookie: name=" << name << ", value=" << value; } return cookieList; } bool CookieJar::setCookiesFromUrl(const QList &cookieList, const QUrl &url) { if (!d->isEnabled) { return false; } QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); Q_FOREACH (const QNetworkCookie &cookie, cookieList) { QByteArray cookieHeader("Set-Cookie: "); if (d->isStorageDisabled && !cookie.isSessionCookie()) { QNetworkCookie sessionCookie(cookie); sessionCookie.setExpirationDate(QDateTime()); cookieHeader += sessionCookie.toRawForm(); } else { cookieHeader += cookie.toRawForm(); } kcookiejar.call(QStringLiteral("addCookies"), url.toString(QUrl::RemoveUserInfo), cookieHeader, (qlonglong)d->windowId); //qDebug() << "[" << d->windowId << "]" << cookieHeader << " from " << url; } return !kcookiejar.lastError().isValid(); } void CookieJar::setDisableCookieStorage(bool disable) { d->isStorageDisabled = disable; } void CookieJar::setWindowId(WId id) { d->windowId = id; } void CookieJar::reparseConfiguration() { KConfigGroup cfg = KSharedConfig::openConfig(QStringLiteral("kcookiejarrc"), KConfig::NoGlobals)->group("Cookie Policy"); d->isEnabled = cfg.readEntry("Cookies", true); } diff --git a/src/widgets/accessmanagerreply_p.cpp b/src/widgets/accessmanagerreply_p.cpp index 62295843..14005de4 100644 --- a/src/widgets/accessmanagerreply_p.cpp +++ b/src/widgets/accessmanagerreply_p.cpp @@ -1,524 +1,524 @@ /* * This file is part of the KDE project. * * Copyright (C) 2008 Alex Merry * Copyright (C) 2008 - 2009 Urs Wolfer * Copyright (C) 2009 - 2012 Dawit Alemayehu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "accessmanagerreply_p.h" #include "accessmanager.h" #include "job.h" #include "scheduler.h" #include "kio_widgets_debug.h" #include #include #include #include #include #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) namespace KDEPrivate { AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, KIO::SimpleJob *kioJob, bool emitReadyReadOnMetaDataChange, QObject *parent) : QNetworkReply(parent), m_offset(0), m_metaDataRead(false), m_ignoreContentDisposition(false), m_emitReadyReadOnMetaDataChange(emitReadyReadOnMetaDataChange), m_kioJob(kioJob) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl(request.url()); setOperation(op); setError(NoError, QString()); if (!request.sslConfiguration().isNull()) { setSslConfiguration(request.sslConfiguration()); } connect(kioJob, SIGNAL(redirection(KIO::Job*,QUrl)), SLOT(slotRedirection(KIO::Job*,QUrl))); connect(kioJob, QOverload::of(&KJob::percent), this, &AccessManagerReply::slotPercent); if (qobject_cast(kioJob)) { connect(kioJob, &KJob::result, this, &AccessManagerReply::slotStatResult); } else { connect(kioJob, &KJob::result, this, &AccessManagerReply::slotResult); connect(kioJob, SIGNAL(data(KIO::Job*,QByteArray)), SLOT(slotData(KIO::Job*,QByteArray))); connect(kioJob, SIGNAL(mimetype(KIO::Job*,QString)), SLOT(slotMimeType(KIO::Job*,QString))); } } AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &data, const QUrl &url, const KIO::MetaData &metaData, QObject *parent) : QNetworkReply(parent), m_data(data), m_offset(0), m_ignoreContentDisposition(false), m_emitReadyReadOnMetaDataChange(false) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl((url.isValid() ? url : request.url())); setOperation(op); setHeaderFromMetaData(metaData); if (!request.sslConfiguration().isNull()) { setSslConfiguration(request.sslConfiguration()); } setError(NoError, QString()); emitFinished(true, Qt::QueuedConnection); } AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, QNetworkReply::NetworkError errorCode, const QString &errorMessage, QObject *parent) : QNetworkReply(parent), m_offset(0) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl(request.url()); setOperation(op); setError(static_cast(errorCode), errorMessage); if (error() != QNetworkReply::NoError) { QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, Q_ARG(QNetworkReply::NetworkError, error())); } emitFinished(true, Qt::QueuedConnection); } AccessManagerReply::~AccessManagerReply() { } void AccessManagerReply::abort() { if (m_kioJob) { m_kioJob.data()->disconnect(this); } m_kioJob.clear(); m_data.clear(); m_offset = 0; m_metaDataRead = false; } qint64 AccessManagerReply::bytesAvailable() const { return (QNetworkReply::bytesAvailable() + m_data.length() - m_offset); } qint64 AccessManagerReply::readData(char *data, qint64 maxSize) { const qint64 length = qMin(qint64(m_data.length() - m_offset), maxSize); if (length <= 0) { return 0; } memcpy(data, m_data.constData() + m_offset, length); m_offset += length; if (m_data.length() == m_offset) { m_data.clear(); m_offset = 0; } return length; } bool AccessManagerReply::ignoreContentDisposition(const KIO::MetaData &metaData) { if (m_ignoreContentDisposition) { return true; } if (!metaData.contains(QStringLiteral("content-disposition-type"))) { return true; } bool ok = false; const int statusCode = attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok); if (!ok || statusCode < 200 || statusCode > 299) { return true; } return false; } void AccessManagerReply::setHeaderFromMetaData(const KIO::MetaData &_metaData) { if (_metaData.isEmpty()) { return; } KIO::MetaData metaData(_metaData); // Set the encryption attribute and values... QSslConfiguration sslConfig; const bool isEncrypted = KIO::Integration::sslConfigFromMetaData(metaData, sslConfig); setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, isEncrypted); if (isEncrypted) { setSslConfiguration(sslConfig); } // Set the raw header information... const QStringList httpHeaders(metaData.value(QStringLiteral("HTTP-Headers")).split(QL1C('\n'), QString::SkipEmptyParts)); if (httpHeaders.isEmpty()) { if (metaData.contains(QStringLiteral("charset"))) { QString mimeType = header(QNetworkRequest::ContentTypeHeader).toString(); mimeType += QLatin1String(" ; charset=") + metaData.value(QStringLiteral("charset")); //qDebug() << "changed content-type to" << mimeType; setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); } } else { Q_FOREACH (const QString &httpHeader, httpHeaders) { int index = httpHeader.indexOf(QL1C(':')); // Handle HTTP status line... if (index == -1) { // Except for the status line, all HTTP header must be an nvpair of // type ":" if (!httpHeader.startsWith(QLatin1String("HTTP/"), Qt::CaseInsensitive)) { continue; } QStringList statusLineAttrs(httpHeader.split(QL1C(' '), QString::SkipEmptyParts)); if (statusLineAttrs.count() > 1) { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusLineAttrs.at(1)); } if (statusLineAttrs.count() > 2) { setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, statusLineAttrs.at(2)); } continue; } - const QString headerName = httpHeader.left(index); + const QStringRef headerName = httpHeader.leftRef(index); QString headerValue = httpHeader.mid(index + 1); // Ignore cookie header since it is handled by the http ioslave. if (headerName.startsWith(QLatin1String("set-cookie"), Qt::CaseInsensitive)) { continue; } if (headerName.startsWith(QLatin1String("content-disposition"), Qt::CaseInsensitive) && ignoreContentDisposition(metaData)) { continue; } // Without overriding the corrected mime-type sent by kio_http, add // back the "charset=" portion of the content-type header if present. if (headerName.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) { QString mimeType(header(QNetworkRequest::ContentTypeHeader).toString()); if (m_ignoreContentDisposition) { // If the server returned application/octet-stream, try to determine the // real content type from the disposition filename. if (mimeType == QStringLiteral("application/octet-stream")) { const QString fileName(metaData.value(QStringLiteral("content-disposition-filename"))); QMimeDatabase db; QMimeType mime = db.mimeTypeForFile((fileName.isEmpty() ? url().path() : fileName), QMimeDatabase::MatchExtension); mimeType = mime.name(); } metaData.remove(QStringLiteral("content-disposition-type")); metaData.remove(QStringLiteral("content-disposition-filename")); } if (!headerValue.contains(mimeType, Qt::CaseInsensitive)) { index = headerValue.indexOf(QL1C(';')); if (index == -1) { headerValue = mimeType; } else { headerValue.replace(0, index, mimeType); } //qDebug() << "Changed mime-type from" << mimeType << "to" << headerValue; } } setRawHeader(headerName.trimmed().toUtf8(), headerValue.trimmed().toUtf8()); } } // Set the returned meta data as attribute... setAttribute(static_cast(KIO::AccessManager::MetaData), metaData.toVariant()); } void AccessManagerReply::setIgnoreContentDisposition(bool on) { //qDebug() << on; m_ignoreContentDisposition = on; } void AccessManagerReply::putOnHold() { if (!m_kioJob || isFinished()) { return; } //qDebug() << m_kioJob << m_data; m_kioJob.data()->disconnect(this); m_kioJob.data()->putOnHold(); m_kioJob.clear(); KIO::Scheduler::publishSlaveOnHold(); } bool AccessManagerReply::isLocalRequest(const QUrl &url) { const QString scheme(url.scheme()); return (KProtocolInfo::isKnownProtocol(scheme) && KProtocolInfo::protocolClass(scheme).compare(QStringLiteral(":local"), Qt::CaseInsensitive) == 0); } void AccessManagerReply::readHttpResponseHeaders(KIO::Job *job) { if (!job || m_metaDataRead) { return; } KIO::MetaData metaData(job->metaData()); if (metaData.isEmpty()) { // Allow handling of local resources such as man pages and file url... if (isLocalRequest(url())) { setHeader(QNetworkRequest::ContentLengthHeader, job->totalAmount(KJob::Bytes)); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QStringLiteral("200")); emit metaDataChanged(); } return; } setHeaderFromMetaData(metaData); m_metaDataRead = true; emit metaDataChanged(); } int AccessManagerReply::jobError(KJob *kJob) { const int errCode = kJob->error(); switch (errCode) { case 0: break; // No error; case KIO::ERR_SLAVE_DEFINED: case KIO::ERR_NO_CONTENT: // Sent by a 204 response is not an error condition. setError(QNetworkReply::NoError, kJob->errorText()); //qDebug() << "0 -> QNetworkReply::NoError"; break; case KIO::ERR_IS_DIRECTORY: // This error condition can happen if you click on an ftp link that points // to a directory instead of a file, e.g. ftp://ftp.kde.org/pub setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("inode/directory")); setError(QNetworkReply::NoError, kJob->errorText()); break; case KIO::ERR_CANNOT_CONNECT: setError(QNetworkReply::ConnectionRefusedError, kJob->errorText()); //qDebug() << "KIO::ERR_CANNOT_CONNECT -> QNetworkReply::ConnectionRefusedError"; break; case KIO::ERR_UNKNOWN_HOST: setError(QNetworkReply::HostNotFoundError, kJob->errorText()); //qDebug() << "KIO::ERR_UNKNOWN_HOST -> QNetworkReply::HostNotFoundError"; break; case KIO::ERR_SERVER_TIMEOUT: setError(QNetworkReply::TimeoutError, kJob->errorText()); //qDebug() << "KIO::ERR_SERVER_TIMEOUT -> QNetworkReply::TimeoutError"; break; case KIO::ERR_USER_CANCELED: case KIO::ERR_ABORTED: setError(QNetworkReply::OperationCanceledError, kJob->errorText()); //qDebug() << "KIO::ERR_ABORTED -> QNetworkReply::OperationCanceledError"; break; case KIO::ERR_UNKNOWN_PROXY_HOST: setError(QNetworkReply::ProxyNotFoundError, kJob->errorText()); //qDebug() << "KIO::UNKNOWN_PROXY_HOST -> QNetworkReply::ProxyNotFoundError"; break; case KIO::ERR_ACCESS_DENIED: setError(QNetworkReply::ContentAccessDenied, kJob->errorText()); //qDebug() << "KIO::ERR_ACCESS_DENIED -> QNetworkReply::ContentAccessDenied"; break; case KIO::ERR_WRITE_ACCESS_DENIED: setError(QNetworkReply::ContentOperationNotPermittedError, kJob->errorText()); //qDebug() << "KIO::ERR_WRITE_ACCESS_DENIED -> QNetworkReply::ContentOperationNotPermittedError"; break; case KIO::ERR_DOES_NOT_EXIST: setError(QNetworkReply::ContentNotFoundError, kJob->errorText()); //qDebug() << "KIO::ERR_DOES_NOT_EXIST -> QNetworkReply::ContentNotFoundError"; break; case KIO::ERR_CANNOT_AUTHENTICATE: setError(QNetworkReply::AuthenticationRequiredError, kJob->errorText()); //qDebug() << "KIO::ERR_CANNOT_AUTHENTICATE -> QNetworkReply::AuthenticationRequiredError"; break; case KIO::ERR_UNSUPPORTED_PROTOCOL: case KIO::ERR_NO_SOURCE_PROTOCOL: setError(QNetworkReply::ProtocolUnknownError, kJob->errorText()); //qDebug() << "KIO::ERR_UNSUPPORTED_PROTOCOL -> QNetworkReply::ProtocolUnknownError"; break; case KIO::ERR_CONNECTION_BROKEN: setError(QNetworkReply::RemoteHostClosedError, kJob->errorText()); //qDebug() << "KIO::ERR_CONNECTION_BROKEN -> QNetworkReply::RemoteHostClosedError"; break; case KIO::ERR_UNSUPPORTED_ACTION: setError(QNetworkReply::ProtocolInvalidOperationError, kJob->errorText()); //qDebug() << "KIO::ERR_UNSUPPORTED_ACTION -> QNetworkReply::ProtocolInvalidOperationError"; break; default: setError(QNetworkReply::UnknownNetworkError, kJob->errorText()); //qDebug() << KIO::rawErrorDetail(errCode, QString()) << "-> QNetworkReply::UnknownNetworkError"; } return errCode; } void AccessManagerReply::slotData(KIO::Job *kioJob, const QByteArray &data) { Q_UNUSED(kioJob); if (data.isEmpty()) { return; } qint64 newSizeWithOffset = m_data.size() + data.size(); if (newSizeWithOffset <= m_data.capacity()) { // Already enough space } else if (newSizeWithOffset - m_offset <= m_data.capacity()) { // We get enough space with ::remove. m_data.remove(0, m_offset); m_offset = 0; } else { // We have to resize the array, which implies an expensive memmove. // Do it ourselves to save m_offset bytes. QByteArray newData; // Leave some free space to avoid that every slotData call results in // a reallocation. qNextPowerOfTwo is what QByteArray does internally. newData.reserve(qNextPowerOfTwo(newSizeWithOffset - m_offset)); newData.append(m_data.constData() + m_offset, m_data.size() - m_offset); m_data = newData; m_offset = 0; } m_data += data; emit readyRead(); } void AccessManagerReply::slotMimeType(KIO::Job *kioJob, const QString &mimeType) { //qDebug() << kioJob << mimeType; setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); readHttpResponseHeaders(kioJob); if (m_emitReadyReadOnMetaDataChange) { emit readyRead(); } } void AccessManagerReply::slotResult(KJob *kJob) { const int errcode = jobError(kJob); const QUrl redirectUrl = attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (!redirectUrl.isValid()) { setAttribute(static_cast(KIO::AccessManager::KioError), errcode); if (errcode && errcode != KIO::ERR_NO_CONTENT) { emit error(error()); } } // Make sure HTTP response headers are always set. if (!m_metaDataRead) { readHttpResponseHeaders(qobject_cast(kJob)); } emitFinished(true); } void AccessManagerReply::slotStatResult(KJob *kJob) { if (jobError(kJob)) { emit error(error()); emitFinished(true); return; } KIO::StatJob *statJob = qobject_cast(kJob); Q_ASSERT(statJob); KIO::UDSEntry entry = statJob->statResult(); QString mimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (mimeType.isEmpty() && entry.isDir()) { mimeType = QStringLiteral("inode/directory"); } if (!mimeType.isEmpty()) { setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); } emitFinished(true); } void AccessManagerReply::slotRedirection(KIO::Job *job, const QUrl &u) { if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), url(), u)) { qCWarning(KIO_WIDGETS) << "Redirection from" << url() << "to" << u << "REJECTED by policy!"; setError(QNetworkReply::ContentAccessDenied, u.toString()); emit error(error()); return; } setAttribute(QNetworkRequest::RedirectionTargetAttribute, QUrl(u)); if (job->queryMetaData(QStringLiteral("redirect-to-get")) == QL1S("true")) { setOperation(QNetworkAccessManager::GetOperation); } } void AccessManagerReply::slotPercent(KJob *job, unsigned long percent) { qulonglong bytesTotal = job->totalAmount(KJob::Bytes); qulonglong bytesProcessed = (bytesTotal * percent) / 100; if (operation() == QNetworkAccessManager::PutOperation || operation() == QNetworkAccessManager::PostOperation) { emit uploadProgress(bytesProcessed, bytesTotal); return; } emit downloadProgress(bytesProcessed, bytesTotal); } void AccessManagerReply::emitFinished(bool state, Qt::ConnectionType type) { setFinished(state); emit QMetaObject::invokeMethod(this, "finished", type); } } diff --git a/src/widgets/kurlcompletion.cpp b/src/widgets/kurlcompletion.cpp index b8b06bc3..4b030c59 100644 --- a/src/widgets/kurlcompletion.cpp +++ b/src/widgets/kurlcompletion.cpp @@ -1,1562 +1,1562 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Smith Copyright (C) 2004 Scott Wheeler This class was inspired by a previous KUrlCompletion by Henner Zeller This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kurlcompletion.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // QT_LSTAT, QT_STAT, QT_STATBUF #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #include #endif static bool expandTilde(QString &); static bool expandEnv(QString &); static QString unescape(const QString &text); // Permission mask for files that are executable by // user, group or other #define MODE_EXE (S_IXUSR | S_IXGRP | S_IXOTH) // Constants for types of completion enum ComplType {CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo}; class CompletionThread; // Ensure that we don't end up with "//". static QUrl addPathToUrl(const QUrl &url, const QString &relPath) { QUrl u(url); u.setPath(concatPaths(url.path(), relPath)); return u; } static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); static int initialWaitDuration() { if (s_waitDuration.load() == -1) { const QByteArray envVar = qgetenv("KURLCOMPLETION_WAIT"); if (envVar.isEmpty()) { s_waitDuration = 200; // default: 200 ms } else { s_waitDuration = envVar.toInt(); } } return s_waitDuration; } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletionPrivate // class KUrlCompletionPrivate { public: explicit KUrlCompletionPrivate(KUrlCompletion *parent) : q(parent), url_auto_completion(true), userListThread(nullptr), dirListThread(nullptr) { } ~KUrlCompletionPrivate(); void _k_slotEntries(KIO::Job *, const KIO::UDSEntryList &); void _k_slotIOFinished(KJob *); void slotCompletionThreadDone(QThread *thread, const QStringList &matches); class MyURL; bool userCompletion(const MyURL &url, QString *match); bool envCompletion(const MyURL &url, QString *match); bool exeCompletion(const MyURL &url, QString *match); bool fileCompletion(const MyURL &url, QString *match); bool urlCompletion(const MyURL &url, QString *match); bool isAutoCompletion(); // List the next dir in m_dirs QString listDirectories(const QStringList &, const QString &, bool only_exe = false, bool only_dir = false, bool no_hidden = false, bool stat_files = true); void listUrls(const QList &urls, const QString &filter = QString(), bool only_exe = false, bool no_hidden = false); void addMatches(const QStringList &); QString finished(); void init(); void setListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); bool isListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); KUrlCompletion * const q; QList list_urls; bool onlyLocalProto; // urlCompletion() in Auto/Popup mode? bool url_auto_completion; // Append '/' to directories in Popup mode? // Doing that stat's all files and is slower bool popup_append_slash; // Keep track of currently listed files to avoid reading them again bool last_no_hidden; QString last_path_listed; QString last_file_listed; QString last_prepend; ComplType last_compl_type; QUrl cwd; // "current directory" = base dir for completion KUrlCompletion::Mode mode; // ExeCompletion, FileCompletion, DirCompletion bool replace_env; bool replace_home; bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path KIO::ListJob *list_job; // kio job to list directories QString prepend; // text to prepend to listed items QString compl_text; // text to pass on to KCompletion // Filters for files read with kio bool list_urls_only_exe; // true = only list executables bool list_urls_no_hidden; QString list_urls_filter; // filter for listed files CompletionThread *userListThread; CompletionThread *dirListThread; QStringList mimeTypeFilters; }; class CompletionThread : public QThread { Q_OBJECT protected: CompletionThread(KUrlCompletionPrivate *receiver) : QThread(), m_prepend(receiver->prepend), m_complete_url(receiver->complete_url), m_terminationRequested(false) {} public: void requestTermination() { if (!isFinished()) { qCDebug(KIO_WIDGETS) << "stopping thread" << this; } m_terminationRequested.store(true); wait(); } QStringList matches() const { QMutexLocker locker(&m_mutex); return m_matches; } Q_SIGNALS: void completionThreadDone(QThread *thread, const QStringList &matches); protected: void addMatch(const QString &match) { QMutexLocker locker(&m_mutex); m_matches.append(match); } bool terminationRequested() const { return m_terminationRequested.load(); } void done() { if (!terminationRequested()) { qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches"; emit completionThreadDone(this, m_matches); } } const QString m_prepend; const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path private: mutable QMutex m_mutex; // protects m_matches QStringList m_matches; // written by secondary thread, read by the matches() method QAtomicInt m_terminationRequested; // used as a bool }; /** * A simple thread that fetches a list of tilde-completions and returns this * to the caller via the completionThreadDone signal. */ class UserListThread : public CompletionThread { Q_OBJECT public: UserListThread(KUrlCompletionPrivate *receiver) : CompletionThread(receiver) {} protected: void run() override { #ifndef Q_OS_ANDROID const QChar tilde = QLatin1Char('~'); // we don't need to handle prepend here, right? ~user is always at pos 0 assert(m_prepend.isEmpty()); #pragma message("TODO: add KUser::allUserNames() with a std::function shouldTerminate parameter") #ifndef Q_OS_WIN struct passwd *pw; ::setpwent(); while ((pw = ::getpwent()) && !terminationRequested()) { addMatch(tilde + QString::fromLocal8Bit(pw->pw_name)); } ::endpwent(); #else //currently terminationRequested is ignored on Windows QStringList allUsers = KUser::allUserNames(); Q_FOREACH(const QString& s, allUsers) { addMatch(tilde + s); } #endif addMatch(QString(tilde)); #endif done(); } }; class DirectoryListThread : public CompletionThread { Q_OBJECT public: DirectoryListThread(KUrlCompletionPrivate *receiver, const QStringList &dirList, const QString &filter, const QStringList &mimeTypeFilters, bool onlyExe, bool onlyDir, bool noHidden, bool appendSlashToDir) : CompletionThread(receiver), m_dirList(dirList), m_filter(filter), m_mimeTypeFilters(mimeTypeFilters), m_onlyExe(onlyExe), m_onlyDir(onlyDir), m_noHidden(noHidden), m_appendSlashToDir(appendSlashToDir) {} void run() override; private: QStringList m_dirList; QString m_filter; QStringList m_mimeTypeFilters; bool m_onlyExe; bool m_onlyDir; bool m_noHidden; bool m_appendSlashToDir; }; void DirectoryListThread::run() { //qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size(); QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot; if (m_onlyExe) { iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable); } else if (m_onlyDir) { iterator_filter |= QDir::Dirs; } else { iterator_filter |= (QDir::Dirs | QDir::Files); } QMimeDatabase mimeTypes; const QStringList::const_iterator end = m_dirList.constEnd(); for (QStringList::const_iterator it = m_dirList.constBegin(); it != end && !terminationRequested(); ++it) { //qDebug() << "Scanning directory" << *it; QDirIterator current_dir_iterator(*it, iterator_filter); while (current_dir_iterator.hasNext() && !terminationRequested()) { current_dir_iterator.next(); QFileInfo file_info = current_dir_iterator.fileInfo(); const QString file_name = file_info.fileName(); //qDebug() << "Found" << file_name; if (!m_filter.isEmpty() && !file_name.startsWith(m_filter)) { continue; } if (!m_mimeTypeFilters.isEmpty() && !file_info.isDir()) { auto mimeType = mimeTypes.mimeTypeForFile(file_info); if (!m_mimeTypeFilters.contains(mimeType.name())) { continue; } } QString toAppend = file_name; // Add '/' to directories if (m_appendSlashToDir && file_info.isDir()) { toAppend.append(QLatin1Char('/')); } if (m_complete_url) { QUrl info(m_prepend); info = addPathToUrl(info, toAppend); addMatch(info.toDisplayString()); } else { addMatch(m_prepend + toAppend); } } } done(); } KUrlCompletionPrivate::~KUrlCompletionPrivate() { } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // MyURL - wrapper for QUrl with some different functionality // class KUrlCompletionPrivate::MyURL { public: MyURL(const QString &url, const QUrl &cwd); MyURL(const MyURL &url); ~MyURL(); QUrl kurl() const { return m_kurl; } bool isLocalFile() const { return m_kurl.isLocalFile(); } QString scheme() const { return m_kurl.scheme(); } // The directory with a trailing '/' QString dir() const { return m_kurl.adjusted(QUrl::RemoveFilename).path(); } QString file() const { return m_kurl.fileName(); } // The initial, unparsed, url, as a string. QString url() const { return m_url; } // Is the initial string a URL, or just a path (whether absolute or relative) bool isURL() const { return m_isURL; } void filter(bool replace_user_dir, bool replace_env); private: void init(const QString &url, const QUrl &cwd); QUrl m_kurl; QString m_url; bool m_isURL; }; KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) { init(_url, cwd); } KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url) : m_kurl(_url.m_kurl) { m_url = _url.m_url; m_isURL = _url.m_isURL; } void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd) { // Save the original text m_url = _url; // Non-const copy QString url_copy = _url; // Special shortcuts for "man:" and "info:" if (url_copy.startsWith(QLatin1Char('#'))) { if (url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#')) { url_copy.replace(0, 2, QStringLiteral("info:")); } else { url_copy.replace(0, 1, QStringLiteral("man:")); } } // Look for a protocol in 'url' QRegExp protocol_regex = QRegExp(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:")); // Assume "file:" or whatever is given by 'cwd' if there is // no protocol. (QUrl does this only for absolute paths) if (protocol_regex.indexIn(url_copy) == 0) { m_kurl = QUrl(url_copy); m_isURL = true; } else { // relative path or ~ or $something m_isURL = false; if (!QDir::isRelativePath(url_copy) || url_copy.startsWith(QLatin1Char('~')) || url_copy.startsWith(QLatin1Char('$'))) { m_kurl = QUrl::fromLocalFile(url_copy); } else { // Relative path if (cwd.isEmpty()) { m_kurl = QUrl(url_copy); } else { m_kurl = cwd; m_kurl.setPath(concatPaths(m_kurl.path(), url_copy)); } } } } KUrlCompletionPrivate::MyURL::~MyURL() { } void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env) { QString d = dir() + file(); if (replace_user_dir) { expandTilde(d); } if (replace_env) { expandEnv(d); } m_kurl.setPath(d); } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletion // KUrlCompletion::KUrlCompletion() : KCompletion(), d(new KUrlCompletionPrivate(this)) { d->init(); } KUrlCompletion::KUrlCompletion(Mode _mode) : KCompletion(), d(new KUrlCompletionPrivate(this)) { d->init(); setMode(_mode); } KUrlCompletion::~KUrlCompletion() { stop(); delete d; } void KUrlCompletionPrivate::init() { cwd = QUrl::fromLocalFile(QDir::homePath()); replace_home = true; replace_env = true; last_no_hidden = false; last_compl_type = CTNone; list_job = nullptr; mode = KUrlCompletion::FileCompletion; // Read settings KConfigGroup cg(KSharedConfig::openConfig(), "URLCompletion"); url_auto_completion = cg.readEntry("alwaysAutoComplete", true); popup_append_slash = cg.readEntry("popupAppendSlash", true); onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false); q->setIgnoreCase(true); } void KUrlCompletion::setDir(const QUrl &dir) { d->cwd = dir; } QUrl KUrlCompletion::dir() const { return d->cwd; } KUrlCompletion::Mode KUrlCompletion::mode() const { return d->mode; } void KUrlCompletion::setMode(Mode _mode) { d->mode = _mode; } bool KUrlCompletion::replaceEnv() const { return d->replace_env; } void KUrlCompletion::setReplaceEnv(bool replace) { d->replace_env = replace; } bool KUrlCompletion::replaceHome() const { return d->replace_home; } void KUrlCompletion::setReplaceHome(bool replace) { d->replace_home = replace; } /* * makeCompletion() * * Entry point for file name completion */ QString KUrlCompletion::makeCompletion(const QString &text) { qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd; KUrlCompletionPrivate::MyURL url(text, d->cwd); d->compl_text = text; // Set d->prepend to the original URL, with the filename [and ref/query] stripped. // This is what gets prepended to the directory-listing matches. if (url.isURL()) { QUrl directoryUrl(url.kurl()); directoryUrl.setQuery(QString()); directoryUrl.setFragment(QString()); directoryUrl.setPath(url.dir()); d->prepend = directoryUrl.toString(); } else { d->prepend = text.left(text.length() - url.file().length()); } d->complete_url = url.isURL(); QString aMatch; // Environment variables // if (d->replace_env && d->envCompletion(url, &aMatch)) { return aMatch; } // User directories // if (d->replace_home && d->userCompletion(url, &aMatch)) { return aMatch; } // Replace user directories and variables url.filter(d->replace_home, d->replace_env); //qDebug() << "Filtered: proto=" << url.scheme() // << ", dir=" << url.dir() // << ", file=" << url.file() // << ", kurl url=" << *url.kurl(); if (d->mode == ExeCompletion) { // Executables // if (d->exeCompletion(url, &aMatch)) { return aMatch; } // KRun can run "man:" and "info:" etc. so why not treat them // as executables... if (d->urlCompletion(url, &aMatch)) { return aMatch; } } else { // Local files, directories // if (d->fileCompletion(url, &aMatch)) { return aMatch; } // All other... // if (d->urlCompletion(url, &aMatch)) { return aMatch; } } d->setListedUrl(CTNone); stop(); return QString(); } /* * finished * * Go on and call KCompletion. * Called when all matches have been added */ QString KUrlCompletionPrivate::finished() { if (last_compl_type == CTInfo) { return q->KCompletion::makeCompletion(compl_text.toLower()); } else { return q->KCompletion::makeCompletion(compl_text); } } /* * isRunning * * Return true if either a KIO job or a thread is running */ bool KUrlCompletion::isRunning() const { return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished()); } /* * stop * * Stop and delete a running KIO job or the DirLister */ void KUrlCompletion::stop() { if (d->list_job) { d->list_job->kill(); d->list_job = nullptr; } if (d->dirListThread) { d->dirListThread->requestTermination(); delete d->dirListThread; d->dirListThread = nullptr; } if (d->userListThread) { d->userListThread->requestTermination(); delete d->userListThread; d->userListThread = nullptr; } } /* * Keep track of the last listed directory */ void KUrlCompletionPrivate::setListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { last_compl_type = complType; last_path_listed = directory; last_file_listed = filter; last_no_hidden = no_hidden; last_prepend = prepend; } bool KUrlCompletionPrivate::isListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { return last_compl_type == complType && (last_path_listed == directory || (directory.isEmpty() && last_path_listed.isEmpty())) && (filter.startsWith(last_file_listed) || (filter.isEmpty() && last_file_listed.isEmpty())) && last_no_hidden == no_hidden && last_prepend == prepend; // e.g. relative path vs absolute } /* * isAutoCompletion * * Returns true if completion mode is Auto or Popup */ bool KUrlCompletionPrivate::isAutoCompletion() { return q->completionMode() == KCompletion::CompletionAuto || q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionMan || q->completionMode() == KCompletion::CompletionPopupAuto; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // User directories // bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.scheme() != QLatin1String("file") || !url.dir().isEmpty() || !url.file().startsWith(QLatin1Char('~')) || !prepend.isEmpty()) { return false; } if (!isListedUrl(CTUser)) { q->stop(); q->clear(); setListedUrl(CTUser); Q_ASSERT(!userListThread); // caller called stop() userListThread = new UserListThread(this); QObject::connect(userListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); }); userListThread->start(); // If the thread finishes quickly make sure that the results // are added to the first matching case. userListThread->wait(initialWaitDuration()); const QStringList l = userListThread->matches(); addMatches(l); } *pMatch = finished(); return true; } ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// // Environment variables // bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.file().isEmpty() || url.file().at(0) != QLatin1Char('$')) { return false; } if (!isListedUrl(CTEnv)) { q->stop(); q->clear(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QStringList keys = env.keys(); QString dollar = QStringLiteral("$"); QStringList l; Q_FOREACH(const QString &key, keys) { l.append(prepend + dollar + key); } addMatches(l); } setListedUrl(CTEnv); *pMatch = finished(); return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Executables // bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); // remove escapes // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. $PATH // 4. no directory at all QStringList dirList; if (!url.file().isEmpty()) { // $PATH dirList = QString::fromLocal8Bit(qgetenv("PATH")).split( QDir::listSeparator(), QString::SkipEmptyParts); QStringList::Iterator it = dirList.begin(); for (; it != dirList.end(); ++it) { it->append(QLatin1Char('/')); } } else if (!QDir::isRelativePath(directory)) { // complete path in url dirList.append(directory); } else if (!directory.isEmpty() && !cwd.isEmpty()) { // current directory dirList.append(cwd.toLocalFile() + QLatin1Char('/') + directory); } // No hidden files unless the user types "." bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.'); // List files if needed // if (!isListedUrl(CTExe, directory, url.file(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTExe, directory, url.file(), no_hidden_files); *pMatch = listDirectories(dirList, url.file(), true, false, no_hidden_files); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Local files // bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); if (url.url() == QLatin1String("..")) { *pMatch = QStringLiteral(".."); return true; } //qDebug() << "fileCompletion" << url << "dir=" << dir; // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. no directory at all QStringList dirList; if (!QDir::isRelativePath(directory)) { // complete path in url dirList.append(directory); } else if (!cwd.isEmpty()) { // current directory QString dirToAdd = cwd.toLocalFile(); if (!directory.isEmpty()) { if (!dirToAdd.endsWith(QLatin1Char('/'))) { dirToAdd.append(QLatin1Char('/')); } dirToAdd.append(directory); } dirList.append(dirToAdd); } // No hidden files unless the user types "." bool no_hidden_files = !url.file().startsWith(QLatin1Char('.')); // List files if needed // if (!isListedUrl(CTFile, directory, QString(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTFile, directory, QString(), no_hidden_files); // Append '/' to directories in Popup mode? bool append_slash = (popup_append_slash && (q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionPopupAuto)); bool only_dir = (mode == KUrlCompletion::DirCompletion); *pMatch = listDirectories(dirList, QString(), false, only_dir, no_hidden_files, append_slash); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // URLs not handled elsewhere... // static bool isLocalProtocol(const QString &protocol) { return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local")); } bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { //qDebug() << *url.kurl(); if (onlyLocalProto && isLocalProtocol(url.scheme())) { return false; } // Use d->cwd as base url in case url is not absolute QUrl url_dir = url.kurl(); if (url_dir.isRelative() && !cwd.isEmpty()) { // Create an URL with the directory to be listed url_dir = cwd.resolved(url_dir); } // url is malformed if (!url_dir.isValid() || url.scheme().isEmpty()) { return false; } // non local urls if (!isLocalProtocol(url.scheme())) { // url does not specify host if (url_dir.host().isEmpty()) { return false; } // url does not specify a valid directory if (url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) { return false; } // automatic completion is disabled if (isAutoCompletion() && !url_auto_completion) { return false; } } // url handler doesn't support listing if (!KProtocolManager::supportsListing(url_dir)) { return false; } // Remove escapes const QString directory = unescape(url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); url_dir.setPath(directory); // List files if needed // if (!isListedUrl(CTUrl, directory, url.file())) { q->stop(); q->clear(); setListedUrl(CTUrl, directory, QString()); QList url_list; url_list.append(url_dir); listUrls(url_list, QString(), false); pMatch->clear(); } else if (!q->isRunning()) { *pMatch = finished(); } else { pMatch->clear(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Directory and URL listing // /* * addMatches * * Called to add matches to KCompletion */ void KUrlCompletionPrivate::addMatches(const QStringList &matchList) { q->insertItems(matchList); } /* * listDirectories * * List files starting with 'filter' in the given directories, * either using DirLister or listURLs() * * In either case, addMatches() is called with the listed * files, and eventually finished() when the listing is done * * Returns the match if available, or QString() if * DirLister timed out or using kio */ QString KUrlCompletionPrivate::listDirectories( const QStringList &dirList, const QString &filter, bool only_exe, bool only_dir, bool no_hidden, bool append_slash_to_dir) { assert(!q->isRunning()); if (qEnvironmentVariableIsEmpty("KURLCOMPLETION_LOCAL_KIO")) { qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread"; // Don't use KIO QStringList dirs; QStringList::ConstIterator end = dirList.constEnd(); for (QStringList::ConstIterator it = dirList.constBegin(); it != end; ++it) { QUrl url = QUrl::fromLocalFile(*it); if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), QUrl(), url)) { dirs.append(*it); } } Q_ASSERT(!dirListThread); // caller called stop() dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters, only_exe, only_dir, no_hidden, append_slash_to_dir); QObject::connect(dirListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); }); dirListThread->start(); dirListThread->wait(initialWaitDuration()); qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches(); addMatches(dirListThread->matches()); return finished(); } // Use KIO //qDebug() << "Listing (listDirectories):" << dirList << "with KIO"; QList url_list; QStringList::ConstIterator it = dirList.constBegin(); QStringList::ConstIterator end = dirList.constEnd(); for (; it != end; ++it) { url_list.append(QUrl(*it)); } listUrls(url_list, filter, only_exe, no_hidden); // Will call addMatches() and finished() return QString(); } /* * listURLs * * Use KIO to list the given urls * * addMatches() is called with the listed files * finished() is called when the listing is done */ void KUrlCompletionPrivate::listUrls( const QList &urls, const QString &filter, bool only_exe, bool no_hidden) { assert(list_urls.isEmpty()); assert(list_job == nullptr); list_urls = urls; list_urls_filter = filter; list_urls_only_exe = only_exe; list_urls_no_hidden = no_hidden; //qDebug() << "Listing URLs:" << *urls[0] << ",..."; // Start it off by calling _k_slotIOFinished // // This will start a new list job as long as there // are urls in d->list_urls // _k_slotIOFinished(nullptr); } /* * _k_slotEntries * * Receive files listed by KIO and call addMatches() */ void KUrlCompletionPrivate::_k_slotEntries(KIO::Job *, const KIO::UDSEntryList &entries) { QStringList matchList; KIO::UDSEntryList::ConstIterator it = entries.constBegin(); const KIO::UDSEntryList::ConstIterator end = entries.constEnd(); QString filter = list_urls_filter; int filter_len = filter.length(); // Iterate over all files // for (; it != end; ++it) { const KIO::UDSEntry &entry = *it; const QString url = entry.stringValue(KIO::UDSEntry::UDS_URL); QString entry_name; if (!url.isEmpty()) { //qDebug() << "url:" << url; entry_name = QUrl(url).fileName(); } else { entry_name = entry.stringValue(KIO::UDSEntry::UDS_NAME); } //qDebug() << "name:" << name; if ((!entry_name.isEmpty() && entry_name.at(0) == QLatin1Char('.')) && (list_urls_no_hidden || entry_name.length() == 1 || (entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.')))) { continue; } const bool isDir = entry.isDir(); if (mode == KUrlCompletion::DirCompletion && !isDir) { continue; } - if (filter_len != 0 && entry_name.left(filter_len) != filter) { + if (filter_len != 0 && entry_name.leftRef(filter_len) != filter) { continue; } if (!mimeTypeFilters.isEmpty() && !isDir && !mimeTypeFilters.contains(entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE))) { continue; } QString toAppend = entry_name; if (isDir) { toAppend.append(QLatin1Char('/')); } if (!list_urls_only_exe || (entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & MODE_EXE) // true if executable ) { if (complete_url) { QUrl url(prepend); url = addPathToUrl(url, toAppend); matchList.append(url.toDisplayString()); } else { matchList.append(prepend + toAppend); } } } addMatches(matchList); } /* * _k_slotIOFinished * * Called when a KIO job is finished. * * Start a new list job if there are still urls in * list_urls, otherwise call finished() */ void KUrlCompletionPrivate::_k_slotIOFinished(KJob *job) { assert(job == list_job); Q_UNUSED(job) if (list_urls.isEmpty()) { list_job = nullptr; finished(); // will call KCompletion::makeCompletion() } else { QUrl kurl(list_urls.takeFirst()); // list_urls.removeAll( kurl ); //qDebug() << "Start KIO::listDir" << kurl; list_job = KIO::listDir(kurl, KIO::HideProgressInfo); list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); assert(list_job); q->connect(list_job, SIGNAL(result(KJob*)), SLOT(_k_slotIOFinished(KJob*))); q->connect(list_job, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), SLOT(_k_slotEntries(KIO::Job*,KIO::UDSEntryList))); } } /////////////////////////////////////////////////// /////////////////////////////////////////////////// /* * postProcessMatch, postProcessMatches * * Called by KCompletion before emitting match() and matches() * * Append '/' to directories for file completion. This is * done here to avoid stat()'ing a lot of files */ void KUrlCompletion::postProcessMatch(QString *pMatch) const { //qDebug() << *pMatch; if (!pMatch->isEmpty() && pMatch->startsWith(QLatin1String("file:"))) { // Add '/' to directories in file completion mode // unless it has already been done if (d->last_compl_type == CTFile && pMatch->at(pMatch->length() - 1) != QLatin1Char('/')) { QString copy = QUrl(*pMatch).toLocalFile(); expandTilde(copy); expandEnv(copy); if (QDir::isRelativePath(copy)) { copy.prepend(d->cwd.toLocalFile() + QLatin1Char('/')); } //qDebug() << "stat'ing" << copy; QByteArray file = QFile::encodeName(copy); QT_STATBUF sbuff; if (QT_STAT(file.constData(), &sbuff) == 0) { if ((sbuff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { pMatch->append(QLatin1Char('/')); } } else { //qDebug() << "Could not stat file" << copy; } } } } void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } // no longer used, KF6 TODO: remove this method void KUrlCompletion::customEvent(QEvent *e) { KCompletion::customEvent(e); } void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches) { if (thread != userListThread && thread != dirListThread) { qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches"; return; } qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread"; q->setItems(matches); if (userListThread == thread) { thread->wait(); delete thread; userListThread = nullptr; } if (dirListThread == thread) { thread->wait(); delete thread; dirListThread = nullptr; } finished(); // will call KCompletion::makeCompletion() } // static QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv) { if (text.isEmpty()) { return text; } KUrlCompletionPrivate::MyURL url(text, QUrl()); // no need to replace something of our current cwd if (!url.kurl().isLocalFile()) { return text; } url.filter(replaceHome, replaceEnv); return url.dir() + url.file(); } QString KUrlCompletion::replacedPath(const QString &text) const { return replacedPath(text, d->replace_home, d->replace_env); } void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) { d->mimeTypeFilters = mimeTypeFilters; } QStringList KUrlCompletion::mimeTypeFilters() const { return d->mimeTypeFilters; } ///////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// // Static functions /* * expandEnv * * Expand environment variables in text. Escaped '$' are ignored. * Return true if expansion was made. */ static bool expandEnv(QString &text) { // Find all environment variables beginning with '$' // int pos = 0; bool expanded = false; while ((pos = text.indexOf(QLatin1Char('$'), pos)) != -1) { // Skip escaped '$' // if (pos > 0 && text.at(pos - 1) == QLatin1Char('\\')) { pos++; } // Variable found => expand // else { // Find the end of the variable = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), pos + 1); int pos_tmp = text.indexOf(QLatin1Char('/'), pos + 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace if the variable is terminated by '/' or ' ' // and defined // if (pos2 >= 0) { int len = pos2 - pos; const QStringRef key = text.midRef(pos + 1, len - 1); QString value = QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData())); if (!value.isEmpty()) { expanded = true; text.replace(pos, len, value); pos = pos + value.length(); } else { pos = pos2; } } } } return expanded; } /* * expandTilde * * Replace "~user" with the users home directory * Return true if expansion was made. */ static bool expandTilde(QString &text) { if (text.isEmpty() || (text.at(0) != QLatin1Char('~'))) { return false; } bool expanded = false; // Find the end of the user name = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), 1); int pos_tmp = text.indexOf(QLatin1Char('/'), 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace ~user if the user name is terminated by '/' or ' ' // if (pos2 >= 0) { QString userName = text.mid(1, pos2 - 1); QString dir; // A single ~ is replaced with $HOME // if (userName.isEmpty()) { dir = QDir::homePath(); } // ~user is replaced with the dir from passwd // else { KUser user(userName); dir = user.homeDir(); } if (!dir.isEmpty()) { expanded = true; text.replace(0, pos2, dir); } } return expanded; } /* * unescape * * Remove escapes and return the result in a new string * */ static QString unescape(const QString &text) { QString result; for (int pos = 0; pos < text.length(); pos++) if (text.at(pos) != QLatin1Char('\\')) { result.insert(result.length(), text.at(pos)); } return result; } #include "moc_kurlcompletion.cpp" #include "kurlcompletion.moc" diff --git a/src/widgets/kurlrequester.cpp b/src/widgets/kurlrequester.cpp index dc1dccba..53fdd671 100644 --- a/src/widgets/kurlrequester.cpp +++ b/src/widgets/kurlrequester.cpp @@ -1,673 +1,673 @@ /* This file is part of the KDE libraries Copyright (C) 1999,2000,2001 Carsten Pfeiffer Copyright (C) 2013 Teo Mrnjavac This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kurlrequester.h" #include "kio_widgets_debug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KUrlDragPushButton : public QPushButton { Q_OBJECT public: explicit KUrlDragPushButton(QWidget *parent) : QPushButton(parent) { new DragDecorator(this); } ~KUrlDragPushButton() {} void setURL(const QUrl &url) { m_urls.clear(); m_urls.append(url); } private: class DragDecorator : public KDragWidgetDecoratorBase { public: explicit DragDecorator(KUrlDragPushButton *button) : KDragWidgetDecoratorBase(button), m_button(button) {} protected: QDrag *dragObject() override { if (m_button->m_urls.isEmpty()) { return nullptr; } QDrag *drag = new QDrag(m_button); QMimeData *mimeData = new QMimeData; mimeData->setUrls(m_button->m_urls); drag->setMimeData(mimeData); return drag; } private: KUrlDragPushButton *m_button; }; QList m_urls; }; class Q_DECL_HIDDEN KUrlRequester::KUrlRequesterPrivate { public: explicit KUrlRequesterPrivate(KUrlRequester *parent) : m_parent(parent), edit(nullptr), combo(nullptr), fileDialogMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly), fileDialogAcceptMode(QFileDialog::AcceptOpen) { } ~KUrlRequesterPrivate() { delete myCompletion; delete myFileDialog; } void init(); void setText(const QString &text) { if (combo) { if (combo->isEditable()) { combo->setEditText(text); } else { int i = combo->findText(text); if (i == -1) { combo->addItem(text); combo->setCurrentIndex(combo->count() - 1); } else { combo->setCurrentIndex(i); } } } else { edit->setText(text); } } void connectSignals(KUrlRequester *receiver) { if (combo) { connect(combo, &QComboBox::currentTextChanged, receiver, &KUrlRequester::textChanged); connect(combo, &QComboBox::editTextChanged, receiver, &KUrlRequester::textEdited); connect(combo, QOverload<>::of(&KComboBox::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); connect(combo, QOverload::of(&KComboBox::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } else if (edit) { connect(edit, &QLineEdit::textChanged, receiver, &KUrlRequester::textChanged); connect(edit, &QLineEdit::textEdited, receiver, &KUrlRequester::textEdited); connect(edit, QOverload<>::of(&QLineEdit::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); if (auto kline = qobject_cast(edit)) { connect(kline, QOverload::of(&KLineEdit::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } } } void setCompletionObject(KCompletion *comp) { if (combo) { combo->setCompletionObject(comp); } else { edit->setCompletionObject(comp); } } void updateCompletionStartDir(const QUrl &newStartDir) { myCompletion->setDir(newStartDir); } QString text() const { return combo ? combo->currentText() : edit->text(); } /** * replaces ~user or $FOO, if necessary * if text() is a relative path, make it absolute using startDir() */ QUrl url() const { const QString txt = text(); KUrlCompletion *comp; if (combo) { comp = qobject_cast(combo->completionObject()); } else { comp = qobject_cast(edit->completionObject()); } QString enteredPath; if (comp) enteredPath = comp->replacedPath(txt); else enteredPath = txt; if (QDir::isAbsolutePath(enteredPath)) { return QUrl::fromLocalFile(enteredPath); } const QUrl enteredUrl = QUrl(enteredPath); // absolute or relative if (enteredUrl.isRelative() && !txt.isEmpty()) { QUrl finalUrl(m_startDir); finalUrl.setPath(concatPaths(finalUrl.path(), enteredPath)); return finalUrl; } else { return enteredUrl; } } static void applyFileMode(QFileDialog *dlg, KFile::Modes m, QFileDialog::AcceptMode acceptMode) { QFileDialog::FileMode fileMode; if (m & KFile::Directory) { fileMode = QFileDialog::Directory; if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) { dlg->setOption(QFileDialog::ShowDirsOnly, true); } } else if (m & KFile::Files && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFiles; } else if (m & KFile::File && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFile; } else { fileMode = QFileDialog::AnyFile; } dlg->setFileMode(fileMode); dlg->setAcceptMode(acceptMode); } // Converts from "*.foo *.bar|Comment" to "Comment (*.foo *.bar)" QStringList kToQFilters(const QString &filters) const { QStringList qFilters = filters.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (QStringList::iterator it = qFilters.begin(); it != qFilters.end(); ++it) { int sep = it->indexOf(QLatin1Char('|')); - QString globs = it->left(sep); - QString desc = it->mid(sep + 1); - *it = QStringLiteral("%1 (%2)").arg(desc, globs); + const QStringRef globs = it->leftRef(sep); + const QStringRef desc = it->midRef(sep + 1); + *it = desc + QLatin1String(" (") + globs + QLatin1Char(')'); } return qFilters; } QUrl getDirFromFileDialog(const QUrl &openUrl) const { return QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly); } // slots void _k_slotUpdateUrl(); void _k_slotOpenDialog(); void _k_slotFileDialogAccepted(); QUrl m_startDir; bool m_startDirCustomized; KUrlRequester * const m_parent; // TODO: rename to 'q' KLineEdit *edit; KComboBox *combo; KFile::Modes fileDialogMode; QFileDialog::AcceptMode fileDialogAcceptMode; QString fileDialogFilter; QStringList mimeTypeFilters; KEditListWidget::CustomEditor editor; KUrlDragPushButton *myButton; QFileDialog *myFileDialog; KUrlCompletion *myCompletion; Qt::WindowModality fileDialogModality; }; KUrlRequester::KUrlRequester(QWidget *editWidget, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { // must have this as parent editWidget->setParent(this); d->combo = qobject_cast(editWidget); d->edit = qobject_cast(editWidget); if (d->edit) { d->edit->setClearButtonEnabled(true); } d->init(); } KUrlRequester::KUrlRequester(QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); } KUrlRequester::KUrlRequester(const QUrl &url, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); setUrl(url); } KUrlRequester::~KUrlRequester() { delete d; } void KUrlRequester::KUrlRequesterPrivate::init() { myFileDialog = nullptr; fileDialogModality = Qt::ApplicationModal; if (!combo && !edit) { edit = new KLineEdit(m_parent); edit->setClearButtonEnabled(true); } QWidget *widget = combo ? static_cast(combo) : static_cast(edit); QHBoxLayout *topLayout = new QHBoxLayout(m_parent); topLayout->setMargin(0); topLayout->setSpacing(-1); // use default spacing topLayout->addWidget(widget); myButton = new KUrlDragPushButton(m_parent); myButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int buttonSize = myButton->sizeHint().expandedTo(widget->sizeHint()).height(); myButton->setFixedSize(buttonSize, buttonSize); myButton->setToolTip(i18n("Open file dialog")); connect(myButton, SIGNAL(pressed()), m_parent, SLOT(_k_slotUpdateUrl())); widget->installEventFilter(m_parent); m_parent->setFocusProxy(widget); m_parent->setFocusPolicy(Qt::StrongFocus); topLayout->addWidget(myButton); connectSignals(m_parent); connect(myButton, SIGNAL(clicked()), m_parent, SLOT(_k_slotOpenDialog())); m_startDir = QUrl::fromLocalFile(QDir::currentPath()); m_startDirCustomized = false; myCompletion = new KUrlCompletion(); updateCompletionStartDir(m_startDir); setCompletionObject(myCompletion); QAction *openAction = new QAction(m_parent); openAction->setShortcut(QKeySequence::Open); m_parent->connect(openAction, SIGNAL(triggered(bool)), SLOT(_k_slotOpenDialog())); } void KUrlRequester::setUrl(const QUrl &url) { d->setText(url.toDisplayString(QUrl::PreferLocalFile)); } #ifndef KIOWIDGETS_NO_DEPRECATED void KUrlRequester::setPath(const QString &path) { d->setText(path); } #endif void KUrlRequester::setText(const QString &text) { d->setText(text); } void KUrlRequester::setStartDir(const QUrl &startDir) { d->m_startDir = startDir; d->m_startDirCustomized = true; d->updateCompletionStartDir(startDir); } void KUrlRequester::changeEvent(QEvent *e) { if (e->type() == QEvent::WindowTitleChange) { if (d->myFileDialog) { d->myFileDialog->setWindowTitle(windowTitle()); } } QWidget::changeEvent(e); } QUrl KUrlRequester::url() const { return d->url(); } QUrl KUrlRequester::startDir() const { return d->m_startDir; } QString KUrlRequester::text() const { return d->text(); } void KUrlRequester::KUrlRequesterPrivate::_k_slotOpenDialog() { if (myFileDialog) if (myFileDialog->isVisible()) { //The file dialog is already being shown, raise it and exit myFileDialog->raise(); myFileDialog->activateWindow(); return; } if (((fileDialogMode & KFile::Directory) && !(fileDialogMode & KFile::File)) || /* catch possible fileDialog()->setMode( KFile::Directory ) changes */ (myFileDialog && (myFileDialog->fileMode() == QFileDialog::Directory && myFileDialog->testOption(QFileDialog::ShowDirsOnly)))) { const QUrl openUrl = (!m_parent->url().isEmpty() && !m_parent->url().isRelative()) ? m_parent->url() : m_startDir; /* FIXME We need a new abstract interface for using KDirSelectDialog in a non-modal way */ QUrl newUrl; if (fileDialogMode & KFile::LocalOnly) { newUrl = QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly, QStringList() << QStringLiteral("file")); } else { newUrl = getDirFromFileDialog(openUrl); } if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); } } else { emit m_parent->openFileDialog(m_parent); //Creates the fileDialog if it doesn't exist yet QFileDialog *dlg = m_parent->fileDialog(); if (!url().isEmpty() && !url().isRelative()) { QUrl u(url()); // If we won't be able to list it (e.g. http), then don't try :) if (KProtocolManager::supportsListing(u)) { dlg->selectUrl(u); } } else { dlg->setDirectoryUrl(m_startDir); } dlg->setAcceptMode(fileDialogAcceptMode); //Update the file dialog window modality if (dlg->windowModality() != fileDialogModality) { dlg->setWindowModality(fileDialogModality); } if (fileDialogModality == Qt::NonModal) { dlg->show(); } else { dlg->exec(); } } } void KUrlRequester::KUrlRequesterPrivate::_k_slotFileDialogAccepted() { if (!myFileDialog) { return; } const QUrl newUrl = myFileDialog->selectedUrls().constFirst(); if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); // remember url as defaultStartDir and update startdir for autocompletion if (newUrl.isLocalFile() && !m_startDirCustomized) { m_startDir = newUrl.adjusted(QUrl::RemoveFilename); updateCompletionStartDir(m_startDir); } } } void KUrlRequester::setMode(KFile::Modes mode) { Q_ASSERT((mode & KFile::Files) == 0); d->fileDialogMode = mode; if ((mode & KFile::Directory) && !(mode & KFile::File)) { d->myCompletion->setMode(KUrlCompletion::DirCompletion); } if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, mode, d->fileDialogAcceptMode); } } KFile::Modes KUrlRequester::mode() const { return d->fileDialogMode; } void KUrlRequester::setAcceptMode(QFileDialog::AcceptMode mode) { d->fileDialogAcceptMode = mode; if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, d->fileDialogMode, mode); } } QFileDialog::AcceptMode KUrlRequester::acceptMode() const { return d->fileDialogAcceptMode; } void KUrlRequester::setFilter(const QString &filter) { d->fileDialogFilter = filter; if (d->myFileDialog) { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } } QString KUrlRequester::filter() const { return d->fileDialogFilter; } void KUrlRequester::setMimeTypeFilters(const QStringList &mimeTypes) { d->mimeTypeFilters = mimeTypes; if (d->myFileDialog) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } d->myCompletion->setMimeTypeFilters(d->mimeTypeFilters); } QStringList KUrlRequester::mimeTypeFilters() const { return d->mimeTypeFilters; } #ifndef KIOWIDGETS_NO_DEPRECATED QFileDialog *KUrlRequester::fileDialog() const { if (!d->myFileDialog) { d->myFileDialog = new QFileDialog(window(), windowTitle()); if (!d->mimeTypeFilters.isEmpty()) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } else { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } d->applyFileMode(d->myFileDialog, d->fileDialogMode, d->fileDialogAcceptMode); d->myFileDialog->setWindowModality(d->fileDialogModality); connect(d->myFileDialog, SIGNAL(accepted()), SLOT(_k_slotFileDialogAccepted())); } return d->myFileDialog; } #endif void KUrlRequester::clear() { d->setText(QString()); } KLineEdit *KUrlRequester::lineEdit() const { return d->edit; } KComboBox *KUrlRequester::comboBox() const { return d->combo; } void KUrlRequester::KUrlRequesterPrivate::_k_slotUpdateUrl() { const QUrl visibleUrl = url(); QUrl u = visibleUrl; if (visibleUrl.isRelative()) { u = QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/')).resolved(visibleUrl); } myButton->setURL(u); } bool KUrlRequester::eventFilter(QObject *obj, QEvent *ev) { if ((d->edit == obj) || (d->combo == obj)) { if ((ev->type() == QEvent::FocusIn) || (ev->type() == QEvent::FocusOut)) // Forward focusin/focusout events to the urlrequester; needed by file form element in khtml { QApplication::sendEvent(this, ev); } } return QWidget::eventFilter(obj, ev); } QPushButton *KUrlRequester::button() const { return d->myButton; } KUrlCompletion *KUrlRequester::completionObject() const { return d->myCompletion; } #ifndef KIOWIDGETS_NO_DEPRECATED void KUrlRequester::setClickMessage(const QString &msg) { setPlaceholderText(msg); } #endif void KUrlRequester::setPlaceholderText(const QString &msg) { if (d->edit) { d->edit->setPlaceholderText(msg); } } #ifndef KIOWIDGETS_NO_DEPRECATED QString KUrlRequester::clickMessage() const { return placeholderText(); } #endif QString KUrlRequester::placeholderText() const { if (d->edit) { return d->edit->placeholderText(); } else { return QString(); } } Qt::WindowModality KUrlRequester::fileDialogModality() const { return d->fileDialogModality; } void KUrlRequester::setFileDialogModality(Qt::WindowModality modality) { d->fileDialogModality = modality; } const KEditListWidget::CustomEditor &KUrlRequester::customEditor() { setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); KLineEdit *edit = d->edit; if (!edit && d->combo) { edit = qobject_cast(d->combo->lineEdit()); } #ifndef NDEBUG if (!edit) { qCWarning(KIO_WIDGETS) << "KUrlRequester's lineedit is not a KLineEdit!??\n"; } #endif d->editor.setRepresentationWidget(this); d->editor.setLineEdit(edit); return d->editor; } KUrlComboRequester::KUrlComboRequester(QWidget *parent) : KUrlRequester(new KComboBox(false), parent), d(nullptr) { } #include "moc_kurlrequester.cpp" #include "kurlrequester.moc"