diff --git a/src/core/authinfo.cpp b/src/core/authinfo.cpp index a948df88..afaab908 100644 --- a/src/core/authinfo.cpp +++ b/src/core/authinfo.cpp @@ -1,481 +1,479 @@ /* * This file is part of the KDE libraries * Copyright (C) 2000-2001 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 "authinfo.h" #include #include #include #include #include #include #include using namespace KIO; ////// class ExtraField { public: ExtraField() : flags(AuthInfo::ExtraFieldNoFlags) { } ExtraField(const ExtraField &other) : customTitle(other.customTitle), flags(other.flags), value(other.value) { } ExtraField &operator=(const ExtraField &other) { customTitle = other.customTitle; flags = other.flags; value = other.value; return *this; } QString customTitle; // reserved for future use AuthInfo::FieldFlags flags; QVariant value; }; Q_DECLARE_METATYPE(ExtraField) static QDataStream &operator<< (QDataStream &s, const ExtraField &extraField) { s << extraField.customTitle; s << static_cast(extraField.flags); s << extraField.value; return s; } static QDataStream &operator>> (QDataStream &s, ExtraField &extraField) { s >> extraField.customTitle; int i; s >> i; extraField.flags = AuthInfo::FieldFlags(i); s >> extraField.value; return s; } static QDBusArgument &operator<<(QDBusArgument &argument, const ExtraField &extraField) { argument.beginStructure(); argument << extraField.customTitle << static_cast(extraField.flags) << QDBusVariant(extraField.value); argument.endStructure(); return argument; } static const QDBusArgument &operator>>(const QDBusArgument &argument, ExtraField &extraField) { QDBusVariant value; int flag; argument.beginStructure(); argument >> extraField.customTitle >> flag >> value; argument.endStructure(); extraField.value = value.variant(); extraField.flags = KIO::AuthInfo::FieldFlags(flag); return argument; } class KIO::AuthInfoPrivate { public: QMap extraFields; }; ////// AuthInfo::AuthInfo() : d(new AuthInfoPrivate()) { modified = false; readOnly = false; verifyPath = false; keepPassword = false; AuthInfo::registerMetaTypes(); } AuthInfo::AuthInfo(const AuthInfo &info) : d(new AuthInfoPrivate()) { (*this) = info; AuthInfo::registerMetaTypes(); } AuthInfo::~AuthInfo() { delete d; } AuthInfo &AuthInfo::operator= (const AuthInfo &info) { url = info.url; username = info.username; password = info.password; prompt = info.prompt; caption = info.caption; comment = info.comment; commentLabel = info.commentLabel; realmValue = info.realmValue; digestInfo = info.digestInfo; verifyPath = info.verifyPath; readOnly = info.readOnly; keepPassword = info.keepPassword; modified = info.modified; d->extraFields = info.d->extraFields; return *this; } bool AuthInfo::isModified() const { return modified; } void AuthInfo::setModified(bool flag) { modified = flag; } ///// void AuthInfo::setExtraField(const QString &fieldName, const QVariant &value) { d->extraFields[fieldName].value = value; } void AuthInfo::setExtraFieldFlags(const QString &fieldName, const FieldFlags flags) { d->extraFields[fieldName].flags = flags; } QVariant AuthInfo::getExtraField(const QString &fieldName) const { const auto it = d->extraFields.constFind(fieldName); if (it == d->extraFields.constEnd()) { return QVariant(); } return it->value; } AuthInfo::FieldFlags AuthInfo::getExtraFieldFlags(const QString &fieldName) const { const auto it = d->extraFields.constFind(fieldName); if (it == d->extraFields.constEnd()) { return AuthInfo::ExtraFieldNoFlags; } return it->flags; } void AuthInfo::registerMetaTypes() { qRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); } ///// QDataStream &KIO::operator<< (QDataStream &s, const AuthInfo &a) { s << quint8(1) << a.url << a.username << a.password << a.prompt << a.caption << a.comment << a.commentLabel << a.realmValue << a.digestInfo << a.verifyPath << a.readOnly << a.keepPassword << a.modified << a.d->extraFields; return s; } QDataStream &KIO::operator>> (QDataStream &s, AuthInfo &a) { quint8 version; s >> version >> a.url >> a.username >> a.password >> a.prompt >> a.caption >> a.comment >> a.commentLabel >> a.realmValue >> a.digestInfo >> a.verifyPath >> a.readOnly >> a.keepPassword >> a.modified >> a.d->extraFields; return s; } QDBusArgument &KIO::operator<<(QDBusArgument &argument, const AuthInfo &a) { argument.beginStructure(); argument << quint8(1) << a.url.toString() << a.username << a.password << a.prompt << a.caption << a.comment << a.commentLabel << a.realmValue << a.digestInfo << a.verifyPath << a.readOnly << a.keepPassword << a.modified << a.d->extraFields; argument.endStructure(); return argument; } const QDBusArgument &KIO::operator>>(const QDBusArgument &argument, AuthInfo &a) { QString url; quint8 version; argument.beginStructure(); argument >> version >> url >> a.username >> a.password >> a.prompt >> a.caption >> a.comment >> a.commentLabel >> a.realmValue >> a.digestInfo >> a.verifyPath >> a.readOnly >> a.keepPassword >> a.modified >> a.d->extraFields; argument.endStructure(); a.url = QUrl(url); return argument; } typedef QList LoginList; typedef QMap LoginMap; class Q_DECL_HIDDEN NetRC::NetRCPrivate { public: NetRCPrivate() : isDirty(false), index(-1) {} QString extract(const QString &buf, const QString &key); void getMachinePart(const QString &line); void getMacdefPart(const QString &line); bool isDirty; LoginMap loginMap; QTextStream fstream; QString type; int index; }; NetRC *NetRC::instance = nullptr; NetRC::NetRC() : d(new NetRCPrivate) { } NetRC::~NetRC() { delete instance; instance = nullptr; delete d; } NetRC *NetRC::self() { if (!instance) { instance = new NetRC; } return instance; } bool NetRC::lookup(const QUrl &url, AutoLogin &login, bool userealnetrc, const QString &_type, LookUpMode mode) { //qDebug() << "AutoLogin lookup for: " << url.host(); if (!url.isValid()) { return false; } QString type = _type; if (type.isEmpty()) { type = url.scheme(); } if (d->loginMap.isEmpty() || d->isDirty) { d->loginMap.clear(); QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kionetrc"); bool kionetrcStatus = parse(filename); bool netrcStatus = false; if (userealnetrc) { filename = QDir::homePath() + QLatin1String("/.netrc"); netrcStatus = parse(filename); } if (!(kionetrcStatus || netrcStatus)) { return false; } } const auto loginIt = d->loginMap.constFind(type); if (loginIt == d->loginMap.constEnd()) { return false; } const LoginList &l = *loginIt; if (l.isEmpty()) { return false; } - for (LoginList::ConstIterator it = l.begin(); it != l.end(); ++it) { - const AutoLogin &log = *it; - + for (const AutoLogin &log : l) { if ((mode & defaultOnly) == defaultOnly && log.machine == QLatin1String("default") && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; } if ((mode & presetOnly) == presetOnly && log.machine == QLatin1String("preset") && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; } if ((mode & exactOnly) == exactOnly && log.machine == url.host() && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; break; } } return true; } void NetRC::reload() { d->isDirty = true; } bool NetRC::parse(const QString &fileName) { QFile file(fileName); if (file.permissions() != (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)) { return false; } if (!file.open(QIODevice::ReadOnly)) { return false; } d->fstream.setDevice(&file); QString line; while (!d->fstream.atEnd()) { line = d->fstream.readLine().simplified(); // If line is a comment or is empty, read next line if ((line.startsWith(QLatin1Char('#')) || line.isEmpty())) { continue; } // If line refers to a machine, maybe it is spread in more lines. // getMachinePart() will take care of getting all the info and putting it into loginMap. if ((line.startsWith(QLatin1String("machine")) || line.startsWith(QLatin1String("default")) || line.startsWith(QLatin1String("preset")))) { d->getMachinePart(line); continue; } // If line refers to a macdef, it will be more than one line. // getMacdefPart() will take care of getting all the lines of the macro // and putting them into loginMap if (line.startsWith(QLatin1String("macdef"))) { d->getMacdefPart(line); continue; } } return true; } QString NetRC::NetRCPrivate::extract(const QString &buf, const QString &key) { QStringList stringList = buf.split(QLatin1Char(' '), QString::SkipEmptyParts); int i = stringList.indexOf(key); if ((i != -1) && (i + 1 < stringList.size())) { return stringList.at(i + 1); } else { return QString(); } } void NetRC::NetRCPrivate::getMachinePart(const QString &line) { QString buf = line; while (!(buf.contains(QLatin1String("login")) && (buf.contains(QLatin1String("password")) || buf.contains(QLatin1String("account")) || buf.contains(QLatin1String("type"))))) { buf += QLatin1Char(' ') + fstream.readLine().simplified(); } // Once we've got all the info, process it. AutoLogin l; l.machine = extract(buf, QStringLiteral("machine")); if (l.machine.isEmpty()) { if (buf.contains(QLatin1String("default"))) { l.machine = QStringLiteral("default"); } else if (buf.contains(QLatin1String("preset"))) { l.machine = QStringLiteral("preset"); } } l.login = extract(buf, QStringLiteral("login")); l.password = extract(buf, QStringLiteral("password")); if (l.password.isEmpty()) { l.password = extract(buf, QStringLiteral("account")); } type = l.type = extract(buf, QStringLiteral("type")); if (l.type.isEmpty() && !l.machine.isEmpty()) { type = l.type = QStringLiteral("ftp"); } loginMap[l.type].append(l); index = loginMap[l.type].count() - 1; } void NetRC::NetRCPrivate::getMacdefPart(const QString &line) { QString buf = line; QString macro = extract(buf, QStringLiteral("macdef")); QString newLine; while (!fstream.atEnd()) { newLine = fstream.readLine().simplified(); if (!newLine.isEmpty()) { buf += QLatin1Char('\n') + newLine; } else { break; } } loginMap[type][index].macdef[macro].append(buf); } diff --git a/src/core/desktopexecparser.cpp b/src/core/desktopexecparser.cpp index 7647de72..6940622d 100644 --- a/src/core/desktopexecparser.cpp +++ b/src/core/desktopexecparser.cpp @@ -1,459 +1,459 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006-2013 David Faure Copyright (C) 2009 Michael Pyne 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 "desktopexecparser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 #include "kiocoredebug.h" class KRunMX1 : public KMacroExpanderBase { public: explicit KRunMX1(const KService &_service) : KMacroExpanderBase(QLatin1Char('%')) , hasUrls(false) , hasSpec(false) , service(_service) {} bool hasUrls; bool hasSpec; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: const KService &service; }; int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'c': ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'k': ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'i': ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'm': // ret << "-miniicon" << service.icon().replace( '%', "%%" ); qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')'; break; case 'u': case 'U': hasUrls = true; Q_FALLTHROUGH(); /* fallthrough */ case 'f': case 'F': case 'n': case 'N': case 'd': case 'D': case 'v': hasSpec = true; Q_FALLTHROUGH(); /* fallthrough */ default: return -2; // subst with same and skip } return 2; } class KRunMX2 : public KMacroExpanderBase { public: explicit KRunMX2(const QList &_urls) : KMacroExpanderBase(QLatin1Char('%')) , ignFile(false), urls(_urls) {} bool ignFile; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: void subst(int option, const QUrl &url, QStringList &ret); const QList &urls; }; void KRunMX2::subst(int option, const QUrl &url, QStringList &ret) { switch (option) { case 'u': ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString()); break; case 'd': ret << url.adjusted(QUrl::RemoveFilename).path(); break; case 'f': ret << QDir::toNativeSeparators(url.toLocalFile()); break; case 'n': ret << url.fileName(); break; case 'v': if (url.isLocalFile() && QFile::exists(url.toLocalFile())) { ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev"); } break; } return; } int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'f': case 'u': case 'n': case 'd': case 'v': if (urls.isEmpty()) { if (!ignFile) { //qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str; } } else if (urls.count() > 1) { qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str; } else { subst(option, urls.first(), ret); } break; case 'F': case 'U': case 'N': case 'D': option += 'a' - 'A'; - for (QList::ConstIterator it = urls.begin(); it != urls.end(); ++it) { - subst(option, *it, ret); + for (const QUrl &url : urls) { + subst(option, url, ret); } break; case '%': ret = QStringList(QStringLiteral("%")); break; default: return -2; // subst with same and skip } return 2; } QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service) { QStringList supportedProtocols = service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); KRunMX1 mx1(service); QString exec = service.exec(); if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) { if (!supportedProtocols.isEmpty()) { qCWarning(KIO_CORE) << service.entryPath() << "contains a X-KDE-Protocols line but doesn't use %u or %U in its Exec line! This is inconsistent."; } return QStringList(); } else { if (supportedProtocols.isEmpty()) { // compat mode: assume KIO if not set and it's a KDE app (or a KDE service) const QStringList categories = service.property(QStringLiteral("Categories")).toStringList(); if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) { supportedProtocols.append(QStringLiteral("KIO")); } else { // if no KDE app, be a bit over-generic supportedProtocols.append(QStringLiteral("http")); supportedProtocols.append(QStringLiteral("https")); // #253294 supportedProtocols.append(QStringLiteral("ftp")); } } } //qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols; return supportedProtocols; } bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols) { if (supportedProtocols.contains(QLatin1String("KIO"))) { return true; } return url.isLocalFile() || supportedProtocols.contains(url.scheme().toLower()); } bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) { if (KProtocolInfo::isHelperProtocol(url)) { return true; } if (KProtocolInfo::isKnownProtocol(url)) { return false; // see schemeHandler()... this is case B, we prefer kioslaves over the competition } const KService::Ptr service = KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + url.scheme()); if (service) { qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName(); } return service; } class KIO::DesktopExecParserPrivate { public: DesktopExecParserPrivate(const KService &_service, const QList &_urls) : service(_service), urls(_urls), tempFiles(false) {} const KService &service; QList urls; bool tempFiles; QString suggestedFileName; }; KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList &urls) : d(new DesktopExecParserPrivate(service, urls)) { } KIO::DesktopExecParser::~DesktopExecParser() { } void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles) { d->tempFiles = tempFiles; } void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName) { d->suggestedFileName = suggestedFileName; } static const QString kioexecPath() { QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec"); if (!QFileInfo::exists(kioexec)) kioexec = QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kioexec"); Q_ASSERT(QFileInfo::exists(kioexec)); return kioexec; } QStringList KIO::DesktopExecParser::resultingArguments() const { QString exec = d->service.exec(); if (exec.isEmpty()) { qCWarning(KIO_CORE) << "KRun: no Exec field in `" << d->service.entryPath() << "' !"; return QStringList(); } QStringList result; bool appHasTempFileOption; KRunMX1 mx1(d->service); KRunMX2 mx2(d->urls); if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax qCWarning(KIO_CORE) << "KRun: syntax error in command" << d->service.exec() << ", service" << d->service.name(); return QStringList(); } // FIXME: the current way of invoking kioexec disables term and su use // Check if we need "tempexec" (kioexec in fact) appHasTempFileOption = d->tempFiles && d->service.property(QStringLiteral("X-KDE-HasTempFileOption")).toBool(); if (d->tempFiles && !appHasTempFileOption && d->urls.size()) { result << kioexecPath() << QStringLiteral("--tempfiles") << exec; if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result += QUrl::toStringList(d->urls); return result; } // Check if we need kioexec bool useKioexec = false; if (!mx1.hasUrls) { Q_FOREACH (const QUrl &url, d->urls) if (!url.isLocalFile() && !hasSchemeHandler(url)) { useKioexec = true; //qCDebug(KIO_CORE) << "non-local files, application does not support urls, using kioexec"; break; } } else { // app claims to support %u/%U, check which protocols QStringList appSupportedProtocols = supportedProtocols(d->service); Q_FOREACH (const QUrl &url, d->urls) { if (!isProtocolInSupportedList(url, appSupportedProtocols) && !hasSchemeHandler(url)) { useKioexec = true; //qCDebug(KIO_CORE) << "application does not support url, using kioexec:" << url; break; } } } if (useKioexec) { // We need to run the app through kioexec result << kioexecPath(); if (d->tempFiles) { result << QStringLiteral("--tempfiles"); } if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result << exec; result += QUrl::toStringList(d->urls); return result; } if (appHasTempFileOption) { exec += QLatin1String(" --tempfile"); } // Did the user forget to append something like '%f'? // If so, then assume that '%f' is the right choice => the application // accepts only local files. if (!mx1.hasSpec) { exec += QLatin1String(" %f"); mx2.ignFile = true; } mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value /* 1 = need_shell, 2 = terminal, 4 = su 0 << split(cmd) 1 << "sh" << "-c" << cmd 2 << split(term) << "-e" << split(cmd) 3 << split(term) << "-e" << "sh" << "-c" << cmd 4 << "kdesu" << "-u" << user << "-c" << cmd 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd)) 6 << split(term) << "-e" << "su" << user << "-c" << cmd 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd)) "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh. this could be optimized with the -s switch of some su versions (e.g., debian linux). */ if (d->service.terminal()) { KConfigGroup cg(KSharedConfig::openConfig(), "General"); QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole")); if (terminal == QLatin1String("konsole")) { if (!d->service.workingDirectory().isEmpty()) { terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory()); } terminal += QLatin1String(" -qwindowtitle '%c' %i"); } terminal += QLatin1Char(' ') + d->service.terminalOptions(); if (!mx1.expandMacrosShellQuote(terminal)) { qCWarning(KIO_CORE) << "KRun: syntax error in command" << terminal << ", service" << d->service.name(); return QStringList(); } mx2.expandMacrosShellQuote(terminal); result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell! result << QStringLiteral("-e"); } KShell::Errors err; QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err); if (err == KShell::NoError && !execlist.isEmpty()) { // mx1 checked for syntax errors already // Resolve the executable to ensure that helpers in libexec are found. // Too bad for commands that need a shell - they must reside in $PATH. QString exePath = QStandardPaths::findExecutable(execlist.first()); if (exePath.isEmpty()) { exePath = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/") + execlist.first(); } if (QFile::exists(exePath)) { execlist[0] = exePath; } } if (d->service.substituteUid()) { if (d->service.terminal()) { result << QStringLiteral("su"); } else { QString kdesu = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kdesu"); if (!QFile::exists(kdesu)) { kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu")); } if (!QFile::exists(kdesu)) { // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu' result << QStringLiteral("kdesu"); return result; } else { result << kdesu << QStringLiteral("-u"); } } result << d->service.username() << QStringLiteral("-c"); if (err == KShell::FoundMeta) { exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec); } else { exec = KShell::joinArgs(execlist); } result << exec; } else { if (err == KShell::FoundMeta) { result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec; } else { result += execlist; } } return result; } //static QString KIO::DesktopExecParser::executableName(const QString &execLine) { const QString bin = executablePath(execLine); return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1); } //static QString KIO::DesktopExecParser::executablePath(const QString &execLine) { // Remove parameters and/or trailing spaces. const QStringList args = KShell::splitArgs(execLine); - for (QStringList::ConstIterator it = args.begin(); it != args.end(); ++it) { - if (!(*it).contains(QLatin1Char('='))) { - return *it; + for (const QString &arg : args) { + if (!arg.contains(QLatin1Char('='))) { + return arg; } } return QString(); } diff --git a/src/core/kcoredirlister.cpp b/src/core/kcoredirlister.cpp index bf4ee82e..a6db991b 100644 --- a/src/core/kcoredirlister.cpp +++ b/src/core/kcoredirlister.cpp @@ -1,2815 +1,2812 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis 2000 Carsten Pfeiffer 2003-2005 David Faure 2001-2006 Michael Brade 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 "kcoredirlister.h" #include "kcoredirlister_p.h" #include #include #include "kprotocolmanager.h" #include "kmountpoint.h" #include "kiocoredebug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_DIRLISTER) Q_LOGGING_CATEGORY(KIO_CORE_DIRLISTER, "kf5.kio.core.dirlister", QtWarningMsg) // Enable this to get printDebug() called often, to see the contents of the cache //#define DEBUG_CACHE // Make really sure it doesn't get activated in the final build #ifdef NDEBUG #undef DEBUG_CACHE #endif Q_GLOBAL_STATIC(KCoreDirListerCache, kDirListerCache) KCoreDirListerCache::KCoreDirListerCache() : itemsCached(10), // keep the last 10 directories around m_cacheHiddenFiles(10) // keep the last 10 ".hidden" files around { qCDebug(KIO_CORE_DIRLISTER); connect(&pendingUpdateTimer, &QTimer::timeout, this, &KCoreDirListerCache::processPendingUpdates); pendingUpdateTimer.setSingleShot(true); connect(KDirWatch::self(), &KDirWatch::dirty, this, &KCoreDirListerCache::slotFileDirty); connect(KDirWatch::self(), &KDirWatch::created, this, &KCoreDirListerCache::slotFileCreated); connect(KDirWatch::self(), &KDirWatch::deleted, this, &KCoreDirListerCache::slotFileDeleted); kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this); connect(kdirnotify, &org::kde::KDirNotify::FileRenamedWithLocalPath, this, &KCoreDirListerCache::slotFileRenamed); connect(kdirnotify, &org::kde::KDirNotify::FilesAdded , this, &KCoreDirListerCache::slotFilesAdded); connect(kdirnotify, &org::kde::KDirNotify::FilesChanged, this, &KCoreDirListerCache::slotFilesChanged); connect(kdirnotify, &org::kde::KDirNotify::FilesRemoved, this, QOverload::of(&KCoreDirListerCache::slotFilesRemoved)); // Probably not needed in KF5 anymore: // The use of KUrl::url() in ~DirItem (sendSignal) crashes if the static for QRegExpEngine got deleted already, // so we need to destroy the KCoreDirListerCache before that. //qAddPostRoutine(kDirListerCache.destroy); } KCoreDirListerCache::~KCoreDirListerCache() { qCDebug(KIO_CORE_DIRLISTER); qDeleteAll(itemsInUse); itemsInUse.clear(); itemsCached.clear(); directoryData.clear(); m_cacheHiddenFiles.clear(); if (KDirWatch::exists()) { KDirWatch::self()->disconnect(this); } } // setting _reload to true will emit the old files and // call updateDirectory bool KCoreDirListerCache::listDir(KCoreDirLister *lister, const QUrl &_u, bool _keep, bool _reload) { QUrl _url(_u); _url.setPath(QDir::cleanPath(_url.path())); // kill consecutive slashes if (!_url.host().isEmpty() && KProtocolInfo::protocolClass(_url.scheme()) == QLatin1String(":local") && _url.scheme() != QLatin1String("file")) { // ":local" protocols ignore the hostname, so strip it out preventively - #160057 // kio_file is special cased since it does honor the hostname (by redirecting to e.g. smb) _url.setHost(QString()); if (_keep == false) { emit lister->redirection(_url); } } // like this we don't have to worry about trailing slashes any further _url = _url.adjusted(QUrl::StripTrailingSlash); QString resolved; if (_url.isLocalFile()) { // Resolve symlinks (#213799) const QString local = _url.toLocalFile(); resolved = QFileInfo(local).canonicalFilePath(); if (local != resolved) { canonicalUrls[QUrl::fromLocalFile(resolved)].append(_url); } // TODO: remove entry from canonicalUrls again in forgetDirs // Note: this is why we use a QStringList value in there rather than a QSet: // we can just remove one entry and not have to worry about other dirlisters // (the non-unicity of the stringlist gives us the refcounting, basically). } if (!validUrl(lister, _url)) { qCDebug(KIO_CORE_DIRLISTER) << lister << "url=" << _url << "not a valid url"; return false; } qCDebug(KIO_CORE_DIRLISTER) << lister << "url=" << _url << "keep=" << _keep << "reload=" << _reload; #ifdef DEBUG_CACHE printDebug(); #endif if (!_keep) { // stop any running jobs for lister stop(lister, true /*silent*/); // clear our internal list for lister forgetDirs(lister); lister->d->rootFileItem = KFileItem(); } else if (lister->d->lstDirs.contains(_url)) { // stop the job listing _url for this lister stopListingUrl(lister, _url, true /*silent*/); // remove the _url as well, it will be added in a couple of lines again! // forgetDirs with three args does not do this // TODO: think about moving this into forgetDirs lister->d->lstDirs.removeAll(_url); // clear _url for lister forgetDirs(lister, _url, true); if (lister->d->url == _url) { lister->d->rootFileItem = KFileItem(); } } lister->d->complete = false; lister->d->lstDirs.append(_url); if (lister->d->url.isEmpty() || !_keep) { // set toplevel URL only if not set yet lister->d->url = _url; } DirItem *itemU = itemsInUse.value(_url); KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; // find or insert if (dirData.listersCurrentlyListing.isEmpty()) { // if there is an update running for _url already we get into // the following case - it will just be restarted by updateDirectory(). dirData.listersCurrentlyListing.append(lister); DirItem *itemFromCache = nullptr; if (itemU || (!_reload && (itemFromCache = itemsCached.take(_url)))) { if (itemU) { qCDebug(KIO_CORE_DIRLISTER) << "Entry already in use:" << _url; // if _reload is set, then we'll emit cached items and then updateDirectory. } else { qCDebug(KIO_CORE_DIRLISTER) << "Entry in cache:" << _url; itemsInUse.insert(_url, itemFromCache); itemU = itemFromCache; } if (lister->d->autoUpdate) { itemU->incAutoUpdate(); } if (itemFromCache && itemFromCache->watchedWhileInCache) { itemFromCache->watchedWhileInCache = false;; itemFromCache->decAutoUpdate(); } emit lister->started(_url); // List items from the cache in a delayed manner, just like things would happen // if we were not using the cache. new KCoreDirLister::Private::CachedItemsJob(lister, _url, _reload); } else { // dir not in cache or _reload is true if (_reload) { qCDebug(KIO_CORE_DIRLISTER) << "Reloading directory:" << _url; itemsCached.remove(_url); } else { qCDebug(KIO_CORE_DIRLISTER) << "Listing directory:" << _url; } itemU = new DirItem(_url, resolved); itemsInUse.insert(_url, itemU); if (lister->d->autoUpdate) { itemU->incAutoUpdate(); } // // we have a limit of MAX_JOBS_PER_LISTER concurrently running jobs // if ( lister->d->numJobs() >= MAX_JOBS_PER_LISTER ) // { // pendingUpdates.insert( _url ); // } // else { KIO::ListJob *job = KIO::listDir(_url, KIO::HideProgressInfo); runningListJobs.insert(job, KIO::UDSEntryList()); lister->jobStarted(job); lister->d->connectJob(job); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotResult); connect(job, &KIO::ListJob::redirection, this, &KCoreDirListerCache::slotRedirection); emit lister->started(_url); } qCDebug(KIO_CORE_DIRLISTER) << "Entry now being listed by" << dirData.listersCurrentlyListing; } } else { qCDebug(KIO_CORE_DIRLISTER) << "Entry currently being listed:" << _url << "by" << dirData.listersCurrentlyListing; #ifdef DEBUG_CACHE printDebug(); #endif emit lister->started(_url); // Maybe listersCurrentlyListing/listersCurrentlyHolding should be QSets? Q_ASSERT(!dirData.listersCurrentlyListing.contains(lister)); dirData.listersCurrentlyListing.append(lister); KIO::ListJob *job = jobForUrl(_url); // job will be 0 if we were listing from cache rather than listing from a kio job. if (job) { lister->jobStarted(job); lister->d->connectJob(job); } Q_ASSERT(itemU); // List existing items in a delayed manner, just like things would happen // if we were not using the cache. qCDebug(KIO_CORE_DIRLISTER) << "Listing" << itemU->lstItems.count() << "cached items soon"; KCoreDirLister::Private::CachedItemsJob* cachedItemsJob = new KCoreDirLister::Private::CachedItemsJob(lister, _url, _reload); if (job) { // The ListJob will take care of emitting completed. // ### If it finishes before the CachedItemsJob, then we'll emit cached items after completed(), not sure how bad this is. cachedItemsJob->setEmitCompleted(false); } #ifdef DEBUG_CACHE printDebug(); #endif } return true; } KCoreDirLister::Private::CachedItemsJob *KCoreDirLister::Private::cachedItemsJobForUrl(const QUrl &url) const { for (CachedItemsJob *job : m_cachedItemsJobs) { if (job->url() == url) { return job; } } return nullptr; } KCoreDirLister::Private::CachedItemsJob::CachedItemsJob(KCoreDirLister *lister, const QUrl &url, bool reload) : KJob(lister), m_lister(lister), m_url(url), m_reload(reload), m_emitCompleted(true) { qCDebug(KIO_CORE_DIRLISTER) << "Creating CachedItemsJob" << this << "for lister" << lister << url; if (lister->d->cachedItemsJobForUrl(url)) { qCWarning(KIO_CORE) << "Lister" << lister << "has a cached items job already for" << url; } lister->d->m_cachedItemsJobs.append(this); setAutoDelete(true); start(); } // Called by start() via QueuedConnection void KCoreDirLister::Private::CachedItemsJob::done() { if (!m_lister) { // job was already killed, but waiting deletion due to deleteLater return; } kDirListerCache()->emitItemsFromCache(this, m_lister, m_url, m_reload, m_emitCompleted); emitResult(); } bool KCoreDirLister::Private::CachedItemsJob::doKill() { qCDebug(KIO_CORE_DIRLISTER) << this; kDirListerCache()->forgetCachedItemsJob(this, m_lister, m_url); if (!property("_kdlc_silent").toBool()) { emit m_lister->canceled(m_url); emit m_lister->canceled(); } m_lister = nullptr; return true; } void KCoreDirListerCache::emitItemsFromCache(KCoreDirLister::Private::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url, bool _reload, bool _emitCompleted) { KCoreDirLister::Private *kdl = lister->d; kdl->complete = false; DirItem *itemU = kDirListerCache()->itemsInUse.value(_url); if (!itemU) { qCWarning(KIO_CORE) << "Can't find item for directory" << _url << "anymore"; } else { const QList items = itemU->lstItems; const KFileItem rootItem = itemU->rootItem; _reload = _reload || !itemU->complete; if (kdl->rootFileItem.isNull() && !rootItem.isNull() && kdl->url == _url) { kdl->rootFileItem = rootItem; } if (!items.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "emitting" << items.count() << "for lister" << lister; kdl->addNewItems(_url, items); kdl->emitItems(); } } forgetCachedItemsJob(cachedItemsJob, lister, _url); // Emit completed, unless we were told not to, // or if listDir() was called while another directory listing for this dir was happening, // so we "joined" it. We detect that using jobForUrl to ensure it's a real ListJob, // not just a lister-specific CachedItemsJob (which wouldn't emit completed for us). if (_emitCompleted) { kdl->complete = true; emit lister->completed(_url); emit lister->completed(); if (_reload) { updateDirectory(_url); } } } void KCoreDirListerCache::forgetCachedItemsJob(KCoreDirLister::Private::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url) { // Modifications to data structures only below this point; // so that addNewItems is called with a consistent state lister->d->m_cachedItemsJobs.removeAll(cachedItemsJob); KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; Q_ASSERT(dirData.listersCurrentlyListing.contains(lister)); KIO::ListJob *listJob = jobForUrl(_url); if (!listJob) { Q_ASSERT(!dirData.listersCurrentlyHolding.contains(lister)); qCDebug(KIO_CORE_DIRLISTER) << "Moving from listing to holding, because no more job" << lister << _url; dirData.listersCurrentlyHolding.append(lister); dirData.listersCurrentlyListing.removeAll(lister); } else { qCDebug(KIO_CORE_DIRLISTER) << "Still having a listjob" << listJob << ", so not moving to currently-holding."; } } bool KCoreDirListerCache::validUrl(KCoreDirLister *lister, const QUrl &url) const { if (!url.isValid()) { qCWarning(KIO_CORE) << url.errorString(); lister->handleErrorMessage(i18n("Malformed URL\n%1", url.errorString())); return false; } if (!KProtocolManager::supportsListing(url)) { lister->handleErrorMessage(i18n("URL cannot be listed\n%1", url.toString())); return false; } return true; } void KCoreDirListerCache::stop(KCoreDirLister *lister, bool silent) { #ifdef DEBUG_CACHE //printDebug(); #endif qCDebug(KIO_CORE_DIRLISTER) << "lister:" << lister << "silent=" << silent; const QList urls = lister->d->lstDirs; for (const QUrl &url : urls) { stopListingUrl(lister, url, silent); } #if 0 // test code QHash::iterator dirit = directoryData.begin(); const QHash::iterator dirend = directoryData.end(); for (; dirit != dirend; ++dirit) { KCoreDirListerCacheDirectoryData &dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { qCDebug(KIO_CORE_DIRLISTER) << "ERROR: found lister" << lister << "in list - for" << dirit.key(); Q_ASSERT(false); } } #endif } void KCoreDirListerCache::stopListingUrl(KCoreDirLister *lister, const QUrl &_u, bool silent) { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); KCoreDirLister::Private::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(url); if (cachedItemsJob) { if (silent) { cachedItemsJob->setProperty("_kdlc_silent", true); } cachedItemsJob->kill(); // removes job from list, too } // TODO: consider to stop all the "child jobs" of url as well qCDebug(KIO_CORE_DIRLISTER) << lister << " url=" << url; const auto dirit = directoryData.find(url); if (dirit == directoryData.end()) { return; } KCoreDirListerCacheDirectoryData &dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { qCDebug(KIO_CORE_DIRLISTER) << " found lister" << lister << "in list - for" << url; if (dirData.listersCurrentlyListing.count() == 1) { // This was the only dirlister interested in the list job -> kill the job stopListJob(url, silent); } else { // Leave the job running for the other dirlisters, just unsubscribe us. dirData.listersCurrentlyListing.removeAll(lister); if (!silent) { emit lister->canceled(); emit lister->canceled(url); } } } } // Helper for stop() and stopListingUrl() void KCoreDirListerCache::stopListJob(const QUrl &url, bool silent) { // Old idea: if it's an update job, let's just leave the job running. // After all, update jobs do run for "listersCurrentlyHolding", // so there's no reason to kill them just because @p lister is now a holder. // However it could be a long-running non-local job (e.g. filenamesearch), which // the user wants to abort, and which will never be used for updating... // And in any case slotEntries/slotResult is not meant to be called by update jobs. // So, change of plan, let's kill it after all, in a way that triggers slotResult/slotUpdateResult. KIO::ListJob *job = jobForUrl(url); if (job) { qCDebug(KIO_CORE_DIRLISTER) << "Killing list job" << job << "for" << url; if (silent) { job->setProperty("_kdlc_silent", true); } job->kill(KJob::EmitResult); } } void KCoreDirListerCache::setAutoUpdate(KCoreDirLister *lister, bool enable) { // IMPORTANT: this method does not check for the current autoUpdate state! for (auto it = lister->d->lstDirs.constBegin(), cend = lister->d->lstDirs.constEnd(); it != cend; ++it) { DirItem *dirItem = itemsInUse.value(*it); Q_ASSERT(dirItem); if (enable) { dirItem->incAutoUpdate(); } else { dirItem->decAutoUpdate(); } } } void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister) { qCDebug(KIO_CORE_DIRLISTER) << lister; emit lister->clear(); // clear lister->d->lstDirs before calling forgetDirs(), so that // it doesn't contain things that itemsInUse doesn't. When emitting // the canceled signals, lstDirs must not contain anything that // itemsInUse does not contain. (otherwise it might crash in findByName()). const QList lstDirsCopy = lister->d->lstDirs; lister->d->lstDirs.clear(); qCDebug(KIO_CORE_DIRLISTER) << "Iterating over dirs" << lstDirsCopy; - for (QList::const_iterator it = lstDirsCopy.begin(); - it != lstDirsCopy.end(); ++it) { - forgetDirs(lister, *it, false); + for (const QUrl &dir : lstDirsCopy) { + forgetDirs(lister, dir, false); } } static bool manually_mounted(const QString &path, const KMountPoint::List &possibleMountPoints) { KMountPoint::Ptr mp = possibleMountPoints.findByPath(path); if (!mp) { // not listed in fstab -> yes, manually mounted if (possibleMountPoints.isEmpty()) { // no fstab at all -> don't assume anything return false; } return true; } // noauto -> manually mounted. Otherwise, mounted at boot time, won't be unmounted any time soon hopefully. return mp->mountOptions().contains(QLatin1String("noauto")); } void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, bool notify) { qCDebug(KIO_CORE_DIRLISTER) << lister << " _url: " << _url; const QUrl url = _url.adjusted(QUrl::StripTrailingSlash); DirectoryDataHash::iterator dit = directoryData.find(url); if (dit == directoryData.end()) { return; } KCoreDirListerCacheDirectoryData &dirData = *dit; dirData.listersCurrentlyHolding.removeAll(lister); // This lister doesn't care for updates running in anymore KIO::ListJob *job = jobForUrl(url); if (job) { lister->d->jobDone(job); } DirItem *item = itemsInUse.value(url); Q_ASSERT(item); bool insertIntoCache = false; if (dirData.listersCurrentlyHolding.isEmpty() && dirData.listersCurrentlyListing.isEmpty()) { // item not in use anymore -> move into cache if complete directoryData.erase(dit); itemsInUse.remove(url); // this job is a running update which nobody cares about anymore if (job) { killJob(job); qCDebug(KIO_CORE_DIRLISTER) << "Killing update job for " << url; // Well, the user of KCoreDirLister doesn't really care that we're stopping // a background-running job from a previous URL (in listDir) -> commented out. // stop() already emitted canceled. //emit lister->canceled( url ); if (lister->d->numJobs() == 0) { lister->d->complete = true; //emit lister->canceled(); } } if (notify) { lister->d->lstDirs.removeAll(url); emit lister->clear(url); } insertIntoCache = item->complete; if (insertIntoCache) { // TODO(afiestas): remove use of KMountPoint+manually_mounted and port to Solid: // 1) find Volume for the local path "item->url.toLocalFile()" (which could be anywhere // under the mount point) -- probably needs a new operator in libsolid query parser // 2) [**] becomes: if (Drive is hotpluggable or Volume is removable) "set to dirty" else "keep watch" const KMountPoint::List possibleMountPoints = KMountPoint::possibleMountPoints(KMountPoint::NeedMountOptions); // Should we forget the dir for good, or keep a watch on it? // Generally keep a watch, except when it would prevent // unmounting a removable device (#37780) const bool isLocal = item->url.isLocalFile(); bool isManuallyMounted = false; bool containsManuallyMounted = false; if (isLocal) { isManuallyMounted = manually_mounted(item->url.toLocalFile(), possibleMountPoints); if (!isManuallyMounted) { // Look for a manually-mounted directory inside // If there's one, we can't keep a watch either, FAM would prevent unmounting the CDROM // I hope this isn't too slow auto kit = item->lstItems.constBegin(); const auto kend = item->lstItems.constEnd(); for (; kit != kend && !containsManuallyMounted; ++kit) if ((*kit).isDir() && manually_mounted((*kit).url().toLocalFile(), possibleMountPoints)) { containsManuallyMounted = true; } } } if (isManuallyMounted || containsManuallyMounted) { // [**] qCDebug(KIO_CORE_DIRLISTER) << "Not adding a watch on " << item->url << " because it " << ( isManuallyMounted ? "is manually mounted" : "contains a manually mounted subdir" ); item->complete = false; // set to "dirty" } else { item->incAutoUpdate(); // keep watch item->watchedWhileInCache = true; } } else { delete item; item = nullptr; } } if (item && lister->d->autoUpdate) { item->decAutoUpdate(); } // Inserting into QCache must be done last, since it might delete the item if (item && insertIntoCache) { qCDebug(KIO_CORE_DIRLISTER) << lister << "item moved into cache:" << url; itemsCached.insert(url, item); } } void KCoreDirListerCache::updateDirectory(const QUrl &_dir) { qCDebug(KIO_CORE_DIRLISTER) << _dir; const QUrl dir = _dir.adjusted(QUrl::StripTrailingSlash); if (!checkUpdate(dir)) { return; } // A job can be running to // - only list a new directory: the listers are in listersCurrentlyListing // - only update a directory: the listers are in listersCurrentlyHolding // - update a currently running listing: the listers are in both KCoreDirListerCacheDirectoryData &dirData = directoryData[dir]; const QList listers = dirData.listersCurrentlyListing; const QList holders = dirData.listersCurrentlyHolding; qCDebug(KIO_CORE_DIRLISTER) << dir << "listers=" << listers << "holders=" << holders; bool killed = false; KIO::ListJob *job = jobForUrl(dir); if (job) { // the job is running already, tell it to do another update at the end // (don't kill it, we would keep doing that during a long download to a slow sshfs mount) job->setProperty("need_another_update", true); return; } else { // Emit any cached items. // updateDirectory() is about the diff compared to the cached items... for (const KCoreDirLister *kdl : listers) { KCoreDirLister::Private::CachedItemsJob *cachedItemsJob = kdl->d->cachedItemsJobForUrl(dir); if (cachedItemsJob) { cachedItemsJob->setEmitCompleted(false); cachedItemsJob->done(); // removes from cachedItemsJobs list delete cachedItemsJob; killed = true; } } } qCDebug(KIO_CORE_DIRLISTER) << "Killed=" << killed; // we don't need to emit canceled signals since we only replaced the job, // the listing is continuing. if (!(listers.isEmpty() || killed)) { qCWarning(KIO_CORE) << "The unexpected happened."; qCWarning(KIO_CORE) << "listers for" << dir << "=" << listers; qCWarning(KIO_CORE) << "job=" << job; for (const KCoreDirLister *kdl : listers) { qCDebug(KIO_CORE_DIRLISTER) << "lister" << kdl << "m_cachedItemsJobs=" << kdl->d->m_cachedItemsJobs; } #ifndef NDEBUG printDebug(); #endif } Q_ASSERT(listers.isEmpty() || killed); job = KIO::listDir(dir, KIO::HideProgressInfo); runningListJobs.insert(job, KIO::UDSEntryList()); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult); qCDebug(KIO_CORE_DIRLISTER) << "update started in" << dir; for (KCoreDirLister *kdl : listers) { kdl->jobStarted(job); } if (!holders.isEmpty()) { if (!killed) { for (KCoreDirLister *kdl : holders) { kdl->jobStarted(job); emit kdl->started(dir); } } else { for (KCoreDirLister *kdl : holders) { kdl->jobStarted(job); } } } } bool KCoreDirListerCache::checkUpdate(const QUrl &_dir) { if (!itemsInUse.contains(_dir)) { DirItem *item = itemsCached[_dir]; if (item && item->complete) { item->complete = false; item->watchedWhileInCache = false; item->decAutoUpdate(); qCDebug(KIO_CORE_DIRLISTER) << "directory " << _dir << " not in use, marked dirty."; } //else qCDebug(KIO_CORE_DIRLISTER) << "aborted, directory " << _dir << " not in cache."; return false; } else { return true; } } KFileItem KCoreDirListerCache::itemForUrl(const QUrl &url) const { return findByUrl(nullptr, url); } KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir) const { const QUrl url = dir.adjusted(QUrl::StripTrailingSlash); DirItem *item = itemsInUse.value(url); if (!item) { item = itemsCached[url]; } return item; } QList *KCoreDirListerCache::itemsForDir(const QUrl &dir) const { DirItem *item = dirItemForUrl(dir); return item ? &item->lstItems : nullptr; } KFileItem KCoreDirListerCache::findByName(const KCoreDirLister *lister, const QString &_name) const { Q_ASSERT(lister); for (QList::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it) { DirItem *dirItem = itemsInUse.value(*it); Q_ASSERT(dirItem); auto lit = dirItem->lstItems.constBegin(); const auto litend = dirItem->lstItems.constEnd(); for (; lit != litend; ++lit) { if ((*lit).name() == _name) { return *lit; } } } return KFileItem(); } KFileItem KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); DirItem *dirItem = dirItemForUrl(parentDir); if (dirItem) { // If lister is set, check that it contains this dir if (!lister || lister->d->lstDirs.contains(parentDir)) { // Binary search auto it = std::lower_bound(dirItem->lstItems.begin(), dirItem->lstItems.end(), url); if (it != dirItem->lstItems.end() && it->url() == url) { return *it; } } } // Maybe _u is a directory itself? (see KDirModelTest::testChmodDirectory) // We check this last, though, we prefer returning a kfileitem with an actual // name if possible (and we make it '.' for root items later). dirItem = dirItemForUrl(url); if (dirItem && !dirItem->rootItem.isNull() && dirItem->rootItem.url() == url) { // If lister is set, check that it contains this dir if (!lister || lister->d->lstDirs.contains(url)) { return dirItem->rootItem; } } return KFileItem(); } void KCoreDirListerCache::slotFilesAdded(const QString &dir /*url*/) // from KDirNotify signals { QUrl urlDir(dir); itemsAddedInDirectory(urlDir); } void KCoreDirListerCache::itemsAddedInDirectory(const QUrl &urlDir) { qCDebug(KIO_CORE_DIRLISTER) << urlDir; const QList urls = directoriesForCanonicalPath(urlDir); for (const QUrl &u : urls) { updateDirectory(u); } } void KCoreDirListerCache::slotFilesRemoved(const QStringList &fileList) // from KDirNotify signals { // TODO: handling of symlinks-to-directories isn't done here, // because I'm not sure how to do it and keep the performance ok... slotFilesRemoved(QUrl::fromStringList(fileList)); } void KCoreDirListerCache::slotFilesRemoved(const QList &fileList) { qCDebug(KIO_CORE_DIRLISTER) << fileList.count(); // Group notifications by parent dirs (usually there would be only one parent dir) QMap removedItemsByDir; QList deletedSubdirs; - for (auto it = fileList.cbegin(), cend = fileList.end(); it != cend; ++it) { - QUrl url(*it); + for (const QUrl &url : fileList) { DirItem *dirItem = dirItemForUrl(url); // is it a listed directory? if (dirItem) { deletedSubdirs.append(url); if (!dirItem->rootItem.isNull()) { removedItemsByDir[url].append(dirItem->rootItem); } } const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); dirItem = dirItemForUrl(parentDir); if (!dirItem) { continue; } for (auto fit = dirItem->lstItems.begin(), fend = dirItem->lstItems.end(); fit != fend; ++fit) { if ((*fit).url() == url) { const KFileItem fileitem = *fit; removedItemsByDir[parentDir].append(fileitem); // If we found a fileitem, we can test if it's a dir. If not, we'll go to deleteDir just in case. if (fileitem.isNull() || fileitem.isDir()) { deletedSubdirs.append(url); } dirItem->lstItems.erase(fit); // remove fileitem from list break; } } } for (auto rit = removedItemsByDir.constBegin(), cend = removedItemsByDir.constEnd(); rit != cend; ++rit) { // Tell the views about it before calling deleteDir. // They might need the subdirs' file items (see the dirtree). auto dit = directoryData.constFind(rit.key()); if (dit != directoryData.constEnd()) { itemsDeleted((*dit).listersCurrentlyHolding, rit.value()); } } for (const QUrl &url : qAsConst(deletedSubdirs)) { // in case of a dir, check if we have any known children, there's much to do in that case // (stopping jobs, removing dirs from cache etc.) deleteDir(url); } } void KCoreDirListerCache::slotFilesChanged(const QStringList &fileList) // from KDirNotify signals { qCDebug(KIO_CORE_DIRLISTER) << fileList; QList dirsToUpdate; - QStringList::const_iterator it = fileList.begin(); - for (; it != fileList.end(); ++it) { - QUrl url(*it); + for (const QString &fileUrl : fileList) { + const QUrl url(fileUrl); const KFileItem &fileitem = findByUrl(nullptr, url); if (fileitem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << "item not found for" << url; continue; } if (url.isLocalFile()) { pendingUpdates.insert(url.toLocalFile()); // delegate the work to processPendingUpdates } else { pendingRemoteUpdates.insert(fileitem); // For remote files, we won't be able to figure out the new information, // we have to do a update (directory listing) const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); if (!dirsToUpdate.contains(dir)) { dirsToUpdate.prepend(dir); } } } - QList::const_iterator itdir = dirsToUpdate.constBegin(); - for (; itdir != dirsToUpdate.constEnd(); ++itdir) { - updateDirectory(*itdir); + for (const QUrl &dirUrl : qAsConst(dirsToUpdate)) { + updateDirectory(dirUrl); } // ## TODO problems with current jobs listing/updating that dir // ( see kde-2.2.2's kdirlister ) processPendingUpdates(); } void KCoreDirListerCache::slotFileRenamed(const QString &_src, const QString &_dst, const QString &dstPath) // from KDirNotify signals { QUrl src(_src); QUrl dst(_dst); qCDebug(KIO_CORE_DIRLISTER) << src << "->" << dst; #ifdef DEBUG_CACHE printDebug(); #endif QUrl oldurl = src.adjusted(QUrl::StripTrailingSlash); KFileItem fileitem = findByUrl(nullptr, oldurl); if (fileitem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << "Item not found:" << oldurl; return; } const KFileItem oldItem = fileitem; // Dest already exists? Was overwritten then (testcase: #151851) // We better emit it as deleted -before- doing the renaming, otherwise // the "update" mechanism will emit the old one as deleted and // kdirmodel will delete the new (renamed) one! const KFileItem &existingDestItem = findByUrl(nullptr, dst); if (!existingDestItem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << dst << "already existed, let's delete it"; slotFilesRemoved(QList() << dst); } // If the item had a UDS_URL as well as UDS_NAME set, the user probably wants // to be updating the name only (since they can't see the URL). // Check to see if a URL exists, and if so, if only the file part has changed, // only update the name and not the underlying URL. bool nameOnly = !fileitem.entry().stringValue(KIO::UDSEntry::UDS_URL).isEmpty(); nameOnly = nameOnly && src.adjusted(QUrl::RemoveFilename) == dst.adjusted(QUrl::RemoveFilename); if (!nameOnly && fileitem.isDir()) { renameDir(oldurl, dst); // #172945 - if the fileitem was the root item of a DirItem that was just removed from the cache, // then it's a dangling pointer now... fileitem = findByUrl(nullptr, oldurl); if (fileitem.isNull()) { //deleted from cache altogether, #188807 return; } } // Now update the KFileItem representing that file or dir (not exclusive with the above!) if (!oldItem.isLocalFile() && !oldItem.localPath().isEmpty() && dstPath.isEmpty()) { // it uses UDS_LOCAL_PATH and we don't know the new path? needs an update then slotFilesChanged(QStringList() << src.toString()); } else { const QUrl &itemOldUrl = fileitem.url(); if (nameOnly) { fileitem.setName(dst.fileName()); } else { fileitem.setUrl(dst); } if (!dstPath.isEmpty()) { fileitem.setLocalPath(dstPath); } fileitem.refreshMimeType(); fileitem.determineMimeType(); reinsert(fileitem, itemOldUrl); const QSet listers = emitRefreshItem(oldItem, fileitem); for (KCoreDirLister *kdl : listers) { kdl->d->emitItems(); } } #ifdef DEBUG_CACHE printDebug(); #endif } QSet KCoreDirListerCache::emitRefreshItem(const KFileItem &oldItem, const KFileItem &fileitem) { qCDebug(KIO_CORE_DIRLISTER) << "old:" << oldItem.name() << oldItem.url() << "new:" << fileitem.name() << fileitem.url(); // Look whether this item was shown in any view, i.e. held by any dirlister const QUrl parentDir = oldItem.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); DirectoryDataHash::iterator dit = directoryData.find(parentDir); QList listers; // Also look in listersCurrentlyListing, in case the user manages to rename during a listing if (dit != directoryData.end()) { listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing; } if (oldItem.isDir()) { // For a directory, look for dirlisters where it's the root item. dit = directoryData.find(oldItem.url()); if (dit != directoryData.end()) { listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing; } } QSet listersToRefresh; for (KCoreDirLister *kdl : qAsConst(listers)) { // For a directory, look for dirlisters where it's the root item. QUrl directoryUrl(oldItem.url()); if (oldItem.isDir() && kdl->d->rootFileItem == oldItem) { const KFileItem oldRootItem = kdl->d->rootFileItem; kdl->d->rootFileItem = fileitem; kdl->d->addRefreshItem(directoryUrl, oldRootItem, fileitem); } else { directoryUrl = directoryUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); kdl->d->addRefreshItem(directoryUrl, oldItem, fileitem); } listersToRefresh.insert(kdl); } return listersToRefresh; } QList KCoreDirListerCache::directoriesForCanonicalPath(const QUrl &dir) const { QList dirs; dirs << dir; dirs << canonicalUrls.value(dir).toSet().toList(); /* make unique; there are faster ways, but this is really small anyway */ if (dirs.count() > 1) { qCDebug(KIO_CORE_DIRLISTER) << dir << "known as" << dirs; } return dirs; } // private slots // Called by KDirWatch - usually when a dir we're watching has been modified, // but it can also be called for a file. void KCoreDirListerCache::slotFileDirty(const QString &path) { qCDebug(KIO_CORE_DIRLISTER) << path; QUrl url = QUrl::fromLocalFile(path).adjusted(QUrl::StripTrailingSlash); // File or dir? bool isDir; const KFileItem item = itemForUrl(url); if (!item.isNull()) { isDir = item.isDir(); } else { QFileInfo info(path); if (!info.exists()) { return; // error } isDir = info.isDir(); } if (isDir) { const QList urls = directoriesForCanonicalPath(url); for (const QUrl &dir : urls) { handleFileDirty(dir); // e.g. for permission changes handleDirDirty(dir); } } else { const QList urls = directoriesForCanonicalPath(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); for (const QUrl &dir : urls) { QUrl aliasUrl(dir); aliasUrl.setPath(concatPaths(aliasUrl.path(), url.fileName())); handleFileDirty(aliasUrl); } } } // Called by slotFileDirty void KCoreDirListerCache::handleDirDirty(const QUrl &url) { // A dir: launch an update job if anyone cares about it // This also means we can forget about pending updates to individual files in that dir const QString dir = url.toLocalFile(); QString dirPath = dir; if (!dirPath.endsWith(QLatin1Char('/'))) { dirPath += QLatin1Char('/'); } QMutableSetIterator pendingIt(pendingUpdates); while (pendingIt.hasNext()) { const QString updPath = pendingIt.next(); qCDebug(KIO_CORE_DIRLISTER) << "had pending update" << updPath; if (updPath.startsWith(dirPath) && updPath.indexOf(QLatin1Char('/'), dirPath.length()) == -1) { // direct child item qCDebug(KIO_CORE_DIRLISTER) << "forgetting about individual update to" << updPath; pendingIt.remove(); } } if (checkUpdate(url) && !pendingDirectoryUpdates.contains(dir)) { pendingDirectoryUpdates.insert(dir); if (!pendingUpdateTimer.isActive()) { pendingUpdateTimer.start(200); } } } // Called by slotFileDirty void KCoreDirListerCache::handleFileDirty(const QUrl &url) { // A file: do we know about it already? const KFileItem &existingItem = findByUrl(nullptr, url); const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); QString filePath = url.toLocalFile(); if (existingItem.isNull()) { // No - update the parent dir then handleDirDirty(dir); } // Delay updating the file, FAM is flooding us with events if (checkUpdate(dir) && !pendingUpdates.contains(filePath)) { pendingUpdates.insert(filePath); if (!pendingUpdateTimer.isActive()) { pendingUpdateTimer.start(200); } } } void KCoreDirListerCache::slotFileCreated(const QString &path) // from KDirWatch { qCDebug(KIO_CORE_DIRLISTER) << path; // XXX: how to avoid a complete rescan here? // We'd need to stat that one file separately and refresh the item(s) for it. QUrl fileUrl(QUrl::fromLocalFile(path)); itemsAddedInDirectory(fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); } void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch { qCDebug(KIO_CORE_DIRLISTER) << path; const QString fileName = QFileInfo(path).fileName(); QUrl dirUrl(QUrl::fromLocalFile(path)); QStringList fileUrls; const QList urls = directoriesForCanonicalPath(dirUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); for (const QUrl &url : urls) { QUrl urlInfo(url); urlInfo.setPath(concatPaths(urlInfo.path(), fileName)); fileUrls << urlInfo.toString(); } slotFilesRemoved(fileUrls); } void KCoreDirListerCache::slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries) { QUrl url(joburl(static_cast(job))); url = url.adjusted(QUrl::StripTrailingSlash); qCDebug(KIO_CORE_DIRLISTER) << "new entries for " << url; DirItem *dir = itemsInUse.value(url); if (!dir) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but itemsInUse only knows about" << itemsInUse.keys(); Q_ASSERT(dir); return; } DirectoryDataHash::iterator dit = directoryData.find(url); if (dit == directoryData.end()) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData doesn't know about that url, only about:" << directoryData.keys(); Q_ASSERT(dit != directoryData.end()); return; } KCoreDirListerCacheDirectoryData &dirData = *dit; const QList listers = dirData.listersCurrentlyListing; if (listers.isEmpty()) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData says no listers are currently listing " << url; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(!listers.isEmpty()); return; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; for (const KCoreDirLister *kdl : listers) { delayedMimeTypes &= kdl->d->delayedMimeTypes; } QSet filesToHide; bool dotHiddenChecked = false; KIO::UDSEntryList::const_iterator it = entries.begin(); const KIO::UDSEntryList::const_iterator end = entries.end(); for (; it != end; ++it) { const QString name = (*it).stringValue(KIO::UDSEntry::UDS_NAME); Q_ASSERT(!name.isEmpty()); if (name.isEmpty()) { continue; } if (name == QLatin1Char('.')) { Q_ASSERT(dir->rootItem.isNull()); // Try to reuse an existing KFileItem (if we listed the parent dir) // rather than creating a new one. There are many reasons: // 1) renames and permission changes to the item would have to emit the signals // twice, otherwise, so that both views manage to recognize the item. // 2) with kio_ftp we can only know that something is a symlink when // listing the parent, so prefer that item, which has more info. // Note that it gives a funky name() to the root item, rather than "." ;) dir->rootItem = itemForUrl(url); if (dir->rootItem.isNull()) { dir->rootItem = KFileItem(*it, url, delayedMimeTypes, true); } for (KCoreDirLister *kdl : listers) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == url) { kdl->d->rootFileItem = dir->rootItem; } } } else if (name != QLatin1String("..")) { KFileItem item(*it, url, delayedMimeTypes, true); // get the names of the files listed in ".hidden", if it exists and is a local file if (!dotHiddenChecked) { const QString localPath = item.localPath(); if (!localPath.isEmpty()) { const QString rootItemPath = QFileInfo(localPath).absolutePath(); filesToHide = filesInDotHiddenForDir(rootItemPath); } dotHiddenChecked = true; } // hide file if listed in ".hidden" if (filesToHide.contains(name)) { item.setHidden(); } qCDebug(KIO_CORE_DIRLISTER)<< "Adding item: " << item.url(); // Add the items sorted by url, needed by findByUrl dir->insert(item); for (KCoreDirLister *kdl : listers) { kdl->d->addNewItem(url, item); } } } for (KCoreDirLister *kdl : listers) { kdl->d->emitItems(); } } void KCoreDirListerCache::slotResult(KJob *j) { #ifdef DEBUG_CACHE //printDebug(); #endif Q_ASSERT(j); KIO::ListJob *job = static_cast(j); runningListJobs.remove(job); QUrl jobUrl(joburl(job)); jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections qCDebug(KIO_CORE_DIRLISTER) << "finished listing" << jobUrl; const auto dit = directoryData.find(jobUrl); if (dit == directoryData.end()) { qCWarning(KIO_CORE) << "Nothing found in directoryData for URL" << jobUrl; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dit != directoryData.end()); return; } KCoreDirListerCacheDirectoryData &dirData = *dit; if (dirData.listersCurrentlyListing.isEmpty()) { qCWarning(KIO_CORE) << "OOOOPS, nothing in directoryData.listersCurrentlyListing for" << jobUrl; // We're about to assert; dump the current state... #ifndef NDEBUG printDebug(); #endif Q_ASSERT(!dirData.listersCurrentlyListing.isEmpty()); } const QList listers = dirData.listersCurrentlyListing; // move all listers to the holding list, do it before emitting // the signals to make sure it exists in KCoreDirListerCache in case someone // calls listDir during the signal emission Q_ASSERT(dirData.listersCurrentlyHolding.isEmpty()); dirData.moveListersWithoutCachedItemsJob(jobUrl); if (job->error()) { for (KCoreDirLister *kdl : listers) { kdl->d->jobDone(job); if (job->error() != KJob::KilledJobError) { kdl->handleError(job); } const bool silent = job->property("_kdlc_silent").toBool(); if (!silent) { emit kdl->canceled(jobUrl); } if (kdl->d->numJobs() == 0) { kdl->d->complete = true; if (!silent) { emit kdl->canceled(); } } } } else { DirItem *dir = itemsInUse.value(jobUrl); Q_ASSERT(dir); dir->complete = true; for (KCoreDirLister *kdl : listers) { kdl->d->jobDone(job); emit kdl->completed(jobUrl); if (kdl->d->numJobs() == 0) { kdl->d->complete = true; emit kdl->completed(); } } } // TODO: hmm, if there was an error and job is a parent of one or more // of the pending urls we should cancel it/them as well processPendingUpdates(); if (job->property("need_another_update").toBool()) { updateDirectory(jobUrl); } #ifdef DEBUG_CACHE printDebug(); #endif } void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url) { Q_ASSERT(j); KIO::ListJob *job = static_cast(j); QUrl oldUrl(job->url()); // here we really need the old url! QUrl newUrl(url); // strip trailing slashes oldUrl = oldUrl.adjusted(QUrl::StripTrailingSlash); newUrl = newUrl.adjusted(QUrl::StripTrailingSlash); if (oldUrl == newUrl) { qCDebug(KIO_CORE_DIRLISTER) << "New redirection url same as old, giving up."; return; } else if (newUrl.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "New redirection url is empty, giving up."; return; } qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl; #ifdef DEBUG_CACHE // Can't do that here. KCoreDirListerCache::joburl() will use the new url already, // while our data structures haven't been updated yet -> assert fail. //printDebug(); #endif // I don't think there can be dirItems that are children of oldUrl. // Am I wrong here? And even if so, we don't need to delete them, right? // DF: redirection happens before listDir emits any item. Makes little sense otherwise. // oldUrl cannot be in itemsCached because only completed items are moved there DirItem *dir = itemsInUse.take(oldUrl); Q_ASSERT(dir); DirectoryDataHash::iterator dit = directoryData.find(oldUrl); Q_ASSERT(dit != directoryData.end()); KCoreDirListerCacheDirectoryData oldDirData = *dit; directoryData.erase(dit); Q_ASSERT(!oldDirData.listersCurrentlyListing.isEmpty()); const QList listers = oldDirData.listersCurrentlyListing; Q_ASSERT(!listers.isEmpty()); for (KCoreDirLister *kdl : listers) { kdl->d->redirect(oldUrl, newUrl, false /*clear items*/); } // when a lister was stopped before the job emits the redirection signal, the old url will // also be in listersCurrentlyHolding const QList holders = oldDirData.listersCurrentlyHolding; for (KCoreDirLister *kdl : holders) { kdl->jobStarted(job); // do it like when starting a new list-job that will redirect later // TODO: maybe don't emit started if there's an update running for newUrl already? emit kdl->started(oldUrl); kdl->d->redirect(oldUrl, newUrl, false /*clear items*/); } const QList allListers = listers + holders; DirItem *newDir = itemsInUse.value(newUrl); if (newDir) { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "already in use"; // only in this case there can newUrl already be in listersCurrentlyListing or listersCurrentlyHolding delete dir; // get the job if one's running for newUrl already (can be a list-job or an update-job), but // do not return this 'job', which would happen because of the use of redirectionURL() KIO::ListJob *oldJob = jobForUrl(newUrl, job); // listers of newUrl with oldJob: forget about the oldJob and use the already running one // which will be converted to an updateJob KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; QList &curListers = newDirData.listersCurrentlyListing; if (!curListers.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "and it is currently listed"; Q_ASSERT(oldJob); // ?! for (KCoreDirLister *kdl : qAsConst(curListers)) { // listers of newUrl kdl->d->jobDone(oldJob); kdl->jobStarted(job); kdl->d->connectJob(job); } // append listers of oldUrl with newJob to listers of newUrl with oldJob for (KCoreDirLister *kdl : listers) { curListers.append(kdl); } } else { curListers = listers; } if (oldJob) { // kill the old job, be it a list-job or an update-job killJob(oldJob); } // holders of newUrl: use the already running job which will be converted to an updateJob QList &curHolders = newDirData.listersCurrentlyHolding; if (!curHolders.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "and it is currently held."; for (KCoreDirLister *kdl : qAsConst(curHolders)) { // holders of newUrl kdl->jobStarted(job); emit kdl->started(newUrl); } // append holders of oldUrl to holders of newUrl for (KCoreDirLister *kdl : holders) { curHolders.append(kdl); } } else { curHolders = holders; } // emit old items: listers, holders. NOT: newUrlListers/newUrlHolders, they already have them listed // TODO: make this a separate method? for (KCoreDirLister *kdl : allListers) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) { kdl->d->rootFileItem = newDir->rootItem; } kdl->d->addNewItems(newUrl, newDir->lstItems); kdl->d->emitItems(); } } else if ((newDir = itemsCached.take(newUrl))) { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "is unused, but already in the cache."; delete dir; itemsInUse.insert(newUrl, newDir); KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; newDirData.listersCurrentlyListing = listers; newDirData.listersCurrentlyHolding = holders; // emit old items: listers, holders for (KCoreDirLister *kdl : allListers) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) { kdl->d->rootFileItem = newDir->rootItem; } kdl->d->addNewItems(newUrl, newDir->lstItems); kdl->d->emitItems(); } } else { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "has not been listed yet."; dir->rootItem = KFileItem(); dir->lstItems.clear(); dir->redirect(newUrl); itemsInUse.insert(newUrl, dir); KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; newDirData.listersCurrentlyListing = listers; newDirData.listersCurrentlyHolding = holders; if (holders.isEmpty()) { #ifdef DEBUG_CACHE printDebug(); #endif return; // only in this case the job doesn't need to be converted, } } // make the job an update job job->disconnect(this); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult); // FIXME: autoUpdate-Counts!! #ifdef DEBUG_CACHE printDebug(); #endif } struct KCoreDirListerCache::ItemInUseChange { ItemInUseChange(const QUrl &old, const QUrl &newU, DirItem *di) : oldUrl(old), newUrl(newU), dirItem(di) {} QUrl oldUrl; QUrl newUrl; DirItem *dirItem; }; void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl) { qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl; //const QString oldUrlStr = oldUrl.url(KUrl::RemoveTrailingSlash); //const QString newUrlStr = newUrl.url(KUrl::RemoveTrailingSlash); // Not enough. Also need to look at any child dir, even sub-sub-sub-dir. //DirItem *dir = itemsInUse.take( oldUrlStr ); //emitRedirections( oldUrl, url ); QLinkedList itemsToChange; QSet listers; // Look at all dirs being listed/shown for (auto itu = itemsInUse.begin(), ituend = itemsInUse.end(); itu != ituend; ++itu) { DirItem *dir = itu.value(); QUrl oldDirUrl(itu.key()); qCDebug(KIO_CORE_DIRLISTER) << "itemInUse:" << oldDirUrl; // Check if this dir is oldUrl, or a subfolder of it if (oldDirUrl == oldUrl || oldUrl.isParentOf(oldDirUrl)) { // TODO should use KUrl::cleanpath like isParentOf does QString relPath = oldDirUrl.path().mid(oldUrl.path().length()+1); QUrl newDirUrl(newUrl); // take new base if (!relPath.isEmpty()) { newDirUrl.setPath(concatPaths(newDirUrl.path(), relPath)); // add unchanged relative path } qCDebug(KIO_CORE_DIRLISTER) << "new url=" << newDirUrl; // Update URL in dir item and in itemsInUse dir->redirect(newDirUrl); itemsToChange.append(ItemInUseChange(oldDirUrl.adjusted(QUrl::StripTrailingSlash), newDirUrl.adjusted(QUrl::StripTrailingSlash), dir)); // Rename all items under that dir // If all items of the directory change the same part of their url, the order is not // changed, therefore just change it in the list. - for (auto kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend; ++kit) { - const KFileItem oldItem = *kit; + for (KFileItem &item : dir->lstItems) { + const KFileItem oldItem = item; KFileItem newItem = oldItem; const QUrl &oldItemUrl = oldItem.url(); QUrl newItemUrl(oldItemUrl); newItemUrl.setPath(concatPaths(newDirUrl.path(), oldItemUrl.fileName())); qCDebug(KIO_CORE_DIRLISTER) << "renaming" << oldItemUrl << "to" << newItemUrl; newItem.setUrl(newItemUrl); listers |= emitRefreshItem(oldItem, newItem); // Change the item - kit->setUrl(newItemUrl); + item.setUrl(newItemUrl); } } } for (KCoreDirLister *kdl : qAsConst(listers)) { kdl->d->emitItems(); } // Do the changes to itemsInUse out of the loop to avoid messing up iterators, // and so that emitRefreshItem can find the stuff in the hash. for (const ItemInUseChange &i : qAsConst(itemsToChange)) { itemsInUse.remove(i.oldUrl); itemsInUse.insert(i.newUrl, i.dirItem); } //Now that all the caches are updated and consistent, emit the redirection. for (const ItemInUseChange& i : qAsConst(itemsToChange)) { emitRedirections(QUrl(i.oldUrl), QUrl(i.newUrl)); } // Is oldUrl a directory in the cache? // Remove any child of oldUrl from the cache - even if the renamed dir itself isn't in it! removeDirFromCache(oldUrl); // TODO rename, instead. } // helper for renameDir, not used for redirections from KIO::listDir(). void KCoreDirListerCache::emitRedirections(const QUrl &_oldUrl, const QUrl &_newUrl) { qCDebug(KIO_CORE_DIRLISTER) << _oldUrl << "->" << _newUrl; const QUrl oldUrl = _oldUrl.adjusted(QUrl::StripTrailingSlash); const QUrl newUrl = _newUrl.adjusted(QUrl::StripTrailingSlash); KIO::ListJob *job = jobForUrl(oldUrl); if (job) { killJob(job); } // Check if we were listing this dir. Need to abort and restart with new name in that case. DirectoryDataHash::iterator dit = directoryData.find(oldUrl); if (dit == directoryData.end()) { return; } const QList listers = (*dit).listersCurrentlyListing; const QList holders = (*dit).listersCurrentlyHolding; KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; // Tell the world that the job listing the old url is dead. for (KCoreDirLister *kdl : listers) { if (job) { kdl->d->jobDone(job); } emit kdl->canceled(oldUrl); } newDirData.listersCurrentlyListing += listers; // Check if we are currently displaying this directory (odds opposite wrt above) for (KCoreDirLister *kdl : holders) { if (job) { kdl->d->jobDone(job); } } newDirData.listersCurrentlyHolding += holders; directoryData.erase(dit); if (!listers.isEmpty()) { updateDirectory(newUrl); // Tell the world about the new url for (KCoreDirLister *kdl : listers) { emit kdl->started(newUrl); } } // And notify the dirlisters of the redirection for (KCoreDirLister *kdl : holders) { kdl->d->redirect(oldUrl, newUrl, true /*keep items*/); } } void KCoreDirListerCache::removeDirFromCache(const QUrl &dir) { qCDebug(KIO_CORE_DIRLISTER) << dir; const QList cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator... for (const QUrl &cachedDir : cachedDirs) { if (dir == cachedDir || dir.isParentOf(cachedDir)) { itemsCached.remove(cachedDir); } } } void KCoreDirListerCache::slotUpdateEntries(KIO::Job *job, const KIO::UDSEntryList &list) { runningListJobs[static_cast(job)] += list; } void KCoreDirListerCache::slotUpdateResult(KJob *j) { Q_ASSERT(j); KIO::ListJob *job = static_cast(j); QUrl jobUrl(joburl(job)); jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections qCDebug(KIO_CORE_DIRLISTER) << "finished update" << jobUrl; KCoreDirListerCacheDirectoryData &dirData = directoryData[jobUrl]; // Collect the dirlisters which were listing the URL using that ListJob // plus those that were already holding that URL - they all get updated. dirData.moveListersWithoutCachedItemsJob(jobUrl); const QList listers = dirData.listersCurrentlyHolding + dirData.listersCurrentlyListing; // once we are updating dirs that are only in the cache this will fail! Q_ASSERT(!listers.isEmpty()); if (job->error()) { for (KCoreDirLister *kdl : listers) { kdl->d->jobDone(job); //don't bother the user //kdl->handleError( job ); const bool silent = job->property("_kdlc_silent").toBool(); if (!silent) { emit kdl->canceled(jobUrl); } if (kdl->d->numJobs() == 0) { kdl->d->complete = true; if (!silent) { emit kdl->canceled(); } } } runningListJobs.remove(job); // TODO: if job is a parent of one or more // of the pending urls we should cancel them processPendingUpdates(); return; } DirItem *dir = itemsInUse.value(jobUrl, nullptr); if (!dir) { qCWarning(KIO_CORE) << "Internal error: itemsInUse did not contain" << jobUrl; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dir); } else { dir->complete = true; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; for (const KCoreDirLister *kdl : listers) { delayedMimeTypes &= kdl->d->delayedMimeTypes; } typedef QHash FileItemHash; // fileName -> KFileItem FileItemHash fileItems; // Fill the hash from the old list of items. We'll remove entries as we see them // in the new listing, and the resulting hash entries will be the deleted items. - for (auto kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend; ++kit) { - fileItems.insert((*kit).name(), *kit); + for (const KFileItem &item : qAsConst(dir->lstItems)) { + fileItems.insert(item.name(), item); } QSet filesToHide; bool dotHiddenChecked = false; const KIO::UDSEntryList &buf = runningListJobs.value(job); KIO::UDSEntryList::const_iterator it = buf.constBegin(); const KIO::UDSEntryList::const_iterator end = buf.constEnd(); for (; it != end; ++it) { // Form the complete url KFileItem item(*it, jobUrl, delayedMimeTypes, true); const QString name = item.name(); Q_ASSERT(!name.isEmpty()); // A kioslave setting an empty UDS_NAME is utterly broken, fix the kioslave! // we duplicate the check for dotdot here, to avoid iterating over // all items again and checking in matchesFilter() that way. if (name.isEmpty() || name == QLatin1String("..")) { continue; } if (name == QLatin1Char('.')) { // if the update was started before finishing the original listing // there is no root item yet if (dir->rootItem.isNull()) { dir->rootItem = item; for (KCoreDirLister *kdl : listers) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == jobUrl) { kdl->d->rootFileItem = dir->rootItem; } } } continue; } else { // get the names of the files listed in ".hidden", if it exists and is a local file if (!dotHiddenChecked) { const QString localPath = item.localPath(); if (!localPath.isEmpty()) { const QString rootItemPath = QFileInfo(localPath).absolutePath(); filesToHide = filesInDotHiddenForDir(rootItemPath); } dotHiddenChecked = true; } } // hide file if listed in ".hidden" if (filesToHide.contains(name)) { item.setHidden(); } // Find this item FileItemHash::iterator fiit = fileItems.find(item.name()); if (fiit != fileItems.end()) { const KFileItem tmp = fiit.value(); QSet::iterator pru_it = pendingRemoteUpdates.find(tmp); const bool inPendingRemoteUpdates = (pru_it != pendingRemoteUpdates.end()); // check if something changed for this file, using KFileItem::cmp() if (!tmp.cmp(item) || inPendingRemoteUpdates) { if (inPendingRemoteUpdates) { pendingRemoteUpdates.erase(pru_it); } qCDebug(KIO_CORE_DIRLISTER) << "file changed:" << tmp.name(); reinsert(item, tmp.url()); for (KCoreDirLister *kdl : listers) { kdl->d->addRefreshItem(jobUrl, tmp, item); } } // Seen, remove fileItems.erase(fiit); } else { // this is a new file qCDebug(KIO_CORE_DIRLISTER) << "new file:" << name; dir->insert(item); for (KCoreDirLister *kdl : listers) { kdl->d->addNewItem(jobUrl, item); } } } runningListJobs.remove(job); if (!fileItems.isEmpty()) { deleteUnmarkedItems(listers, dir->lstItems, fileItems); } for (KCoreDirLister *kdl : listers) { kdl->d->emitItems(); kdl->d->jobDone(job); emit kdl->completed(jobUrl); if (kdl->d->numJobs() == 0) { kdl->d->complete = true; emit kdl->completed(); } } // TODO: hmm, if there was an error and job is a parent of one or more // of the pending urls we should cancel it/them as well processPendingUpdates(); if (job->property("need_another_update").toBool()) { updateDirectory(jobUrl); } } // private KIO::ListJob *KCoreDirListerCache::jobForUrl(const QUrl &url, KIO::ListJob *not_job) { auto it = runningListJobs.constBegin(); while (it != runningListJobs.constEnd()) { KIO::ListJob *job = it.key(); const QUrl jobUrl = joburl(job).adjusted(QUrl::StripTrailingSlash); if (jobUrl == url && job != not_job) { return job; } ++it; } return nullptr; } const QUrl &KCoreDirListerCache::joburl(KIO::ListJob *job) { if (job->redirectionUrl().isValid()) { return job->redirectionUrl(); } else { return job->url(); } } void KCoreDirListerCache::killJob(KIO::ListJob *job) { runningListJobs.remove(job); job->disconnect(this); job->kill(); } void KCoreDirListerCache::deleteUnmarkedItems(const QList &listers, QList &lstItems, const QHash &itemsToDelete) { // Make list of deleted items (for emitting) KFileItemList deletedItems; QHashIterator kit(itemsToDelete); while (kit.hasNext()) { const KFileItem item = kit.next().value(); deletedItems.append(item); qCDebug(KIO_CORE_DIRLISTER) << "deleted:" << item.name() << item; } // Delete all remaining items QMutableListIterator it(lstItems); while (it.hasNext()) { if (itemsToDelete.contains(it.next().name())) { it.remove(); } } itemsDeleted(listers, deletedItems); } void KCoreDirListerCache::itemsDeleted(const QList &listers, const KFileItemList &deletedItems) { for (KCoreDirLister *kdl : listers) { kdl->d->emitItemsDeleted(deletedItems); } for (const KFileItem &item : deletedItems) { if (item.isDir()) { deleteDir(item.url()); } } } void KCoreDirListerCache::deleteDir(const QUrl &_dirUrl) { qCDebug(KIO_CORE_DIRLISTER) << _dirUrl; // unregister and remove the children of the deleted item. // Idea: tell all the KCoreDirListers that they should forget the dir // and then remove it from the cache. QUrl dirUrl(_dirUrl.adjusted(QUrl::StripTrailingSlash)); // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse) QList affectedItems; auto itu = itemsInUse.begin(); const auto ituend = itemsInUse.end(); for (; itu != ituend; ++itu) { const QUrl deletedUrl(itu.key()); if (dirUrl == deletedUrl || dirUrl.isParentOf(deletedUrl)) { affectedItems.append(deletedUrl); } } for (const QUrl &deletedUrl : qAsConst(affectedItems)) { // stop all jobs for deletedUrlStr DirectoryDataHash::iterator dit = directoryData.find(deletedUrl); if (dit != directoryData.end()) { // we need a copy because stop modifies the list const QList listers = (*dit).listersCurrentlyListing; for (KCoreDirLister *kdl : listers) { stopListingUrl(kdl, deletedUrl); } // tell listers holding deletedUrl to forget about it // this will stop running updates for deletedUrl as well // we need a copy because forgetDirs modifies the list const QList holders = (*dit).listersCurrentlyHolding; for (KCoreDirLister *kdl : holders) { // lister's root is the deleted item if (kdl->d->url == deletedUrl) { // tell the view first. It might need the subdirs' items (which forgetDirs will delete) if (!kdl->d->rootFileItem.isNull()) { emit kdl->itemsDeleted(KFileItemList() << kdl->d->rootFileItem); } forgetDirs(kdl); kdl->d->rootFileItem = KFileItem(); } else { const bool treeview = kdl->d->lstDirs.count() > 1; if (!treeview) { emit kdl->clear(); kdl->d->lstDirs.clear(); } else { kdl->d->lstDirs.removeAll(deletedUrl); } forgetDirs(kdl, deletedUrl, treeview); } } } // delete the entry for deletedUrl - should not be needed, it's in // items cached now int count = itemsInUse.remove(deletedUrl); Q_ASSERT(count == 0); Q_UNUSED(count); //keep gcc "unused variable" complaining quiet when in release mode } // remove the children from the cache removeDirFromCache(dirUrl); } // delayed updating of files, FAM is flooding us with events void KCoreDirListerCache::processPendingUpdates() { QSet listers; for (const QString &file : qAsConst(pendingUpdates)) { // always a local path qCDebug(KIO_CORE_DIRLISTER) << file; QUrl u = QUrl::fromLocalFile(file); KFileItem item = findByUrl(nullptr, u); // search all items if (!item.isNull()) { // we need to refresh the item, because e.g. the permissions can have changed. KFileItem oldItem = item; item.refresh(); if (!oldItem.cmp(item)) { reinsert(item, oldItem.url()); listers |= emitRefreshItem(oldItem, item); } } } pendingUpdates.clear(); for (KCoreDirLister *kdl : qAsConst(listers)) { kdl->d->emitItems(); } // Directories in need of updating for (const QString &dir : qAsConst(pendingDirectoryUpdates)) { updateDirectory(QUrl::fromLocalFile(dir)); } pendingDirectoryUpdates.clear(); } #ifndef NDEBUG void KCoreDirListerCache::printDebug() { qCDebug(KIO_CORE_DIRLISTER) << "Items in use:"; auto itu = itemsInUse.constBegin(); const auto ituend = itemsInUse.constEnd(); for (; itu != ituend; ++itu) { qCDebug(KIO_CORE_DIRLISTER) << " " << itu.key() << "URL:" << itu.value()->url << "rootItem:" << (!itu.value()->rootItem.isNull() ? itu.value()->rootItem.url() : QUrl()) << "autoUpdates refcount:" << itu.value()->autoUpdates << "complete:" << itu.value()->complete << QStringLiteral("with %1 items.").arg(itu.value()->lstItems.count()); } QList listersWithoutJob; qCDebug(KIO_CORE_DIRLISTER) << "Directory data:"; DirectoryDataHash::const_iterator dit = directoryData.constBegin(); for (; dit != directoryData.constEnd(); ++dit) { QString list; const QList listers = (*dit).listersCurrentlyListing; for (KCoreDirLister *listit : listers) { list += QLatin1String(" 0x") + QString::number(reinterpret_cast(listit), 16); } qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << listers.count() << "listers:" << list; for (KCoreDirLister *listit : listers) { if (!listit->d->m_cachedItemsJobs.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has CachedItemsJobs" << listit->d->m_cachedItemsJobs; } else if (KIO::ListJob *listJob = jobForUrl(dit.key())) { qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has ListJob" << listJob; } else { listersWithoutJob.append(listit); } } list.clear(); const QList holders = (*dit).listersCurrentlyHolding; for (KCoreDirLister *listit : holders) { list += QLatin1String(" 0x") + QString::number(reinterpret_cast(listit), 16); } qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << holders.count() << "holders:" << list; } QMap< KIO::ListJob *, KIO::UDSEntryList >::Iterator jit = runningListJobs.begin(); qCDebug(KIO_CORE_DIRLISTER) << "Jobs:"; for (; jit != runningListJobs.end(); ++jit) { qCDebug(KIO_CORE_DIRLISTER) << " " << jit.key() << "listing" << joburl(jit.key()) << ":" << (*jit).count() << "entries."; } qCDebug(KIO_CORE_DIRLISTER) << "Items in cache:"; const QList cachedDirs = itemsCached.keys(); for (const QUrl &cachedDir : cachedDirs) { DirItem *dirItem = itemsCached.object(cachedDir); qCDebug(KIO_CORE_DIRLISTER) << " " << cachedDir << "rootItem:" << (!dirItem->rootItem.isNull() ? dirItem->rootItem.url().toString() : QStringLiteral("NULL")) << "with" << dirItem->lstItems.count() << "items."; } // Abort on listers without jobs -after- showing the full dump. Easier debugging. for (KCoreDirLister *listit : qAsConst(listersWithoutJob)) { qCWarning(KIO_CORE) << "Fatal Error: HUH? Lister" << listit << "is supposed to be listing, but has no job!"; abort(); } } #endif KCoreDirLister::KCoreDirLister(QObject *parent) : QObject(parent), d(new Private(this)) { qCDebug(KIO_CORE_DIRLISTER) << "+KCoreDirLister"; d->complete = true; setAutoUpdate(true); setDirOnlyMode(false); setShowingDotFiles(false); } KCoreDirLister::~KCoreDirLister() { qCDebug(KIO_CORE_DIRLISTER) << "~KCoreDirLister" << this; // Stop all running jobs, remove lister from lists if (!kDirListerCache.isDestroyed()) { stop(); kDirListerCache()->forgetDirs(this); } delete d; } bool KCoreDirLister::openUrl(const QUrl &_url, OpenUrlFlags _flags) { // emit the current changes made to avoid an inconsistent treeview if (d->hasPendingChanges && (_flags & Keep)) { emitChanges(); } d->hasPendingChanges = false; return kDirListerCache()->listDir(this, _url, _flags & Keep, _flags & Reload); } void KCoreDirLister::stop() { kDirListerCache()->stop(this); } void KCoreDirLister::stop(const QUrl &_url) { kDirListerCache()->stopListingUrl(this, _url); } bool KCoreDirLister::autoUpdate() const { return d->autoUpdate; } void KCoreDirLister::setAutoUpdate(bool _enable) { if (d->autoUpdate == _enable) { return; } d->autoUpdate = _enable; kDirListerCache()->setAutoUpdate(this, _enable); } bool KCoreDirLister::showingDotFiles() const { return d->settings.isShowingDotFiles; } void KCoreDirLister::setShowingDotFiles(bool _showDotFiles) { if (d->settings.isShowingDotFiles == _showDotFiles) { return; } d->prepareForSettingsChange(); d->settings.isShowingDotFiles = _showDotFiles; } bool KCoreDirLister::dirOnlyMode() const { return d->settings.dirOnlyMode; } void KCoreDirLister::setDirOnlyMode(bool _dirsOnly) { if (d->settings.dirOnlyMode == _dirsOnly) { return; } d->prepareForSettingsChange(); d->settings.dirOnlyMode = _dirsOnly; } QUrl KCoreDirLister::url() const { return d->url; } QList KCoreDirLister::directories() const { return d->lstDirs; } void KCoreDirLister::emitChanges() { d->emitChanges(); } void KCoreDirLister::Private::emitChanges() { if (!hasPendingChanges) { return; } // reset 'hasPendingChanges' now, in case of recursion // (testcase: enabling recursive scan in ktorrent, #174920) hasPendingChanges = false; const Private::FilterSettings newSettings = settings; settings = oldSettings; // temporarily // Fill hash with all items that are currently visible QSet oldVisibleItems; for (const QUrl &dir : qAsConst(lstDirs)) { const QList *itemList = kDirListerCache()->itemsForDir(dir); if (!itemList) { continue; } for (const KFileItem &item : *itemList) { if (isItemVisible(item) && m_parent->matchesMimeFilter(item)) { oldVisibleItems.insert(item.name()); } } } settings = newSettings; const QList dirs = lstDirs; for (const QUrl &dir : dirs) { KFileItemList deletedItems; const QList *itemList = kDirListerCache()->itemsForDir(dir); if (!itemList) { continue; } auto kit = itemList->begin(); const auto kend = itemList->end(); for (; kit != kend; ++kit) { const KFileItem &item = *kit; const QString text = item.text(); if (text == QLatin1Char('.') || text == QLatin1String("..")) { continue; } const bool wasVisible = oldVisibleItems.contains(item.name()); const bool nowVisible = isItemVisible(item) && m_parent->matchesMimeFilter(item); if (nowVisible && !wasVisible) { addNewItem(dir, item); // takes care of emitting newItem or itemsFilteredByMime } else if (!nowVisible && wasVisible) { deletedItems.append(*kit); } } if (!deletedItems.isEmpty()) { emit m_parent->itemsDeleted(deletedItems); } emitItems(); } oldSettings = settings; } void KCoreDirLister::updateDirectory(const QUrl &_u) { kDirListerCache()->updateDirectory(_u); } bool KCoreDirLister::isFinished() const { return d->complete; } KFileItem KCoreDirLister::rootItem() const { return d->rootFileItem; } KFileItem KCoreDirLister::findByUrl(const QUrl &_url) const { return kDirListerCache()->findByUrl(this, _url); } KFileItem KCoreDirLister::findByName(const QString &_name) const { return kDirListerCache()->findByName(this, _name); } // ================ public filter methods ================ // void KCoreDirLister::setNameFilter(const QString &nameFilter) { if (d->nameFilter == nameFilter) { return; } d->prepareForSettingsChange(); d->settings.lstFilters.clear(); d->nameFilter = nameFilter; // Split on white space const QStringList list = nameFilter.split(QLatin1Char(' '), QString::SkipEmptyParts); - for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it) { - d->settings.lstFilters.append(QRegExp(*it, Qt::CaseInsensitive, QRegExp::Wildcard)); + for (const QString &filter : list) { + d->settings.lstFilters.append(QRegExp(filter, Qt::CaseInsensitive, QRegExp::Wildcard)); } } QString KCoreDirLister::nameFilter() const { return d->nameFilter; } void KCoreDirLister::setMimeFilter(const QStringList &mimeFilter) { if (d->settings.mimeFilter == mimeFilter) { return; } d->prepareForSettingsChange(); if (mimeFilter.contains(QLatin1String("application/octet-stream")) || mimeFilter.contains(QLatin1String("all/allfiles"))) { // all files d->settings.mimeFilter.clear(); } else { d->settings.mimeFilter = mimeFilter; } } void KCoreDirLister::setMimeExcludeFilter(const QStringList &mimeExcludeFilter) { if (d->settings.mimeExcludeFilter == mimeExcludeFilter) { return; } d->prepareForSettingsChange(); d->settings.mimeExcludeFilter = mimeExcludeFilter; } void KCoreDirLister::clearMimeFilter() { d->prepareForSettingsChange(); d->settings.mimeFilter.clear(); d->settings.mimeExcludeFilter.clear(); } QStringList KCoreDirLister::mimeFilters() const { return d->settings.mimeFilter; } bool KCoreDirLister::matchesFilter(const QString &name) const { return doNameFilter(name, d->settings.lstFilters); } bool KCoreDirLister::matchesMimeFilter(const QString &mime) const { return doMimeFilter(mime, d->settings.mimeFilter) && d->doMimeExcludeFilter(mime, d->settings.mimeExcludeFilter); } // ================ protected methods ================ // bool KCoreDirLister::matchesFilter(const KFileItem &item) const { Q_ASSERT(!item.isNull()); if (item.text() == QLatin1String("..")) { return false; } if (!d->settings.isShowingDotFiles && item.isHidden()) { return false; } if (item.isDir() || d->settings.lstFilters.isEmpty()) { return true; } return matchesFilter(item.text()); } bool KCoreDirLister::matchesMimeFilter(const KFileItem &item) const { Q_ASSERT(!item.isNull()); // Don't lose time determining the mimetype if there is no filter if (d->settings.mimeFilter.isEmpty() && d->settings.mimeExcludeFilter.isEmpty()) { return true; } return matchesMimeFilter(item.mimetype()); } bool KCoreDirLister::doNameFilter(const QString &name, const QList &filters) const { - for (QList::const_iterator it = filters.begin(); it != filters.end(); ++it) - if ((*it).exactMatch(name)) { + for (const QRegExp &filter : filters) { + if (filter.exactMatch(name)) { return true; } + } return false; } bool KCoreDirLister::doMimeFilter(const QString &mime, const QStringList &filters) const { if (filters.isEmpty()) { return true; } QMimeDatabase db; const QMimeType mimeptr = db.mimeTypeForName(mime); if (!mimeptr.isValid()) { return false; } qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: investigating:" << mimeptr.name(); QStringList::const_iterator it = filters.begin(); for (; it != filters.end(); ++it) if (mimeptr.inherits(*it)) { return true; } //else qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: compared without result to "<<*it; return false; } bool KCoreDirLister::Private::doMimeExcludeFilter(const QString &mime, const QStringList &filters) const { if (filters.isEmpty()) { return true; } QStringList::const_iterator it = filters.begin(); for (; it != filters.end(); ++it) if ((*it) == mime) { return false; } return true; } void KCoreDirLister::handleError(KIO::Job *job) { qCWarning(KIO_CORE) << job->errorString(); } void KCoreDirLister::handleErrorMessage(const QString &message) { qCWarning(KIO_CORE) << message; } // ================= private methods ================= // void KCoreDirLister::Private::addNewItem(const QUrl &directoryUrl, const KFileItem &item) { if (!isItemVisible(item)) { return; // No reason to continue... bailing out here prevents a mimetype scan. } qCDebug(KIO_CORE_DIRLISTER) << "in" << directoryUrl << "item:" << item.url(); if (m_parent->matchesMimeFilter(item)) { Q_ASSERT(!item.isNull()); lstNewItems[directoryUrl].append(item); // items not filtered } else { Q_ASSERT(!item.isNull()); lstMimeFilteredItems.append(item); // only filtered by mime } } void KCoreDirLister::Private::addNewItems(const QUrl &directoryUrl, const QList &items) { // TODO: make this faster - test if we have a filter at all first // DF: was this profiled? The matchesFoo() functions should be fast, w/o filters... // Of course if there is no filter and we can do a range-insertion instead of a loop, that might be good. auto kit = items.cbegin(); const auto kend = items.cend(); for (; kit != kend; ++kit) { addNewItem(directoryUrl, *kit); } } void KCoreDirLister::Private::addRefreshItem(const QUrl &directoryUrl, const KFileItem &oldItem, const KFileItem &item) { const bool refreshItemWasFiltered = !isItemVisible(oldItem) || !m_parent->matchesMimeFilter(oldItem); if (isItemVisible(item) && m_parent->matchesMimeFilter(item)) { if (refreshItemWasFiltered) { Q_ASSERT(!item.isNull()); lstNewItems[directoryUrl].append(item); } else { Q_ASSERT(!item.isNull()); lstRefreshItems.append(qMakePair(oldItem, item)); } } else if (!refreshItemWasFiltered) { // notify the user that the mimetype of a file changed that doesn't match // a filter or does match an exclude filter // This also happens when renaming foo to .foo and dot files are hidden (#174721) Q_ASSERT(!oldItem.isNull()); lstRemoveItems.append(oldItem); } } void KCoreDirLister::Private::emitItems() { if (!lstNewItems.empty()) { QHashIterator it(lstNewItems); while (it.hasNext()) { it.next(); emit m_parent->itemsAdded(it.key(), it.value()); emit m_parent->newItems(it.value()); // compat } lstNewItems.clear(); } if (!lstMimeFilteredItems.empty()) { emit m_parent->itemsFilteredByMime(lstMimeFilteredItems); lstMimeFilteredItems.clear(); } if (!lstRefreshItems.empty()) { emit m_parent->refreshItems(lstRefreshItems); lstRefreshItems.clear(); } if (!lstRemoveItems.empty()) { emit m_parent->itemsDeleted(lstRemoveItems); lstRemoveItems.clear(); } } bool KCoreDirLister::Private::isItemVisible(const KFileItem &item) const { // Note that this doesn't include mime filters, because // of the itemsFilteredByMime signal. Filtered-by-mime items are // considered "visible", they are just visible via a different signal... return (!settings.dirOnlyMode || item.isDir()) && m_parent->matchesFilter(item); } void KCoreDirLister::Private::emitItemsDeleted(const KFileItemList &_items) { KFileItemList items = _items; QMutableListIterator it(items); while (it.hasNext()) { const KFileItem &item = it.next(); if (!isItemVisible(item) || !m_parent->matchesMimeFilter(item)) { it.remove(); } } if (!items.isEmpty()) { emit m_parent->itemsDeleted(items); } } // ================ private slots ================ // void KCoreDirLister::Private::_k_slotInfoMessage(KJob *, const QString &message) { emit m_parent->infoMessage(message); } void KCoreDirLister::Private::_k_slotPercent(KJob *job, unsigned long pcnt) { jobData[static_cast(job)].percent = pcnt; int result = 0; KIO::filesize_t size = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).percent * (*dataIt).totalSize; size += (*dataIt).totalSize; ++dataIt; } if (size != 0) { result /= size; } else { result = 100; } emit m_parent->percent(result); } void KCoreDirLister::Private::_k_slotTotalSize(KJob *job, qulonglong size) { jobData[static_cast(job)].totalSize = size; KIO::filesize_t result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).totalSize; ++dataIt; } emit m_parent->totalSize(result); } void KCoreDirLister::Private::_k_slotProcessedSize(KJob *job, qulonglong size) { jobData[static_cast(job)].processedSize = size; KIO::filesize_t result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).processedSize; ++dataIt; } emit m_parent->processedSize(result); } void KCoreDirLister::Private::_k_slotSpeed(KJob *job, unsigned long spd) { jobData[static_cast(job)].speed = spd; int result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).speed; ++dataIt; } emit m_parent->speed(result); } uint KCoreDirLister::Private::numJobs() { #ifdef DEBUG_CACHE // This code helps detecting stale entries in the jobData map. qCDebug(KIO_CORE_DIRLISTER) << m_parent << "numJobs:" << jobData.count(); QMapIterator it(jobData); while (it.hasNext()) { it.next(); qCDebug(KIO_CORE_DIRLISTER) << (void*)it.key(); qCDebug(KIO_CORE_DIRLISTER) << it.key(); } #endif return jobData.count(); } void KCoreDirLister::Private::jobDone(KIO::ListJob *job) { jobData.remove(job); } void KCoreDirLister::jobStarted(KIO::ListJob *job) { Private::JobData data; data.speed = 0; data.percent = 0; data.processedSize = 0; data.totalSize = 0; d->jobData.insert(job, data); d->complete = false; } void KCoreDirLister::Private::connectJob(KIO::ListJob *job) { m_parent->connect(job, &KJob::infoMessage, m_parent, [this](KJob *job, const QString &plain){ _k_slotInfoMessage(job, plain); }); m_parent->connect(job, QOverload::of(&KJob::percent), m_parent, [this](KJob *job, ulong _percent){ _k_slotPercent(job, _percent); }); m_parent->connect(job, &KJob::totalSize, m_parent, [this](KJob *job, qulonglong _size){ _k_slotTotalSize(job, _size); }); m_parent->connect(job, &KJob::processedSize, m_parent, [this](KJob *job, qulonglong _psize){ _k_slotProcessedSize(job, _psize); }); m_parent->connect(job, &KJob::speed, m_parent, [this](KJob *job, qulonglong _speed){ _k_slotSpeed(job, _speed); }); } KFileItemList KCoreDirLister::items(WhichItems which) const { return itemsForDir(url(), which); } KFileItemList KCoreDirLister::itemsForDir(const QUrl &dir, WhichItems which) const { QList *allItems = kDirListerCache()->itemsForDir(dir); KFileItemList result; if (!allItems) { return result; } if (which == AllItems) { return KFileItemList(*allItems); } else { // only items passing the filters auto kit = allItems->constBegin(); const auto kend = allItems->constEnd(); for (; kit != kend; ++kit) { const KFileItem &item = *kit; if (d->isItemVisible(item) && matchesMimeFilter(item)) { result.append(item); } } } return result; } bool KCoreDirLister::delayedMimeTypes() const { return d->delayedMimeTypes; } void KCoreDirLister::setDelayedMimeTypes(bool delayedMimeTypes) { d->delayedMimeTypes = delayedMimeTypes; } // called by KCoreDirListerCache::slotRedirection void KCoreDirLister::Private::redirect(const QUrl &oldUrl, const QUrl &newUrl, bool keepItems) { if (url.matches(oldUrl, QUrl::StripTrailingSlash)) { if (!keepItems) { rootFileItem = KFileItem(); } else { rootFileItem.setUrl(newUrl); } url = newUrl; } const int idx = lstDirs.indexOf(oldUrl); if (idx == -1) { qCWarning(KIO_CORE) << "Unexpected redirection from" << oldUrl << "to" << newUrl << "but this dirlister is currently listing/holding" << lstDirs; } else { lstDirs[ idx ] = newUrl; } if (lstDirs.count() == 1) { if (!keepItems) { emit m_parent->clear(); } emit m_parent->redirection(newUrl); } else { if (!keepItems) { emit m_parent->clear(oldUrl); } } emit m_parent->redirection(oldUrl, newUrl); } void KCoreDirListerCacheDirectoryData::moveListersWithoutCachedItemsJob(const QUrl &url) { // Move dirlisters from listersCurrentlyListing to listersCurrentlyHolding, // but not those that are still waiting on a CachedItemsJob... // Unit-testing note: // Run kdirmodeltest in valgrind to hit the case where an update // is triggered while a lister has a CachedItemsJob (different timing...) QMutableListIterator lister_it(listersCurrentlyListing); while (lister_it.hasNext()) { KCoreDirLister *kdl = lister_it.next(); if (!kdl->d->cachedItemsJobForUrl(url)) { // OK, move this lister from "currently listing" to "currently holding". // Huh? The KCoreDirLister was present twice in listersCurrentlyListing, or was in both lists? Q_ASSERT(!listersCurrentlyHolding.contains(kdl)); if (!listersCurrentlyHolding.contains(kdl)) { listersCurrentlyHolding.append(kdl); } lister_it.remove(); } else { qCDebug(KIO_CORE_DIRLISTER) << "Not moving" << kdl << "to listersCurrentlyHolding because it still has job" << kdl->d->m_cachedItemsJobs; } } } KFileItem KCoreDirLister::cachedItemForUrl(const QUrl &url) { if (kDirListerCache.exists()) { return kDirListerCache()->itemForUrl(url); } else { return {}; } } QSet KCoreDirListerCache::filesInDotHiddenForDir(const QString& dir) { const QString path = dir + QLatin1String("/.hidden"); QFile dotHiddenFile(path); if (dotHiddenFile.exists()) { const QDateTime mtime = QFileInfo(dotHiddenFile).lastModified(); const CacheHiddenFile *cachedDotHiddenFile = m_cacheHiddenFiles.object(path); if (cachedDotHiddenFile && mtime <= cachedDotHiddenFile->mtime) { // ".hidden" is in cache and still valid (the file was not modified since then), // so return it return cachedDotHiddenFile->listedFiles; } else { // read the ".hidden" file, then cache it and return it if (dotHiddenFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QSet filesToHide; QTextStream stream(&dotHiddenFile); while (!stream.atEnd()) { QString name = stream.readLine(); if (!name.isEmpty()) { filesToHide.insert(name); } } m_cacheHiddenFiles.insert(path, new CacheHiddenFile(mtime, filesToHide)); return filesToHide; } } } return QSet(); } #include "moc_kcoredirlister.cpp" #include "moc_kcoredirlister_p.cpp" diff --git a/src/core/kioglobal_p.cpp b/src/core/kioglobal_p.cpp index 0621cf28..78a0e37a 100644 --- a/src/core/kioglobal_p.cpp +++ b/src/core/kioglobal_p.cpp @@ -1,57 +1,56 @@ /* This file is part of the KDE libraries Copyright (C) 2015 Harald Sitter 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 "kioglobal_p.h" #include static QMap standardLocationsMap() { static const struct { QStandardPaths::StandardLocation location; QString name; } mapping[] = { { QStandardPaths::MusicLocation, QStringLiteral("folder-music") }, { QStandardPaths::MoviesLocation, QStringLiteral("folder-videos") }, { QStandardPaths::PicturesLocation, QStringLiteral("folder-pictures") }, { QStandardPaths::TempLocation, QStringLiteral("folder-temp") }, { QStandardPaths::DownloadLocation, QStringLiteral("folder-download") }, // Order matters here as paths can be reused for multiple purposes // We essentially want more generic choices to trump more specific // ones. // home > desktop > documents > *. { QStandardPaths::DocumentsLocation, QStringLiteral("folder-documents") }, { QStandardPaths::DesktopLocation, QStringLiteral("user-desktop") }, { QStandardPaths::HomeLocation, QStringLiteral("user-home") } }; - static const int count = sizeof mapping / sizeof *mapping; QMap map; - for (int i = 0 ; i < count; ++i) { - const auto locations = QStandardPaths::standardLocations(mapping[i].location); + for (const auto &row : mapping) { + const QStringList locations = QStandardPaths::standardLocations(row.location); for (const QString &location : locations) { - map.insert(location, mapping[i].name); + map.insert(location, row.name); } } return map; } QString KIOPrivate::iconForStandardPath(const QString &localDirectory) { static auto map = standardLocationsMap(); return map.value(localDirectory, QString()); } diff --git a/src/core/kmountpoint.cpp b/src/core/kmountpoint.cpp index bb4f2cae..ca634b52 100644 --- a/src/core/kmountpoint.cpp +++ b/src/core/kmountpoint.cpp @@ -1,487 +1,487 @@ /* * This file is part of the KDE libraries * Copyright (c) 2003 Waldo Bastian * 2007 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 "kmountpoint.h" #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif #ifdef Q_OS_WIN static const Qt::CaseSensitivity cs = Qt::CaseInsensitive; #else static const Qt::CaseSensitivity cs = Qt::CaseSensitive; #endif #if HAVE_VOLMGT #include #endif #if HAVE_SYS_MNTTAB_H #include #endif #if HAVE_MNTENT_H #include #elif HAVE_SYS_MNTENT_H #include #endif // This is the *BSD branch #if HAVE_SYS_MOUNT_H #if HAVE_SYS_TYPES_H #include #endif #if HAVE_SYS_PARAM_H #include #endif #include #endif #if HAVE_FSTAB_H #include #endif #if ! HAVE_GETMNTINFO # ifdef _PATH_MOUNTED // On some Linux, MNTTAB points to /etc/fstab ! # undef MNTTAB # define MNTTAB _PATH_MOUNTED # else # ifndef MNTTAB # ifdef MTAB_FILE # define MNTTAB MTAB_FILE # else # define MNTTAB "/etc/mnttab" # endif # endif # endif #endif #ifdef _OS_SOLARIS_ #define FSTAB "/etc/vfstab" #else #define FSTAB "/etc/fstab" #endif class Q_DECL_HIDDEN KMountPoint::Private { public: void finalizePossibleMountPoint(DetailsNeededFlags infoNeeded); void finalizeCurrentMountPoint(DetailsNeededFlags infoNeeded); QString mountedFrom; QString device; // Only available when the NeedRealDeviceName flag was set. QString mountPoint; QString mountType; QStringList mountOptions; }; KMountPoint::KMountPoint() : d(new Private) { } KMountPoint::~KMountPoint() { delete d; } // There are (at least) four kind of APIs: // setmntent + getmntent + struct mntent (linux...) // getmntent + struct mnttab // getmntinfo + struct statfs&flags (BSD 4.4 and friends) // getfsent + char* (BSD 4.3 and friends) #if HAVE_SETMNTENT #define SETMNTENT setmntent #define ENDMNTENT endmntent #define STRUCT_MNTENT struct mntent * #define STRUCT_SETMNTENT FILE * #define GETMNTENT(file, var) ((var = getmntent(file)) != nullptr) #define MOUNTPOINT(var) var->mnt_dir #define MOUNTTYPE(var) var->mnt_type #define MOUNTOPTIONS(var) var->mnt_opts #define FSNAME(var) var->mnt_fsname #else #define SETMNTENT fopen #define ENDMNTENT fclose #define STRUCT_MNTENT struct mnttab #define STRUCT_SETMNTENT FILE * #define GETMNTENT(file, var) (getmntent(file, &var) == nullptr) #define MOUNTPOINT(var) var.mnt_mountp #define MOUNTTYPE(var) var.mnt_fstype #define MOUNTOPTIONS(var) var.mnt_mntopts #define FSNAME(var) var.mnt_special #endif void KMountPoint::Private::finalizePossibleMountPoint(DetailsNeededFlags infoNeeded) { if (mountedFrom.startsWith(QLatin1String("UUID="))) { const QStringRef uuid = mountedFrom.midRef(5); const QString potentialDevice = QFile::symLinkTarget(QLatin1String("/dev/disk/by-uuid/") + uuid); if (QFile::exists(potentialDevice)) { mountedFrom = potentialDevice; } } if (mountedFrom.startsWith(QLatin1String("LABEL="))) { const QStringRef label = mountedFrom.midRef(6); const QString potentialDevice = QFile::symLinkTarget(QLatin1String("/dev/disk/by-label/") + label); if (QFile::exists(potentialDevice)) { mountedFrom = potentialDevice; } } if (infoNeeded & NeedRealDeviceName) { if (mountedFrom.startsWith(QLatin1Char('/'))) { device = QFileInfo(mountedFrom).canonicalFilePath(); } } // Chop trailing slash if (mountedFrom.endsWith(QLatin1Char('/'))) { mountedFrom.chop(1); } } void KMountPoint::Private::finalizeCurrentMountPoint(DetailsNeededFlags infoNeeded) { if (infoNeeded & NeedRealDeviceName) { if (mountedFrom.startsWith(QLatin1Char('/'))) { device = QFileInfo(mountedFrom).canonicalFilePath(); } } } KMountPoint::List KMountPoint::possibleMountPoints(DetailsNeededFlags infoNeeded) { #ifdef Q_OS_WIN return KMountPoint::currentMountPoints(infoNeeded); #endif KMountPoint::List result; #if HAVE_SETMNTENT STRUCT_SETMNTENT fstab; if ((fstab = SETMNTENT(FSTAB, "r")) == nullptr) { return result; } STRUCT_MNTENT fe; while (GETMNTENT(fstab, fe)) { Ptr mp(new KMountPoint); mp->d->mountedFrom = QFile::decodeName(FSNAME(fe)); mp->d->mountPoint = QFile::decodeName(MOUNTPOINT(fe)); mp->d->mountType = QFile::decodeName(MOUNTTYPE(fe)); if (infoNeeded & NeedMountOptions) { QString options = QFile::decodeName(MOUNTOPTIONS(fe)); mp->d->mountOptions = options.split(QLatin1Char(',')); } mp->d->finalizePossibleMountPoint(infoNeeded); result.append(mp); } ENDMNTENT(fstab); #else QFile f(QLatin1String(FSTAB)); if (!f.open(QIODevice::ReadOnly)) { return result; } QTextStream t(&f); QString s; while (! t.atEnd()) { s = t.readLine().simplified(); if (s.isEmpty() || (s[0] == QLatin1Char('#'))) { continue; } // not empty or commented out by '#' const QStringList item = s.split(QLatin1Char(' ')); #ifdef _OS_SOLARIS_ if (item.count() < 5) { continue; } #else if (item.count() < 4) { continue; } #endif Ptr mp(new KMountPoint); int i = 0; mp->d->mountedFrom = item[i++]; #ifdef _OS_SOLARIS_ //device to fsck i++; #endif mp->d->mountPoint = item[i++]; mp->d->mountType = item[i++]; QString options = item[i++]; if (infoNeeded & NeedMountOptions) { mp->d->mountOptions = options.split(QLatin1Char(',')); } mp->d->finalizePossibleMountPoint(infoNeeded); result.append(mp); } //while f.close(); #endif return result; } KMountPoint::List KMountPoint::currentMountPoints(DetailsNeededFlags infoNeeded) { KMountPoint::List result; #if HAVE_GETMNTINFO #if GETMNTINFO_USES_STATVFS struct statvfs *mounted; #else struct statfs *mounted; #endif int num_fs = getmntinfo(&mounted, MNT_NOWAIT); for (int i = 0; i < num_fs; i++) { Ptr mp(new KMountPoint); mp->d->mountedFrom = QFile::decodeName(mounted[i].f_mntfromname); mp->d->mountPoint = QFile::decodeName(mounted[i].f_mntonname); #ifdef __osf__ mp->d->mountType = QFile::decodeName(mnt_names[mounted[i].f_type]); #else mp->d->mountType = QFile::decodeName(mounted[i].f_fstypename); #endif if (infoNeeded & NeedMountOptions) { struct fstab *ft = getfsfile(mounted[i].f_mntonname); if (ft != 0) { QString options = QFile::decodeName(ft->fs_mntops); mp->d->mountOptions = options.split(QLatin1Char(',')); } else { // TODO: get mount options if not mounted via fstab, see mounted[i].f_flags } } mp->d->finalizeCurrentMountPoint(infoNeeded); // TODO: Strip trailing '/' ? result.append(mp); } #elif defined(Q_OS_WIN) //nothing fancy with infoNeeded but it gets the job done DWORD bits = GetLogicalDrives(); if (!bits) { return result; } for (int i = 0; i < 26; i++) { if (bits & (1 << i)) { Ptr mp(new KMountPoint); mp->d->mountPoint = QString(QLatin1Char('A' + i) + QLatin1String(":/")); result.append(mp); } } #elif !defined(Q_OS_ANDROID) STRUCT_SETMNTENT mnttab; if ((mnttab = SETMNTENT(MNTTAB, "r")) == nullptr) { return result; } STRUCT_MNTENT fe; while (GETMNTENT(mnttab, fe)) { Ptr mp(new KMountPoint); mp->d->mountedFrom = QFile::decodeName(FSNAME(fe)); mp->d->mountPoint = QFile::decodeName(MOUNTPOINT(fe)); mp->d->mountType = QFile::decodeName(MOUNTTYPE(fe)); if (infoNeeded & NeedMountOptions) { QString options = QFile::decodeName(MOUNTOPTIONS(fe)); mp->d->mountOptions = options.split(QLatin1Char(',')); } // Resolve gvfs mountpoints if (mp->d->mountedFrom == QLatin1String("gvfsd-fuse")) { const QDir gvfsDir(mp->d->mountPoint); const QStringList mountDirs = gvfsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString &mountDir : mountDirs) { const QString type = mountDir.section(QLatin1Char(':'), 0, 0); if (type.isEmpty()) { continue; } Ptr gvfsmp(new KMountPoint); gvfsmp->d->mountedFrom = mp->d->mountedFrom; gvfsmp->d->mountPoint = mp->d->mountPoint + QLatin1Char('/') + mountDir; gvfsmp->d->mountType = type; result.append(gvfsmp); } } mp->d->finalizeCurrentMountPoint(infoNeeded); result.append(mp); } ENDMNTENT(mnttab); #endif return result; } QString KMountPoint::mountedFrom() const { return d->mountedFrom; } QString KMountPoint::realDeviceName() const { return d->device; } QString KMountPoint::mountPoint() const { return d->mountPoint; } QString KMountPoint::mountType() const { return d->mountType; } QStringList KMountPoint::mountOptions() const { return d->mountOptions; } KMountPoint::List::List() : QList() { } static bool pathsAreParentAndChildOrEqual(const QString &parent, const QString &child) { const QLatin1Char slash('/'); if (child.startsWith(parent, cs)) { // Check if either // (a) both paths are equal, or // (b) parent ends with '/', or // (c) the first character of child that is not shared with parent is '/'. // Note that child is guaranteed to be longer than parent if (a) is false. // // This prevents that we incorrectly consider "/books" a child of "/book". return parent.compare(child, cs) == 0 || parent.endsWith(slash) || child.at(parent.length()) == slash; } else { // Note that "/books" is a child of "/books/". return parent.endsWith(slash) && (parent.length() == child.length() + 1) && parent.startsWith(child, cs); } } KMountPoint::Ptr KMountPoint::List::findByPath(const QString &path) const { #ifndef Q_OS_WIN /* If the path contains symlinks, get the real name */ QFileInfo fileinfo(path); const QString realname = fileinfo.exists() ? fileinfo.canonicalFilePath() : fileinfo.absolutePath(); //canonicalFilePath won't work unless file exists #else const QString realname = QDir::fromNativeSeparators(QDir(path).absolutePath()); #endif int max = 0; KMountPoint::Ptr result; - for (const_iterator it = begin(); it != end(); ++it) { - const QString mountpoint = (*it)->d->mountPoint; + for (const KMountPoint::Ptr &mp : *this) { + const QString mountpoint = mp->d->mountPoint; const int length = mountpoint.length(); if (length > max && pathsAreParentAndChildOrEqual(mountpoint, realname)) { max = length; - result = *it; + result = mp; // keep iterating to check for a better match (bigger max) } } return result; } KMountPoint::Ptr KMountPoint::List::findByDevice(const QString &device) const { const QString realDevice = QFileInfo(device).canonicalFilePath(); if (realDevice.isEmpty()) { // d->device can be empty in the loop below, don't match empty with it return Ptr(); } - for (const_iterator it = begin(); it != end(); ++it) { - if (realDevice.compare((*it)->d->device, cs) == 0 || - realDevice.compare((*it)->d->mountedFrom, cs) == 0) { - return *it; + for (const KMountPoint::Ptr &mountPoint : *this) { + if (realDevice.compare(mountPoint->d->device, cs) == 0 || + realDevice.compare(mountPoint->d->mountedFrom, cs) == 0) { + return mountPoint; } } return Ptr(); } bool KMountPoint::probablySlow() const { return d->mountType == QLatin1String("nfs") || d->mountType == QLatin1String("nfs4") || d->mountType == QLatin1String("cifs") || d->mountType == QLatin1String("autofs") || d->mountType == QLatin1String("subfs"); } bool KMountPoint::testFileSystemFlag(FileSystemFlag flag) const { const bool isMsDos = (d->mountType == QLatin1String("msdos") || d->mountType == QLatin1String("fat") || d->mountType == QLatin1String("vfat")); const bool isNtfs = d->mountType.contains(QLatin1String("fuse.ntfs")) || d->mountType.contains(QLatin1String("fuseblk.ntfs")) // fuseblk could really be anything. But its most common use is for NTFS mounts, these days. || d->mountType == QLatin1String("fuseblk"); const bool isSmb = d->mountType == QLatin1String("cifs") || d->mountType == QLatin1String("smbfs") // gvfs-fuse mounted SMB share || d->mountType == QLatin1String("smb-share"); switch (flag) { case SupportsChmod: case SupportsChown: case SupportsUTime: case SupportsSymlinks: return !isMsDos && !isNtfs && !isSmb; // it's amazing the number of things Microsoft filesystems don't support :) case CaseInsensitive: return isMsDos; } return false; } diff --git a/src/core/kprotocolmanager.cpp b/src/core/kprotocolmanager.cpp index b2ee2b5c..34b5957c 100644 --- a/src/core/kprotocolmanager.cpp +++ b/src/core/kprotocolmanager.cpp @@ -1,1327 +1,1326 @@ /* 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 #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 QStringRef portStr(proxyStr.rightRef(proxyStr.length() - index - 1)); portStr.toInt(&ok); if (ok) { 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); proxies.reserve(proxyList.size()); for (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()); for (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"))) { for (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(QLatin1String("; ")); 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(QLatin1String("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; for (const QString &lang : qAsConst(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; for (const QString &lang : qAsConst(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; + for (KProtocolInfoPrivate *allProtocol : allProtocols) { + const QStringList archiveMimetypes = allProtocol->m_archiveMimeTypes; for (const QString &mime : archiveMimetypes) { - d->protocolForArchiveMimetypes.insert(mime, (*it)->m_name); + d->protocolForArchiveMimetypes.insert(mime, allProtocol->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/core/krecentdocument.cpp b/src/core/krecentdocument.cpp index b26b0e8c..6ff97098 100644 --- a/src/core/krecentdocument.cpp +++ b/src/core/krecentdocument.cpp @@ -1,181 +1,180 @@ /* -*- c++ -*- * Copyright (C)2000 Daniel M. Duley * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * */ #include "krecentdocument.h" #ifdef Q_OS_WIN #include #else #include #endif #include #include #include #include #include #include #include #include #include QString KRecentDocument::recentDocumentDirectory() { // need to change this path, not sure where return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/RecentDocuments/"); } QStringList KRecentDocument::recentDocuments() { QDir d(recentDocumentDirectory(), QStringLiteral("*.desktop"), QDir::Time, QDir::Files | QDir::Readable | QDir::Hidden); if (!d.exists()) { d.mkdir(recentDocumentDirectory()); } const QStringList list = d.entryList(); QStringList fullList; - for (QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { - QString fileName = *it; + for (const QString &fileName : list) { QString pathDesktop; if (fileName.startsWith(QLatin1Char(':'))) { // See: https://bugreports.qt.io/browse/QTBUG-11223 - pathDesktop = KRecentDocument::recentDocumentDirectory() + *it; + pathDesktop = KRecentDocument::recentDocumentDirectory() + fileName; } else { - pathDesktop = d.absoluteFilePath(*it); + pathDesktop = d.absoluteFilePath(fileName); } KDesktopFile tmpDesktopFile(pathDesktop); QUrl urlDesktopFile(tmpDesktopFile.desktopGroup().readPathEntry("URL", QString())); if (urlDesktopFile.isLocalFile() && !QFile(urlDesktopFile.toLocalFile()).exists()) { d.remove(pathDesktop); } else { fullList.append(pathDesktop); } } return fullList; } void KRecentDocument::add(const QUrl &url) { // desktopFileName is in QGuiApplication but we're in KIO Core here QString desktopEntryName = QCoreApplication::instance()->property("desktopFileName").toString(); if (desktopEntryName.isEmpty()) { desktopEntryName = QCoreApplication::applicationName(); } KRecentDocument::add(url, desktopEntryName); // ### componentName might not match the service filename... } void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName) { if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) { return; // inside tmp resource, do not save } QString openStr = url.toDisplayString(); openStr.replace(QRegExp(QStringLiteral("\\$")), QStringLiteral("$$")); // Desktop files with type "Link" are $-variable expanded // qDebug() << "KRecentDocument::add for " << openStr; KConfigGroup config = KSharedConfig::openConfig()->group(QByteArray("RecentDocuments")); bool useRecent = config.readEntry(QStringLiteral("UseRecent"), true); int maxEntries = config.readEntry(QStringLiteral("MaxEntries"), 10); if (!useRecent || maxEntries <= 0) { return; } const QString path = recentDocumentDirectory(); const QString fileName = url.fileName(); // don't create a file called ".desktop", it will lead to an empty name in kio_recentdocuments const QString dStr = path + (fileName.isEmpty() ? QStringLiteral("unnamed") : fileName); QString ddesktop = dStr + QLatin1String(".desktop"); int i = 1; // check for duplicates while (QFile::exists(ddesktop)) { // see if it points to the same file and application KDesktopFile tmp(ddesktop); if (tmp.desktopGroup().readPathEntry("URL", QString()) == url.toDisplayString() && tmp.desktopGroup().readEntry("X-KDE-LastOpenedWith") == desktopEntryName) { // Set access and modification time to current time ::utime(QFile::encodeName(ddesktop).constData(), nullptr); return; } // if not append a (num) to it ++i; if (i > maxEntries) { break; } ddesktop = dStr + QStringLiteral("[%1].desktop").arg(i); } QDir dir(path); // check for max entries, delete oldest files if exceeded const QStringList list = dir.entryList(QDir::Files | QDir::Hidden, QFlags(QDir::Time | QDir::Reversed)); i = list.count(); if (i > maxEntries - 1) { QStringList::ConstIterator it; it = list.begin(); while (i > maxEntries - 1) { QFile::remove(dir.absolutePath() + QLatin1Char('/') + (*it)); --i; ++it; } } // create the applnk KDesktopFile configFile(ddesktop); KConfigGroup conf = configFile.desktopGroup(); conf.writeEntry("Type", QStringLiteral("Link")); conf.writePathEntry("URL", openStr); // If you change the line below, change the test in the above loop conf.writeEntry("X-KDE-LastOpenedWith", desktopEntryName); conf.writeEntry("Name", url.fileName()); conf.writeEntry("Icon", KIO::iconNameForUrl(url)); } void KRecentDocument::clear() { const QStringList list = recentDocuments(); QDir dir; - for (QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { - dir.remove(*it); + for (const QString &desktopFilePath : list) { + dir.remove(desktopFilePath); } } int KRecentDocument::maximumItems() { KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("RecentDocuments")); return cg.readEntry(QStringLiteral("MaxEntries"), 10); } diff --git a/src/core/ksambashare.cpp b/src/core/ksambashare.cpp index ef67af54..e09d6cf6 100644 --- a/src/core/ksambashare.cpp +++ b/src/core/ksambashare.cpp @@ -1,528 +1,526 @@ /* This file is part of the KDE project Copyright (c) 2004 Jan Schaefer Copyright 2010 Rodrigo Belem 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 "ksambashare.h" #include "ksambashare_p.h" #include "ksambasharedata.h" #include "ksambasharedata_p.h" #include "kiocoredebug.h" #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE) Q_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE, "kf5.kio.core.sambashare", QtWarningMsg) // Default smb.conf locations // sorted by priority, most priority first static const char *const DefaultSambaConfigFilePathList[] = { "/etc/samba/smb.conf", "/etc/smb.conf", "/usr/local/etc/smb.conf", "/usr/local/samba/lib/smb.conf", "/usr/samba/lib/smb.conf", "/usr/lib/smb.conf", "/usr/local/lib/smb.conf" }; -static const int DefaultSambaConfigFilePathListSize = sizeof(DefaultSambaConfigFilePathList) - / sizeof(char *); KSambaSharePrivate::KSambaSharePrivate(KSambaShare *parent) : q_ptr(parent) , data() , smbConf() , userSharePath() , skipUserShare(false) { setUserSharePath(); findSmbConf(); data = parse(getNetUserShareInfo()); } KSambaSharePrivate::~KSambaSharePrivate() { } bool KSambaSharePrivate::isSambaInstalled() { if (QFile::exists(QStringLiteral("/usr/sbin/smbd")) || QFile::exists(QStringLiteral("/usr/local/sbin/smbd"))) { return true; } //qDebug() << "Samba is not installed!"; return false; } // Try to find the samba config file path // in several well-known paths bool KSambaSharePrivate::findSmbConf() { - for (int i = 0; i < DefaultSambaConfigFilePathListSize; ++i) { - const QString filePath = QString::fromLatin1(DefaultSambaConfigFilePathList[i]); + for (const char *str : DefaultSambaConfigFilePathList) { + const QString filePath = QString::fromLatin1(str); if (QFile::exists(filePath)) { smbConf = filePath; return true; } } qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smb.conf!"; return false; } void KSambaSharePrivate::setUserSharePath() { const QString rawString = testparmParamValue(QStringLiteral("usershare path")); const QFileInfo fileInfo(rawString); if (fileInfo.isDir()) { userSharePath = rawString; } } int KSambaSharePrivate::runProcess(const QString &progName, const QStringList &args, QByteArray &stdOut, QByteArray &stdErr) { QProcess process; process.setProcessChannelMode(QProcess::SeparateChannels); process.start(progName, args); //TODO: make it async in future process.waitForFinished(); stdOut = process.readAllStandardOutput(); stdErr = process.readAllStandardError(); return process.exitCode(); } QString KSambaSharePrivate::testparmParamValue(const QString ¶meterName) { if (!isSambaInstalled()) { return QString(); } QByteArray stdErr; QByteArray stdOut; const QStringList args{ QStringLiteral("-d0"), QStringLiteral("-s"), QStringLiteral("--parameter-name"), parameterName, }; runProcess(QStringLiteral("testparm"), args, stdOut, stdErr); //TODO: parse and process error messages. // create a parser for the error output and // send error message somewhere if (!stdErr.isEmpty()) { QList err; err << stdErr.trimmed().split('\n'); if ((err.count() == 2) && err.at(0).startsWith("Load smb config files from") && err.at(1).startsWith("Loaded services file OK.")) { //qDebug() << "Running testparm" << args; } else { qCWarning(KIO_CORE) << "We got some errors while running testparm" << stdErr; } } if (!stdOut.isEmpty()) { return QString::fromLocal8Bit(stdOut.trimmed()); } return QString(); } QByteArray KSambaSharePrivate::getNetUserShareInfo() { if (skipUserShare || !isSambaInstalled()) { return QByteArray(); } QByteArray stdOut; QByteArray stdErr; const QStringList args{ QStringLiteral("usershare"), QStringLiteral("info"), }; runProcess(QStringLiteral("net"), args, stdOut, stdErr); if (!stdErr.isEmpty()) { if (stdErr.contains("You do not have permission to create a usershare")) { skipUserShare = true; } else if (stdErr.contains("usershares are currently disabled")) { skipUserShare = true; } else { //TODO: parse and process other error messages. // create a parser for the error output and // send error message somewhere qCWarning(KIO_CORE) << "We got some errors while running 'net usershare info'"; qCWarning(KIO_CORE) << stdErr; } } return stdOut; } QStringList KSambaSharePrivate::shareNames() const { return data.keys(); } QStringList KSambaSharePrivate::sharedDirs() const { QStringList dirs; QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (!dirs.contains(i.value().path())) { dirs << i.value().path(); } } return dirs; } KSambaShareData KSambaSharePrivate::getShareByName(const QString &shareName) const { return data.value(shareName); } QList KSambaSharePrivate::getSharesByPath(const QString &path) const { QList shares; QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (i.value().path() == path) { shares << i.value(); } } return shares; } bool KSambaSharePrivate::isShareNameValid(const QString &name) const { // Samba forbidden chars const QRegExp notToMatchRx(QStringLiteral("[%<>*\?|/\\+=;:\",]")); return (notToMatchRx.indexIn(name) == -1); } bool KSambaSharePrivate::isDirectoryShared(const QString &path) const { QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (i.value().path() == path) { return true; } } return false; } bool KSambaSharePrivate::isShareNameAvailable(const QString &name) const { // Samba does not allow to name a share with a user name registered in the system return (!KUser::allUserNames().contains(name) || !data.contains(name)); } KSambaShareData::UserShareError KSambaSharePrivate::isPathValid(const QString &path) const { QFileInfo pathInfo = path; if (!pathInfo.exists()) { return KSambaShareData::UserSharePathNotExists; } if (!pathInfo.isDir()) { return KSambaShareData::UserSharePathNotDirectory; } if (pathInfo.isRelative()) { if (pathInfo.makeAbsolute()) { return KSambaShareData::UserSharePathNotAbsolute; } } // TODO: check if the user is root if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare owner only")) == QLatin1String("Yes")) { if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) { return KSambaShareData::UserSharePathNotAllowed; } } return KSambaShareData::UserSharePathOk; } KSambaShareData::UserShareError KSambaSharePrivate::isAclValid(const QString &acl) const { const QRegExp aclRx(QStringLiteral("(?:(?:(\\w(\\w|\\s)*)\\\\|)(\\w+\\s*):([fFrRd]{1})(?:,|))*")); // TODO: check if user is a valid smb user return aclRx.exactMatch(acl) ? KSambaShareData::UserShareAclOk : KSambaShareData::UserShareAclInvalid; } KSambaShareData::UserShareError KSambaSharePrivate::guestsAllowed(const KSambaShareData::GuestPermission &guestok) const { if (guestok == KSambaShareData::GuestsAllowed) { if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare allow guests")) == QLatin1String("No")) { return KSambaShareData::UserShareGuestsNotAllowed; } } return KSambaShareData::UserShareGuestsOk; } KSambaShareData::UserShareError KSambaSharePrivate::add(const KSambaShareData &shareData) { // TODO: // * check for usershare max shares if (!isSambaInstalled()) { return KSambaShareData::UserShareSystemError; } QByteArray stdOut; QByteArray stdErr; if (data.contains(shareData.name())) { if (data.value(shareData.name()).path() != shareData.path()) { return KSambaShareData::UserShareNameInUse; } } else { // It needs to be added here, otherwise another instance of KSambaShareDataPrivate // will be created and added to data. data.insert(shareData.name(), shareData); } QString guestok = QStringLiteral("guest_ok=%1").arg( (shareData.guestPermission() == KSambaShareData::GuestsNotAllowed) ? QStringLiteral("n") : QStringLiteral("y")); const QStringList args{ QStringLiteral("usershare"), QStringLiteral("add"), shareData.name(), shareData.path(), shareData.comment(), shareData.acl(), guestok, }; int ret = runProcess(QStringLiteral("net"), args, stdOut, stdErr); //TODO: parse and process error messages. if (!stdErr.isEmpty()) { // create a parser for the error output and // send error message somewhere qCWarning(KIO_CORE) << "We got some errors while running 'net usershare add'" << args; qCWarning(KIO_CORE) << stdErr; } return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError; } KSambaShareData::UserShareError KSambaSharePrivate::remove(const KSambaShareData &shareData) const { if (!isSambaInstalled()) { return KSambaShareData::UserShareSystemError; } if (!data.contains(shareData.name())) { return KSambaShareData::UserShareNameInvalid; } const QStringList args{ QStringLiteral("usershare"), QStringLiteral("delete"), shareData.name(), }; int result = QProcess::execute(QStringLiteral("net"), args); return (result == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError; } QMap KSambaSharePrivate::parse(const QByteArray &usershareData) { const QRegExp headerRx(QString::fromLatin1("^\\s*\\[" "([^%<>*\?|/\\+=;:\",]+)" "\\]")); const QRegExp OptValRx(QString::fromLatin1("^\\s*([\\w\\d\\s]+)" "=" "(.*)$")); QTextStream stream(usershareData); QString currentShare; QMap shares; while (!stream.atEnd()) { const QString line = stream.readLine().trimmed(); if (headerRx.exactMatch(line)) { currentShare = headerRx.cap(1).trimmed(); if (!shares.contains(currentShare)) { KSambaShareData shareData; shareData.dd->name = currentShare; shares.insert(currentShare, shareData); } } else if (OptValRx.exactMatch(line)) { const QString key = OptValRx.cap(1).trimmed(); const QString value = OptValRx.cap(2).trimmed(); KSambaShareData shareData = shares[currentShare]; if (key == QLatin1String("path")) { // Samba accepts paths with and w/o trailing slash, we // use and expect path without slash if (value.endsWith(QLatin1Char('/'))) { shareData.dd->path = value.left(value.size() - 1); } else { shareData.dd->path = value; } } else if (key == QLatin1String("comment")) { shareData.dd->comment = value; } else if (key == QLatin1String("usershare_acl")) { shareData.dd->acl = value; } else if (key == QLatin1String("guest_ok")) { shareData.dd->guestPermission = value; } else { qCWarning(KIO_CORE) << "Something nasty happen while parsing 'net usershare info'" << "share:" << currentShare << "key:" << key; } } else if (line.trimmed().isEmpty()) { continue; } else { return shares; } } return shares; } void KSambaSharePrivate::_k_slotFileChange(const QString &path) { if (path != userSharePath) { return; } data = parse(getNetUserShareInfo()); //qDebug() << "path changed:" << path; Q_Q(KSambaShare); emit q->changed(); } KSambaShare::KSambaShare() : QObject(nullptr) , d_ptr(new KSambaSharePrivate(this)) { Q_D(const KSambaShare); if (!d->userSharePath.isEmpty() && QFileInfo::exists(d->userSharePath)) { KDirWatch::self()->addDir(d->userSharePath, KDirWatch::WatchFiles); connect(KDirWatch::self(), SIGNAL(dirty(QString)), this, SLOT(_k_slotFileChange(QString))); } } KSambaShare::~KSambaShare() { Q_D(const KSambaShare); if (KDirWatch::exists() && KDirWatch::self()->contains(d->userSharePath)) { KDirWatch::self()->removeDir(d->userSharePath); } delete d_ptr; } #ifndef KIOCORE_NO_DEPRECATED QString KSambaShare::smbConfPath() const { Q_D(const KSambaShare); return d->smbConf; } #endif bool KSambaShare::isDirectoryShared(const QString &path) const { Q_D(const KSambaShare); return d->isDirectoryShared(path); } bool KSambaShare::isShareNameAvailable(const QString &name) const { Q_D(const KSambaShare); return d->isShareNameValid(name) && d->isShareNameAvailable(name); } QStringList KSambaShare::shareNames() const { Q_D(const KSambaShare); return d->shareNames(); } QStringList KSambaShare::sharedDirectories() const { Q_D(const KSambaShare); return d->sharedDirs(); } KSambaShareData KSambaShare::getShareByName(const QString &name) const { Q_D(const KSambaShare); return d->getShareByName(name); } QList KSambaShare::getSharesByPath(const QString &path) const { Q_D(const KSambaShare); return d->getSharesByPath(path); } class KSambaShareSingleton { public: KSambaShare instance; }; Q_GLOBAL_STATIC(KSambaShareSingleton, _instance) KSambaShare *KSambaShare::instance() { return &_instance()->instance; } #include "moc_ksambashare.cpp" diff --git a/src/core/ksslcertificatemanager.cpp b/src/core/ksslcertificatemanager.cpp index d525998c..b87aec4b 100644 --- a/src/core/ksslcertificatemanager.cpp +++ b/src/core/ksslcertificatemanager.cpp @@ -1,498 +1,498 @@ /* This file is part of the KDE project * * Copyright (C) 2007, 2008, 2010 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 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 "ksslcertificatemanager.h" #include "ksslcertificatemanager_p.h" #include "ktcpsocket.h" #include "ksslerroruidata_p.h" #include #include #include #include #include #include #include #include #include #include "kssld_interface.h" /* Config file format: [] = #for example #mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned, Expired #very.old.com = ExpireUTC 2008-08-20T18:22:14, TooWeakEncryption <- not actually planned to implement #clueless.admin.com = ExpireUTC 2008-08-20T18:22:14, HostNameMismatch # #Wildcard syntax #* = ExpireUTC 2008-08-20T18:22:14, SelfSigned #*.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned #mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, All <- not implemented #* = ExpireUTC 9999-12-31T23:59:59, Reject #we know that something is wrong with that certificate CertificatePEM = #host entries are all lowercase, thus no clashes */ // TODO GUI for managing exception rules class KSslCertificateRulePrivate { public: QSslCertificate certificate; QString hostName; bool isRejected; QDateTime expiryDateTime; QList ignoredErrors; }; KSslCertificateRule::KSslCertificateRule(const QSslCertificate &cert, const QString &hostName) : d(new KSslCertificateRulePrivate()) { d->certificate = cert; d->hostName = hostName; d->isRejected = false; } KSslCertificateRule::KSslCertificateRule(const KSslCertificateRule &other) : d(new KSslCertificateRulePrivate()) { *d = *other.d; } KSslCertificateRule::~KSslCertificateRule() { delete d; } KSslCertificateRule &KSslCertificateRule::operator=(const KSslCertificateRule &other) { *d = *other.d; return *this; } QSslCertificate KSslCertificateRule::certificate() const { return d->certificate; } QString KSslCertificateRule::hostName() const { return d->hostName; } void KSslCertificateRule::setExpiryDateTime(const QDateTime &dateTime) { d->expiryDateTime = dateTime; } QDateTime KSslCertificateRule::expiryDateTime() const { return d->expiryDateTime; } void KSslCertificateRule::setRejected(bool rejected) { d->isRejected = rejected; } bool KSslCertificateRule::isRejected() const { return d->isRejected; } bool KSslCertificateRule::isErrorIgnored(KSslError::Error error) const { foreach (KSslError::Error ignoredError, d->ignoredErrors) if (error == ignoredError) { return true; } return false; } void KSslCertificateRule::setIgnoredErrors(const QList &errors) { d->ignoredErrors.clear(); //### Quadratic runtime, woohoo! Use a QSet if that should ever be an issue. for (KSslError::Error e : errors) if (!isErrorIgnored(e)) { d->ignoredErrors.append(e); } } void KSslCertificateRule::setIgnoredErrors(const QList &errors) { QList el; el.reserve(errors.size()); for (const KSslError &e : errors) { el.append(e.error()); } setIgnoredErrors(el); } QList KSslCertificateRule::ignoredErrors() const { return d->ignoredErrors; } QList KSslCertificateRule::filterErrors(const QList &errors) const { QList ret; for (KSslError::Error error : errors) { if (!isErrorIgnored(error)) { ret.append(error); } } return ret; } QList KSslCertificateRule::filterErrors(const QList &errors) const { QList ret; for (const KSslError &error : errors) { if (!isErrorIgnored(error.error())) { ret.append(error); } } return ret; } //////////////////////////////////////////////////////////////////// static QList deduplicate(const QList &certs) { QSet digests; QList ret; for (const QSslCertificate &cert : certs) { QByteArray digest = cert.digest(); if (!digests.contains(digest)) { digests.insert(digest); ret.append(cert); } } return ret; } KSslCertificateManagerPrivate::KSslCertificateManagerPrivate() : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig), iface(new org::kde::KSSLDInterface(QStringLiteral("org.kde.kssld5"), QStringLiteral("/modules/kssld"), QDBusConnection::sessionBus())), isCertListLoaded(false), userCertDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kssl/userCaCertificates/")) { } KSslCertificateManagerPrivate::~KSslCertificateManagerPrivate() { delete iface; iface = nullptr; } void KSslCertificateManagerPrivate::loadDefaultCaCertificates() { defaultCaCertificates.clear(); QList certs = deduplicate(QSslConfiguration::systemCaCertificates()); KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); certs.append(QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QRegExp::Wildcard)); for (const QSslCertificate &cert : qAsConst(certs)) { const QByteArray digest = cert.digest().toHex(); if (!group.hasKey(digest.constData())) { defaultCaCertificates += cert; } } isCertListLoaded = true; } bool KSslCertificateManagerPrivate::addCertificate(const KSslCaCertificate &in) { //qDebug() << Q_FUNC_INFO; // cannot add a certificate to the system store if (in.store == KSslCaCertificate::SystemStore) { Q_ASSERT(false); return false; } if (knownCerts.contains(in.certHash)) { Q_ASSERT(false); return false; } QString certFilename = userCertDir + QString::fromLatin1(in.certHash); QFile certFile(certFilename); if (!QDir().mkpath(userCertDir) || certFile.open(QIODevice::ReadOnly)) { return false; } if (!certFile.open(QIODevice::WriteOnly)) { return false; } if (certFile.write(in.cert.toPem()) < 1) { return false; } knownCerts.insert(in.certHash); updateCertificateBlacklisted(in); return true; } bool KSslCertificateManagerPrivate::removeCertificate(const KSslCaCertificate &old) { //qDebug() << Q_FUNC_INFO; // cannot remove a certificate from the system store if (old.store == KSslCaCertificate::SystemStore) { Q_ASSERT(false); return false; } if (!QFile::remove(userCertDir + QString::fromLatin1(old.certHash))) { // suppose somebody copied a certificate file into userCertDir without changing the // filename to the digest. // the rest of the code will work fine because it loads all certificate files from // userCertDir without asking for the name, we just can't remove the certificate using // its digest as filename - so search the whole directory. // if the certificate was added with the digest as name *and* with a different name, we // still fail to remove it completely at first try - BAD USER! BAD! bool removed = false; QDir dir(userCertDir); foreach (const QString &certFilename, dir.entryList(QDir::Files)) { const QString certPath = userCertDir + certFilename; QList certs = QSslCertificate::fromPath(certPath); if (!certs.isEmpty() && certs.at(0).digest().toHex() == old.certHash) { if (QFile::remove(certPath)) { removed = true; } else { // maybe the file is readable but not writable return false; } } } if (!removed) { // looks like the file is not there return false; } } // note that knownCerts *should* need no updating due to the way setAllCertificates() works - // it should never call addCertificate and removeCertificate for the same cert in one run // clean up the blacklist setCertificateBlacklisted(old.certHash, false); return true; } static bool certLessThan(const KSslCaCertificate &cacert1, const KSslCaCertificate &cacert2) { if (cacert1.store != cacert2.store) { // SystemStore is numerically smaller so the system certs come first; this is important // so that system certificates come first in case the user added an already-present // certificate as a user certificate. return cacert1.store < cacert2.store; } return cacert1.certHash < cacert2.certHash; } void KSslCertificateManagerPrivate::setAllCertificates(const QList &certsIn) { Q_ASSERT(knownCerts.isEmpty()); QList in = certsIn; QList old = allCertificates(); std::sort(in.begin(), in.end(), certLessThan); std::sort(old.begin(), old.end(), certLessThan); for (int ii = 0, oi = 0; ii < in.size() || oi < old.size(); ++ii, ++oi) { // look at all elements in both lists, even if we reach the end of one early. if (ii >= in.size()) { removeCertificate(old.at(oi)); continue; } else if (oi >= old.size()) { addCertificate(in.at(ii)); continue; } if (certLessThan(old.at(oi), in.at(ii))) { // the certificate in "old" is not in "in". only advance the index of "old". removeCertificate(old.at(oi)); ii--; } else if (certLessThan(in.at(ii), old.at(oi))) { // the certificate in "in" is not in "old". only advance the index of "in". addCertificate(in.at(ii)); oi--; } else { // in.at(ii) "==" old.at(oi) if (in.at(ii).cert != old.at(oi).cert) { // hash collision, be prudent(?) and don't do anything. } else { knownCerts.insert(old.at(oi).certHash); if (in.at(ii).isBlacklisted != old.at(oi).isBlacklisted) { updateCertificateBlacklisted(in.at(ii)); } } } } knownCerts.clear(); QMutexLocker certListLocker(&certListMutex); isCertListLoaded = false; loadDefaultCaCertificates(); } QList KSslCertificateManagerPrivate::allCertificates() const { //qDebug() << Q_FUNC_INFO; QList ret; foreach (const QSslCertificate &cert, deduplicate(QSslConfiguration::systemCaCertificates())) { ret += KSslCaCertificate(cert, KSslCaCertificate::SystemStore, false); } foreach (const QSslCertificate &cert, QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QRegExp::Wildcard)) { ret += KSslCaCertificate(cert, KSslCaCertificate::UserStore, false); } KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); - for (int i = 0; i < ret.size(); i++) { - if (group.hasKey(ret[i].certHash.constData())) { - ret[i].isBlacklisted = true; + for (KSslCaCertificate &cert : ret) { + if (group.hasKey(cert.certHash.constData())) { + cert.isBlacklisted = true; //qDebug() << "is blacklisted"; } } return ret; } bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert) { return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted); } bool KSslCertificateManagerPrivate::setCertificateBlacklisted(const QByteArray &certHash, bool isBlacklisted) { //qDebug() << Q_FUNC_INFO << isBlacklisted; KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); if (isBlacklisted) { // TODO check against certificate list ? group.writeEntry(certHash.constData(), QString()); } else { if (!group.hasKey(certHash.constData())) { return false; } group.deleteEntry(certHash.constData()); } return true; } class KSslCertificateManagerContainer { public: KSslCertificateManager sslCertificateManager; }; Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance) KSslCertificateManager::KSslCertificateManager() : d(new KSslCertificateManagerPrivate()) { } KSslCertificateManager::~KSslCertificateManager() { delete d; } //static KSslCertificateManager *KSslCertificateManager::self() { return &g_instance()->sslCertificateManager; } void KSslCertificateManager::setRule(const KSslCertificateRule &rule) { d->iface->setRule(rule); } void KSslCertificateManager::clearRule(const KSslCertificateRule &rule) { d->iface->clearRule(rule); } void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName) { d->iface->clearRule(cert, hostName); } KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const { return d->iface->rule(cert, hostName); } QList KSslCertificateManager::caCertificates() const { QMutexLocker certLocker(&d->certListMutex); if (!d->isCertListLoaded) { d->loadDefaultCaCertificates(); } return d->defaultCaCertificates; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } QList _allKsslCaCertificates(KSslCertificateManager *cm) { return KSslCertificateManagerPrivate::get(cm)->allCertificates(); } void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList &certsIn) { KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn); } #include "moc_kssld_interface.cpp" diff --git a/src/core/udsentry.cpp b/src/core/udsentry.cpp index ba50559c..db594b9d 100644 --- a/src/core/udsentry.cpp +++ b/src/core/udsentry.cpp @@ -1,496 +1,495 @@ /* This file is part of the KDE project Copyright (C) 2000-2005 David Faure Copyright (C) 2007 Norbert Frese Copyright (C) 2007 Thiago Macieira Copyright (C) 2013-2014 Frank Reininghaus 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 "udsentry.h" #include #include #include #include #include #include using namespace KIO; //BEGIN UDSEntryPrivate /* ---------- UDSEntryPrivate ------------ */ class KIO::UDSEntryPrivate : public QSharedData { public: void reserve(int size); void insert(uint udsField, const QString &value); void replace(uint udsField, const QString &value); void insert(uint udsField, long long value); void replace(uint udsField, long long value); int count() const; QString stringValue(uint udsField) const; long long numberValue(uint udsField, long long defaultValue = -1) const; #ifndef KIOCORE_NO_DEPRECATED QList listFields() const; #endif QVector fields() const; bool contains(uint udsField) const; void clear(); void save(QDataStream &s) const; void load(QDataStream &s); void debugUDSEntry(QDebug &stream) const; /** * @param field numeric UDS field id * @return the name of the field */ static QString nameOfUdsField(uint field); private: struct Field { inline Field() {} inline Field(const uint index, const QString &value) : m_str(value), m_index(index) {} inline Field(const uint index, long long value = 0) : m_long(value), m_index(index) {} QString m_str; long long m_long = LLONG_MIN; uint m_index = 0; }; std::vector storage; }; void UDSEntryPrivate::reserve(int size) { storage.reserve(size); } void UDSEntryPrivate::insert(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); Q_ASSERT(std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}) == storage.cend()); storage.emplace_back(udsField, value); } void UDSEntryPrivate::replace(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); auto it = std::find_if(storage.begin(), storage.end(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.end()) { it->m_str = value; return; } storage.emplace_back(udsField, value); } void UDSEntryPrivate::insert(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); Q_ASSERT(std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}) == storage.cend()); storage.emplace_back(udsField, value); } void UDSEntryPrivate::replace(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); auto it = std::find_if(storage.begin(), storage.end(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.end()) { it->m_long = value; return; } storage.emplace_back(udsField, value); } int UDSEntryPrivate::count() const { return storage.size(); } QString UDSEntryPrivate::stringValue(uint udsField) const { auto it = std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.cend()) { return it->m_str; } return QString(); } long long UDSEntryPrivate::numberValue(uint udsField, long long defaultValue) const { auto it = std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.cend()) { return it->m_long; } return defaultValue; } #ifndef KIOCORE_NO_DEPRECATED QList UDSEntryPrivate::listFields() const { QList res; res.reserve(storage.size()); - for (auto it = storage.cbegin(), end = storage.cend(); it != end; ++it) { - res.append(it->m_index); + for (const Field &field : storage) { + res.append(field.m_index); } return res; } #endif QVector UDSEntryPrivate::fields() const { QVector res; res.reserve(storage.size()); - for (auto it = storage.cbegin(), end = storage.cend(); it != end; ++it) { - res.append(it->m_index); + for (const Field &field : storage) { + res.append(field.m_index); } return res; } bool UDSEntryPrivate::contains(uint udsField) const { auto it = std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}); return (it != storage.cend()); } void UDSEntryPrivate::clear() { storage.clear(); } void UDSEntryPrivate::save(QDataStream &s) const { s << static_cast(storage.size()); - for (auto it = storage.cbegin(), end = storage.cend(); it != end; ++it) - { - uint uds = it->m_index; + for (const Field &field : storage) { + uint uds = field.m_index; s << uds; if (uds & KIO::UDSEntry::UDS_STRING) { - s << it->m_str; + s << field.m_str; } else if (uds & KIO::UDSEntry::UDS_NUMBER) { - s << it->m_long; + s << field.m_long; } else { Q_ASSERT_X(false, "KIO::UDSEntry", "Found a field with an invalid type"); } } } void UDSEntryPrivate::load(QDataStream &s) { clear(); quint32 size; s >> size; reserve(size); // We cache the loaded strings. Some of them, like, e.g., the user, // will often be the same for many entries in a row. Caching them // permits to use implicit sharing to save memory. static QVector cachedStrings; if (quint32(cachedStrings.size()) < size) { cachedStrings.resize(size); } for (quint32 i = 0; i < size; ++i) { quint32 uds; s >> uds; if (uds & KIO::UDSEntry::UDS_STRING) { // If the QString is the same like the one we read for the // previous UDSEntry at the i-th position, use an implicitly // shared copy of the same QString to save memory. QString buffer; s >> buffer; if (buffer != cachedStrings.at(i)) { cachedStrings[i] = buffer; } insert(uds, cachedStrings.at(i)); } else if (uds & KIO::UDSEntry::UDS_NUMBER) { long long value; s >> value; insert(uds, value); } else { Q_ASSERT_X(false, "KIO::UDSEntry", "Found a field with an invalid type"); } } } QString UDSEntryPrivate::nameOfUdsField(uint field) { switch (field) { case UDSEntry::UDS_SIZE: return QStringLiteral("UDS_SIZE"); case UDSEntry::UDS_SIZE_LARGE: return QStringLiteral("UDS_SIZE_LARGE"); case UDSEntry::UDS_USER: return QStringLiteral("UDS_USER"); case UDSEntry::UDS_ICON_NAME: return QStringLiteral("UDS_ICON_NAME"); case UDSEntry::UDS_GROUP: return QStringLiteral("UDS_GROUP"); case UDSEntry::UDS_NAME: return QStringLiteral("UDS_NAME"); case UDSEntry::UDS_LOCAL_PATH: return QStringLiteral("UDS_LOCAL_PATH"); case UDSEntry::UDS_HIDDEN: return QStringLiteral("UDS_HIDDEN"); case UDSEntry::UDS_ACCESS: return QStringLiteral("UDS_ACCESS"); case UDSEntry::UDS_MODIFICATION_TIME: return QStringLiteral("UDS_MODIFICATION_TIME"); case UDSEntry::UDS_ACCESS_TIME: return QStringLiteral("UDS_ACCESS_TIME"); case UDSEntry::UDS_CREATION_TIME: return QStringLiteral("UDS_CREATION_TIME"); case UDSEntry::UDS_FILE_TYPE: return QStringLiteral("UDS_FILE_TYPE"); case UDSEntry::UDS_LINK_DEST: return QStringLiteral("UDS_LINK_DEST"); case UDSEntry::UDS_URL: return QStringLiteral("UDS_URL"); case UDSEntry::UDS_MIME_TYPE: return QStringLiteral("UDS_MIME_TYPE"); case UDSEntry::UDS_GUESSED_MIME_TYPE: return QStringLiteral("UDS_GUESSED_MIME_TYPE"); case UDSEntry::UDS_XML_PROPERTIES: return QStringLiteral("UDS_XML_PROPERTIES"); case UDSEntry::UDS_EXTENDED_ACL: return QStringLiteral("UDS_EXTENDED_ACL"); case UDSEntry::UDS_ACL_STRING: return QStringLiteral("UDS_ACL_STRING"); case UDSEntry::UDS_DEFAULT_ACL_STRING: return QStringLiteral("UDS_DEFAULT_ACL_STRING"); case UDSEntry::UDS_DISPLAY_NAME: return QStringLiteral("UDS_DISPLAY_NAME"); case UDSEntry::UDS_TARGET_URL: return QStringLiteral("UDS_TARGET_URL"); case UDSEntry::UDS_DISPLAY_TYPE: return QStringLiteral("UDS_DISPLAY_TYPE"); case UDSEntry::UDS_ICON_OVERLAY_NAMES: return QStringLiteral("UDS_ICON_OVERLAY_NAMES"); case UDSEntry::UDS_COMMENT: return QStringLiteral("UDS_COMMENT"); case UDSEntry::UDS_DEVICE_ID: return QStringLiteral("UDS_DEVICE_ID"); case UDSEntry::UDS_INODE: return QStringLiteral("UDS_INODE"); case UDSEntry::UDS_EXTRA: return QStringLiteral("UDS_EXTRA"); case UDSEntry::UDS_EXTRA_END: return QStringLiteral("UDS_EXTRA_END"); default: return QStringLiteral("Unknown uds field %1").arg(field); } } void UDSEntryPrivate::debugUDSEntry(QDebug &stream) const { QDebugStateSaver saver(stream); stream.nospace() << "["; - for (auto it = storage.cbegin(), end = storage.cend(); it != end; ++it) { - stream << " " << nameOfUdsField(it->m_index) << "="; - if (it->m_index & KIO::UDSEntry::UDS_STRING) { - stream << it->m_str; - } else if (it->m_index & KIO::UDSEntry::UDS_NUMBER) { - stream << it->m_long; + for (const Field &field : storage) { + stream << " " << nameOfUdsField(field.m_index) << "="; + if (field.m_index & KIO::UDSEntry::UDS_STRING) { + stream << field.m_str; + } else if (field.m_index & KIO::UDSEntry::UDS_NUMBER) { + stream << field.m_long; } else { Q_ASSERT_X(false, "KIO::UDSEntry", "Found a field with an invalid type"); } } stream << " ]"; } //END UDSEntryPrivate //BEGIN UDSEntry /* ---------- UDSEntry ------------ */ UDSEntry::UDSEntry() : d(new UDSEntryPrivate()) { } // BUG: this API doesn't allow to handle symlinks correctly (we need buff from QT_LSTAT for most things, but buff from QT_STAT for st_mode and st_size) UDSEntry::UDSEntry(const QT_STATBUF &buff, const QString &name) : d(new UDSEntryPrivate()) { #ifndef Q_OS_WIN d->reserve(10); #else d->reserve(8); #endif d->insert(UDS_NAME, name); d->insert(UDS_SIZE, buff.st_size); d->insert(UDS_DEVICE_ID, buff.st_dev); d->insert(UDS_INODE, buff.st_ino); d->insert(UDS_FILE_TYPE, buff.st_mode & QT_STAT_MASK); // extract file type d->insert(UDS_ACCESS, buff.st_mode & 07777); // extract permissions d->insert(UDS_MODIFICATION_TIME, buff.st_mtime); d->insert(UDS_ACCESS_TIME, buff.st_atime); #ifndef Q_OS_WIN d->insert(UDS_USER, KUser(buff.st_uid).loginName()); d->insert(UDS_GROUP, KUserGroup(buff.st_gid).name()); #endif } UDSEntry::UDSEntry(const UDSEntry&) = default; UDSEntry::~UDSEntry() = default; UDSEntry::UDSEntry(UDSEntry&&) = default; UDSEntry& UDSEntry::operator=(const UDSEntry&) = default; UDSEntry& UDSEntry::operator=(UDSEntry&&) = default; QString UDSEntry::stringValue(uint field) const { return d->stringValue(field); } long long UDSEntry::numberValue(uint field, long long defaultValue) const { return d->numberValue(field, defaultValue); } bool UDSEntry::isDir() const { return (numberValue(UDS_FILE_TYPE) & QT_STAT_MASK) == QT_STAT_DIR; } bool UDSEntry::isLink() const { return !stringValue(UDS_LINK_DEST).isEmpty(); } void UDSEntry::reserve(int size) { d->reserve(size); } void UDSEntry::fastInsert(uint field, const QString &value) { d->insert(field, value); } void UDSEntry::fastInsert(uint field, long long value) { d->insert(field, value); } void UDSEntry::insert(uint field, const QString &value) { d->replace(field, value); } void UDSEntry::insert(uint field, long long value) { d->replace(field, value); } void UDSEntry::replace(uint field, const QString &value) { d->replace(field, value); } void UDSEntry::replace(uint field, long long value) { d->replace(field, value); } #ifndef KIOCORE_NO_DEPRECATED QList UDSEntry::listFields() const { return d->listFields(); } #endif QVector UDSEntry::fields() const { return d->fields(); } int UDSEntry::count() const { return d->count(); } bool UDSEntry::contains(uint field) const { return d->contains(field); } void UDSEntry::clear() { d->clear(); } //END UDSEntry KIOCORE_EXPORT QDebug operator<<(QDebug stream, const KIO::UDSEntry &entry) { entry.d->debugUDSEntry(stream); return stream; } KIOCORE_EXPORT QDataStream &operator<<(QDataStream &s, const KIO::UDSEntry &a) { a.d->save(s); return s; } KIOCORE_EXPORT QDataStream &operator>>(QDataStream &s, KIO::UDSEntry &a) { a.d->load(s); return s; } KIOCORE_EXPORT bool operator==(const KIO::UDSEntry &entry, const KIO::UDSEntry &other) { if (entry.count() != other.count()) { return false; } const QVector fields = entry.fields(); for (uint field : fields) { if (!other.contains(field)) { return false; } if (field & UDSEntry::UDS_STRING) { if (entry.stringValue(field) != other.stringValue(field)) { return false; } } else { if (entry.numberValue(field) != other.numberValue(field)) { return false; } } } return true; } KIOCORE_EXPORT bool operator!=(const KIO::UDSEntry &entry, const KIO::UDSEntry &other) { return !(entry == other); } diff --git a/src/filewidgets/kdirsortfilterproxymodel.cpp b/src/filewidgets/kdirsortfilterproxymodel.cpp index b5108ffe..09fcbc21 100644 --- a/src/filewidgets/kdirsortfilterproxymodel.cpp +++ b/src/filewidgets/kdirsortfilterproxymodel.cpp @@ -1,292 +1,292 @@ /* Copyright (C) 2006 by Peter Penz Copyright (C) 2006 by Dominic Battre Copyright (C) 2006 by Martin Pool Separated from Dolphin by Nick Shaforostoff 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 "kdirsortfilterproxymodel.h" #include #include #include #include #include #include class Q_DECL_HIDDEN KDirSortFilterProxyModel::KDirSortFilterProxyModelPrivate { public: KDirSortFilterProxyModelPrivate(); int compare(const QString &, const QString &, Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive); void slotNaturalSortingChanged(); bool m_sortFoldersFirst; bool m_naturalSorting; QCollator m_collator; }; KDirSortFilterProxyModel::KDirSortFilterProxyModelPrivate::KDirSortFilterProxyModelPrivate() : m_sortFoldersFirst(true) { slotNaturalSortingChanged(); } int KDirSortFilterProxyModel::KDirSortFilterProxyModelPrivate::compare(const QString &a, const QString &b, Qt::CaseSensitivity caseSensitivity) { int result; if (m_naturalSorting) { m_collator.setCaseSensitivity(caseSensitivity); result = m_collator.compare(a, b); } else { result = QString::compare(a, b, caseSensitivity); } if (caseSensitivity == Qt::CaseSensitive || result != 0) { // Only return the result, if the strings are not equal. If they are equal by a case insensitive // comparison, still a deterministic sort order is required. A case sensitive // comparison is done as fallback. return result; } return QString::compare(a, b, Qt::CaseSensitive); } void KDirSortFilterProxyModel::KDirSortFilterProxyModelPrivate::slotNaturalSortingChanged() { KConfigGroup g(KSharedConfig::openConfig(), "KDE"); m_naturalSorting = g.readEntry("NaturalSorting", true); m_collator.setNumericMode(m_naturalSorting); } KDirSortFilterProxyModel::KDirSortFilterProxyModel(QObject *parent) : KCategorizedSortFilterProxyModel(parent), d(new KDirSortFilterProxyModelPrivate) { setDynamicSortFilter(true); // sort by the user visible string for now setSortCaseSensitivity(Qt::CaseInsensitive); sort(KDirModel::Name, Qt::AscendingOrder); } Qt::DropActions KDirSortFilterProxyModel::supportedDragOptions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction; } KDirSortFilterProxyModel::~KDirSortFilterProxyModel() { delete d; } bool KDirSortFilterProxyModel::hasChildren(const QModelIndex &parent) const { const QModelIndex sourceParent = mapToSource(parent); return sourceModel()->hasChildren(sourceParent); } bool KDirSortFilterProxyModel::canFetchMore(const QModelIndex &parent) const { const QModelIndex sourceParent = mapToSource(parent); return sourceModel()->canFetchMore(sourceParent); } int KDirSortFilterProxyModel::pointsForPermissions(const QFileInfo &info) { int points = 0; - QFile::Permission permissionsCheck[] = { QFile::ReadUser, - QFile::WriteUser, - QFile::ExeUser, - QFile::ReadGroup, - QFile::WriteGroup, - QFile::ExeGroup, - QFile::ReadOther, - QFile::WriteOther, - QFile::ExeOther - }; - - for (int i = 0; i < 9; i++) { - points += info.permission(permissionsCheck[i]) ? 1 : 0; + const QFile::Permission permissionsCheck[] = { QFile::ReadUser, + QFile::WriteUser, + QFile::ExeUser, + QFile::ReadGroup, + QFile::WriteGroup, + QFile::ExeGroup, + QFile::ReadOther, + QFile::WriteOther, + QFile::ExeOther + }; + + for (QFile::Permission perm : permissionsCheck) { + points += info.permission(perm) ? 1 : 0; } return points; } void KDirSortFilterProxyModel::setSortFoldersFirst(bool foldersFirst) { d->m_sortFoldersFirst = foldersFirst; } bool KDirSortFilterProxyModel::sortFoldersFirst() const { return d->m_sortFoldersFirst; } bool KDirSortFilterProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const { KDirModel *dirModel = static_cast(sourceModel()); const KFileItem leftFileItem = dirModel->itemForIndex(left); const KFileItem rightFileItem = dirModel->itemForIndex(right); const bool isLessThan = (sortOrder() == Qt::AscendingOrder); // Folders go before files if the corresponding setting is set. if (d->m_sortFoldersFirst) { const bool leftItemIsDir = leftFileItem.isDir(); const bool rightItemIsDir = rightFileItem.isDir(); if (leftItemIsDir && !rightItemIsDir) { return isLessThan; } else if (!leftItemIsDir && rightItemIsDir) { return !isLessThan; } } // Hidden elements go before visible ones. const bool leftItemIsHidden = leftFileItem.isHidden(); const bool rightItemIsHidden = rightFileItem.isHidden(); if (leftItemIsHidden && !rightItemIsHidden) { return isLessThan; } else if (!leftItemIsHidden && rightItemIsHidden) { return !isLessThan; } switch (left.column()) { case KDirModel::Name: { int result = d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()); if (result == 0) { // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used result = d->compare(leftFileItem.name(sortCaseSensitivity() == Qt::CaseInsensitive), rightFileItem.name(sortCaseSensitivity() == Qt::CaseInsensitive), sortCaseSensitivity()); if (result == 0) { // If KFileItem::text() is also not unique most probably a search protocol is used // that allows showing the same file names from different directories result = d->compare(leftFileItem.url().toString(), rightFileItem.url().toString(), sortCaseSensitivity()); } } return result < 0; } case KDirModel::Size: { // If we have two folders, what we have to measure is the number of // items that contains each other if (leftFileItem.isDir() && rightFileItem.isDir()) { QVariant leftValue = dirModel->data(left, KDirModel::ChildCountRole); int leftCount = (leftValue.type() == QVariant::Int) ? leftValue.toInt() : KDirModel::ChildCountUnknown; QVariant rightValue = dirModel->data(right, KDirModel::ChildCountRole); int rightCount = (rightValue.type() == QVariant::Int) ? rightValue.toInt() : KDirModel::ChildCountUnknown; // In the case they two have the same child items, we sort them by // their names. So we have always everything ordered. We also check // if we are taking in count their cases. if (leftCount == rightCount) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } // If one of them has unknown child items, place them on the end. If we // were comparing two unknown childed items, the previous comparison // sorted them by QCollator between them. This case is when we // have an unknown childed item, and another known. if (leftCount == KDirModel::ChildCountUnknown) { return false; } if (rightCount == KDirModel::ChildCountUnknown) { return true; } // If they had different number of items, we sort them depending // on how many items had each other. return leftCount < rightCount; } // If what we are measuring is two files and they have the same size, // sort them by their file names. if (leftFileItem.size() == rightFileItem.size()) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } // If their sizes are different, sort them by their sizes, as expected. return leftFileItem.size() < rightFileItem.size(); } case KDirModel::ModifiedTime: { QDateTime leftModifiedTime = leftFileItem.time(KFileItem::ModificationTime).toLocalTime(); QDateTime rightModifiedTime = rightFileItem.time(KFileItem::ModificationTime).toLocalTime(); if (leftModifiedTime == rightModifiedTime) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } return leftModifiedTime < rightModifiedTime; } case KDirModel::Permissions: { const int leftPermissions = leftFileItem.permissions(); const int rightPermissions = rightFileItem.permissions(); if (leftPermissions == rightPermissions) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } return leftPermissions > rightPermissions; } case KDirModel::Owner: { if (leftFileItem.user() == rightFileItem.user()) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } return d->compare(leftFileItem.user(), rightFileItem.user()) < 0; } case KDirModel::Group: { if (leftFileItem.group() == rightFileItem.group()) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } return d->compare(leftFileItem.group(), rightFileItem.group()) < 0; } case KDirModel::Type: { if (leftFileItem.mimetype() == rightFileItem.mimetype()) { return d->compare(leftFileItem.text(), rightFileItem.text(), sortCaseSensitivity()) < 0; } return d->compare(leftFileItem.mimeComment(), rightFileItem.mimeComment()) < 0; } } // We have set a SortRole and trust the ProxyModel to do // the right thing for now. return KCategorizedSortFilterProxyModel::subSortLessThan(left, right); } #include "moc_kdirsortfilterproxymodel.cpp" diff --git a/src/ioslaves/help/kio_help.cpp b/src/ioslaves/help/kio_help.cpp index e62405a7..b0b25097 100644 --- a/src/ioslaves/help/kio_help.cpp +++ b/src/ioslaves/help/kio_help.cpp @@ -1,399 +1,399 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Matthias Hoelzer-Kluepfel Copyright (C) 2001 Stephan Kulow Copyright (C) 2003 Cornelius Schumacher 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 "kio_help.h" #include "xslt_help.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KIO; QString HelpProtocol::langLookup(const QString &fname) { QStringList search; // assemble the local search paths const QStringList localDoc = KDocTools::documentationDirs(); QStringList langs = KLocalizedString::languages(); langs.append(QStringLiteral("en")); langs.removeAll(QStringLiteral("C")); // this is kind of compat hack as we install our docs in en/ but the // default language is en_US - for (QStringList::Iterator it = langs.begin(); it != langs.end(); ++it) - if (*it == QLatin1String("en_US")) { - *it = QStringLiteral("en"); + for (QString &lang : langs) { + if (lang == QLatin1String("en_US")) { + lang = QStringLiteral("en"); } + } // look up the different languages int ldCount = localDoc.count(); search.reserve(ldCount * langs.size()); for (int id = 0; id < ldCount; id++) { - QStringList::ConstIterator lang; - for (lang = langs.constBegin(); lang != langs.constEnd(); ++lang) { - search.append(QStringLiteral("%1/%2/%3").arg(localDoc[id], *lang, fname)); + for (const QString &lang : qAsConst(langs)) { + search.append(QStringLiteral("%1/%2/%3").arg(localDoc[id], lang, fname)); } } // try to locate the file for (QStringList::ConstIterator it = search.constBegin(); it != search.constEnd(); ++it) { //qDebug() << "Looking for help in: " << *it; QFileInfo info(*it); if (info.exists() && info.isFile() && info.isReadable()) { return *it; } if ((*it).endsWith(QLatin1String(".html"))) { const QString file = (*it).leftRef((*it).lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook"); //qDebug() << "Looking for help in: " << file; info.setFile(file); if (info.exists() && info.isFile() && info.isReadable()) { return *it; } } } return QString(); } QString HelpProtocol::lookupFile(const QString &fname, const QString &query, bool &redirect) { redirect = false; const QString path = fname; QString result = langLookup(path); if (result.isEmpty()) { result = langLookup(path + QLatin1String("/index.html")); if (!result.isEmpty()) { QUrl red; red.setScheme(QStringLiteral("help")); red.setPath(path + QLatin1String("/index.html")); red.setQuery(query); redirection(red); //qDebug() << "redirect to " << red; redirect = true; } else { const QString documentationNotFound = QStringLiteral("kioslave5/help/documentationnotfound/index.html"); if (!langLookup(documentationNotFound).isEmpty()) { QUrl red; red.setScheme(QStringLiteral("help")); red.setPath(documentationNotFound); red.setQuery(query); redirection(red); redirect = true; } else { sendError(i18n("There is no documentation available for %1.", path.toHtmlEscaped())); return QString(); } } } else { //qDebug() << "result " << result; } return result; } void HelpProtocol::sendError(const QString &t) { data(QStringLiteral( "\n%1").arg(t.toHtmlEscaped()).toUtf8()); } HelpProtocol *slave = nullptr; HelpProtocol::HelpProtocol(bool ghelp, const QByteArray &pool, const QByteArray &app) : SlaveBase(ghelp ? QByteArrayLiteral("ghelp") : QByteArrayLiteral("help"), pool, app), mGhelp(ghelp) { slave = this; } void HelpProtocol::get(const QUrl &url) { ////qDebug() << "path=" << url.path() //<< "query=" << url.query(); bool redirect; QString doc = QDir::cleanPath(url.path()); if (doc.contains(QLatin1String(".."))) { error(KIO::ERR_DOES_NOT_EXIST, url.toString()); return; } if (!mGhelp) { if (!doc.startsWith(QLatin1Char('/'))) { doc.prepend(QLatin1Char('/')); } if (doc.endsWith(QLatin1Char('/'))) { doc += QLatin1String("index.html"); } } infoMessage(i18n("Looking up correct file")); if (!mGhelp) { doc = lookupFile(doc, url.query(), redirect); if (redirect) { finished(); return; } } if (doc.isEmpty()) { error(KIO::ERR_DOES_NOT_EXIST, url.toString()); return; } QUrl target; target.setPath(doc); if (url.hasFragment()) { target.setFragment(url.fragment()); } //qDebug() << "target " << target; QString file = target.isLocalFile() ? target.toLocalFile() : target.path(); if (mGhelp) { if (!file.endsWith(QLatin1String(".xml"))) { get_file(file); return; } } else { const QString docbook_file = file.leftRef(file.lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook"); if (!QFile::exists(file)) { file = docbook_file; } else { QFileInfo fi(file); if (fi.isDir()) { file = file + QLatin1String("/index.docbook"); } else { if (!file.endsWith(QLatin1String(".html")) || !compareTimeStamps(file, docbook_file)) { get_file(file); return; } else { file = docbook_file; } } } } infoMessage(i18n("Preparing document")); mimeType(QStringLiteral("text/html")); if (mGhelp) { QString xsl = QStringLiteral("customization/kde-nochunk.xsl"); mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(xsl)); //qDebug() << "parsed " << mParsed.length(); if (mParsed.isEmpty()) { sendError(i18n("The requested help file could not be parsed:
%1", file)); } else { int pos1 = mParsed.indexOf(QLatin1String("charset=")); if (pos1 > 0) { int pos2 = mParsed.indexOf(QLatin1Char('"'), pos1); if (pos2 > 0) { mParsed.replace(pos1, pos2 - pos1, QStringLiteral("charset=UTF-8")); } } data(mParsed.toUtf8()); } } else { //qDebug() << "look for cache for " << file; mParsed = lookForCache(file); //qDebug() << "cached parsed " << mParsed.length(); if (mParsed.isEmpty()) { mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(QStringLiteral("customization/kde-chunk.xsl"))); if (!mParsed.isEmpty()) { infoMessage(i18n("Saving to cache")); #ifdef Q_OS_WIN QFileInfo fi(file); // make sure filenames do not contain the base path, otherwise // accessing user data from another location invalids cached files // Accessing user data under a different path is possible // when using usb sticks - this may affect unix/mac systems also const QString installPath = KDocTools::documentationDirs().last(); QString cache = QLatin1Char('/') + fi.absolutePath().remove(installPath, Qt::CaseInsensitive).replace(QLatin1Char('/'), QLatin1Char('_')) + QLatin1Char('_') + fi.baseName() + QLatin1Char('.'); #else QString cache = file.left(file.length() - 7); #endif KDocTools::saveToCache(mParsed, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_help") + cache + QLatin1String("cache.bz2")); } } else { infoMessage(i18n("Using cached version")); } //qDebug() << "parsed " << mParsed.length(); if (mParsed.isEmpty()) { sendError(i18n("The requested help file could not be parsed:
%1", file)); } else { QString anchor; QString query = url.query(); // if we have a query, look if it contains an anchor if (!query.isEmpty()) if (query.startsWith(QLatin1String("?anchor="))) { anchor = query.mid(8).toLower(); QUrl redirURL(url); redirURL.setQuery(QString()); redirURL.setFragment(anchor); redirection(redirURL); finished(); return; } if (anchor.isEmpty() && url.hasFragment()) { anchor = url.fragment(); } //qDebug() << "anchor: " << anchor; if (!anchor.isEmpty()) { int index = 0; while (true) { index = mParsed.indexOf(QRegExp(QStringLiteral("").arg(anchor)) { index = mParsed.lastIndexOf(QLatin1String(" 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 "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) { + for (const QString &header : headers) { // 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)) { + if (!header.contains(QLatin1Char(':')) || + header.startsWith(QLatin1String("host"), Qt::CaseInsensitive) || + header.startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) || + header.startsWith(QLatin1String("via"), Qt::CaseInsensitive)) { continue; } - sanitizedHeaders += (*it) + QLatin1String("\r\n"); + sanitizedHeaders += header + 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.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()) { const QByteArray request = "\r\n" "\r\n" + query.toUtf8() + "\r\n"; davSetRequest(request); } else { // We are only after certain features... QByteArray request = QByteArrayLiteral("" ""); // 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"))).toSecsSinceEpoch()); } 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() == QLatin1Char('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"))).toSecsSinceEpoch()); } 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.isEmpty()) { 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) { QUrl redir = m_request.redirectUrl; resetSessionSettings(); m_request.url = redir; 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; 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("
    "); for (const QString &error : qAsConst(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; for (const HTTPRequest &r : qAsConst(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()) { for (const QString &url : qAsConst(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; for (const QString &proxyUrl : qAsConst(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: case DAV_REPORT: 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: 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 (hasDavData || 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:"; const QStringList list = header.split(QStringLiteral("\r\n"), QString::SkipEmptyParts); for (const QString &s : list) { 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; } for (const QString &str : qAsConst(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... for (const QByteArray &statement : qAsConst(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); } for (const QString &opt : qAsConst(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.toSecsSinceEpoch()); setMetaData(QStringLiteral("expire-date"), tmp); // slightly changed semantics from old creationDate, probably more correct now tmp.setNum(m_request.cacheTag.servedDate.toSecsSinceEpoch()); 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.setSecsSinceEpoch(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/trash/kio_trash.cpp b/src/ioslaves/trash/kio_trash.cpp index d172c997..4b7a49d0 100644 --- a/src/ioslaves/trash/kio_trash.cpp +++ b/src/ioslaves/trash/kio_trash.cpp @@ -1,680 +1,678 @@ /* This file is part of the KDE project Copyright (C) 2004 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) 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 "kio_trash.h" #include "kiotrashdebug.h" #include "../../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.trash" FILE "trash.json") }; extern "C" { int Q_DECL_EXPORT kdemain(int argc, char **argv) { // necessary to use other kio slaves QCoreApplication app(argc, argv); KIO::setDefaultJobUiDelegateExtension(nullptr); // start the slave TrashProtocol slave(argv[1], argv[2], argv[3]); slave.dispatchLoop(); return 0; } } static bool isTopLevelEntry(const QUrl &url) { const QString dir = url.adjusted(QUrl::RemoveFilename).path(); return dir.length() <= 1; } #define INIT_IMPL \ if ( !impl.init() ) { \ error( impl.lastErrorCode(), impl.lastErrorMessage() ); \ return; \ } TrashProtocol::TrashProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) : SlaveBase(protocol, pool, app) { struct passwd *user = getpwuid(getuid()); if (user) { m_userName = QString::fromLatin1(user->pw_name); } struct group *grp = getgrgid(getgid()); if (grp) { m_groupName = QString::fromLatin1(grp->gr_name); } } TrashProtocol::~TrashProtocol() { } void TrashProtocol::enterLoop() { QEventLoop eventLoop; connect(this, &TrashProtocol::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } void TrashProtocol::restore(const QUrl &trashURL) { int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(trashURL, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", trashURL.toString())); return; } TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } QUrl dest = QUrl::fromLocalFile(info.origPath); if (!relativePath.isEmpty()) { dest.setPath(concatPaths(dest.path(), relativePath)); } // Check that the destination directory exists, to improve the error code in case it doesn't. const QString destDir = dest.adjusted(QUrl::RemoveFilename).path(); QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(destDir).constData(), &buff) == -1) { error(KIO::ERR_SLAVE_DEFINED, i18n("The directory %1 does not exist anymore, so it is not possible to restore this item to its original location. " "You can either recreate that directory and use the restore operation again, or drag the item anywhere else to restore it.", destDir)); return; } copyOrMoveFromTrash(trashURL, dest, false /*overwrite*/, Move); } void TrashProtocol::rename(const QUrl &oldURL, const QUrl &newURL, KIO::JobFlags flags) { INIT_IMPL; qCDebug(KIO_TRASH) << "TrashProtocol::rename(): old=" << oldURL << " new=" << newURL << " overwrite=" << (flags & KIO::Overwrite); if (oldURL.scheme() == QLatin1String("trash") && newURL.scheme() == QLatin1String("trash")) { if (!isTopLevelEntry(oldURL) || !isTopLevelEntry(newURL)) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } int oldTrashId; QString oldFileId, oldRelativePath; bool oldOk = TrashImpl::parseURL(oldURL, oldTrashId, oldFileId, oldRelativePath); if (!oldOk) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", oldURL.toString())); return; } if (!oldRelativePath.isEmpty()) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } // Dolphin/KIO can't specify a trashid in the new URL so here path == filename //bool newOk = TrashImpl::parseURL(newURL, newTrashId, newFileId, newRelativePath); const QString newFileId = newURL.path().mid(1); if (newFileId.contains(QLatin1Char('/'))) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } bool ok = impl.moveInTrash(oldTrashId, oldFileId, newFileId); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } const QUrl finalUrl = TrashImpl::makeURL(oldTrashId, newFileId, QString()); org::kde::KDirNotify::emitFileRenamed(oldURL, finalUrl); finished(); return; } if (oldURL.scheme() == QLatin1String("trash") && newURL.isLocalFile()) { copyOrMoveFromTrash(oldURL, newURL, (flags & KIO::Overwrite), Move); } else if (oldURL.isLocalFile() && newURL.scheme() == QLatin1String("trash")) { copyOrMoveToTrash(oldURL, newURL, Move); } else { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Invalid combination of protocols.")); } } void TrashProtocol::copy(const QUrl &src, const QUrl &dest, int /*permissions*/, KIO::JobFlags flags) { INIT_IMPL; qCDebug(KIO_TRASH) << "TrashProtocol::copy(): " << src << " " << dest; if (src.scheme() == QLatin1String("trash") && dest.scheme() == QLatin1String("trash")) { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("This file is already in the trash bin.")); return; } if (src.scheme() == QLatin1String("trash") && dest.isLocalFile()) { copyOrMoveFromTrash(src, dest, (flags & KIO::Overwrite), Copy); } else if (src.isLocalFile() && dest.scheme() == QLatin1String("trash")) { copyOrMoveToTrash(src, dest, Copy); } else { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Invalid combination of protocols.")); } } void TrashProtocol::copyOrMoveFromTrash(const QUrl &src, const QUrl &dest, bool overwrite, CopyOrMove action) { // Extracting (e.g. via dnd). Ignore original location stored in info file. int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(src, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", src.toString())); return; } const QString destPath = dest.path(); if (QFile::exists(destPath)) { if (overwrite) { ok = QFile::remove(destPath); Q_ASSERT(ok); // ### TODO } else { error(KIO::ERR_FILE_ALREADY_EXIST, destPath); return; } } if (action == Move) { qCDebug(KIO_TRASH) << "calling moveFromTrash(" << destPath << " " << trashId << " " << fileId << ")"; ok = impl.moveFromTrash(destPath, trashId, fileId, relativePath); } else { // Copy qCDebug(KIO_TRASH) << "calling copyFromTrash(" << destPath << " " << trashId << " " << fileId << ")"; ok = impl.copyFromTrash(destPath, trashId, fileId, relativePath); } if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { if (action == Move && relativePath.isEmpty()) { (void)impl.deleteInfo(trashId, fileId); } finished(); } } void TrashProtocol::copyOrMoveToTrash(const QUrl &src, const QUrl &dest, CopyOrMove action) { qCDebug(KIO_TRASH) << "trashing a file" << src << dest; // Trashing a file // We detect the case where this isn't normal trashing, but // e.g. if kwrite tries to save (moving tempfile over destination) if (isTopLevelEntry(dest) && src.fileName() == dest.fileName()) { // new toplevel entry const QString srcPath = src.path(); // In theory we should use TrashImpl::parseURL to give the right filename to createInfo, // in case the trash URL didn't contain the same filename as srcPath. // But this can only happen with copyAs/moveAs, not available in the GUI // for the trash (New/... or Rename from iconview/listview). int trashId; QString fileId; if (!impl.createInfo(srcPath, trashId, fileId)) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { bool ok; if (action == Move) { qCDebug(KIO_TRASH) << "calling moveToTrash(" << srcPath << " " << trashId << " " << fileId << ")"; ok = impl.moveToTrash(srcPath, trashId, fileId); } else { // Copy qCDebug(KIO_TRASH) << "calling copyToTrash(" << srcPath << " " << trashId << " " << fileId << ")"; ok = impl.copyToTrash(srcPath, trashId, fileId); } if (!ok) { (void)impl.deleteInfo(trashId, fileId); error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { // Inform caller of the final URL. Used by konq_undo. const QUrl url = impl.makeURL(trashId, fileId, QString()); setMetaData(QLatin1String("trashURL-") + srcPath, url.url()); finished(); } } } else { qCDebug(KIO_TRASH) << "returning KIO::ERR_ACCESS_DENIED, it's not allowed to add a file to an existing trash directory"; // It's not allowed to add a file to an existing trash directory. error(KIO::ERR_ACCESS_DENIED, dest.toString()); } } void TrashProtocol::createTopLevelDirEntry(KIO::UDSEntry &entry) { entry.clear(); entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, i18n("Trash")); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, impl.isEmpty() ? QStringLiteral("user-trash") : QStringLiteral("user-trash-full") ); entry.fastInsert(KIO::UDSEntry::UDS_USER, m_userName); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, m_groupName); } void TrashProtocol::stat(const QUrl &url) { INIT_IMPL; const QString path = url.path(); if (path.isEmpty() || path == QLatin1String("/")) { // The root is "virtual" - it's not a single physical directory KIO::UDSEntry entry; createTopLevelDirEntry(entry); statEntry(entry); finished(); } else { int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { // ######## do we still need this? qCDebug(KIO_TRASH) << url << " looks fishy, returning does-not-exist"; // A URL like trash:/file simply means that CopyJob is trying to see if // the destination exists already (it made up the URL by itself). error(KIO::ERR_DOES_NOT_EXIST, url.toString()); //error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.toString() ) ); return; } qCDebug(KIO_TRASH) << "parsed" << url << "got" << trashId << fileId << relativePath; const QString filePath = impl.physicalPath(trashId, fileId, relativePath); if (filePath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } // For a toplevel file, use the fileId as display name (to hide the trashId) // For a file in a subdir, use the fileName as is. QString fileDisplayName = relativePath.isEmpty() ? fileId : url.fileName(); QUrl fileURL; if (url.path().length() > 1) { fileURL = url; } KIO::UDSEntry entry; TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (ok) { ok = createUDSEntry(filePath, fileDisplayName, fileURL.fileName(), entry, info); } if (!ok) { error(KIO::ERR_COULD_NOT_STAT, url.toString()); return; } statEntry(entry); finished(); } } void TrashProtocol::del(const QUrl &url, bool /*isfile*/) { INIT_IMPL; int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } ok = relativePath.isEmpty(); if (!ok) { error(KIO::ERR_ACCESS_DENIED, url.toString()); return; } ok = impl.del(trashId, fileId); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } finished(); } void TrashProtocol::listDir(const QUrl &url) { INIT_IMPL; qCDebug(KIO_TRASH) << "listdir: " << url; const QString path = url.path(); if (path.isEmpty() || path == QLatin1String("/")) { listRoot(); return; } int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } //was: const QString physicalPath = impl.physicalPath( trashId, fileId, relativePath ); // Get info for deleted directory - the date of deletion and orig path will be used // for all the items in it, and we need the physicalPath. TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (!ok || info.physicalPath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } if (!relativePath.isEmpty()) { info.physicalPath += QLatin1Char('/') + relativePath; } // List subdir. Can't use kio_file here since we provide our own info... qCDebug(KIO_TRASH) << "listing " << info.physicalPath; const QStringList entryNames = impl.listDir(info.physicalPath); totalSize(entryNames.count()); KIO::UDSEntry entry; - for (QStringList::const_iterator entryIt = entryNames.begin(), entryEnd = entryNames.end(); - entryIt != entryEnd; ++entryIt) { - const QString fileName = *entryIt; + for (const QString &fileName : entryNames) { if (fileName == QLatin1String("..")) { continue; } const QString filePath = info.physicalPath + QLatin1Char('/') + fileName; // shouldn't be necessary //const QString url = TrashImpl::makeURL( trashId, fileId, relativePath + '/' + fileName ); entry.clear(); TrashedFileInfo infoForItem(info); infoForItem.origPath += QLatin1Char('/') + fileName; if (createUDSEntry(filePath, fileName, fileName, entry, infoForItem)) { listEntry(entry); } } entry.clear(); finished(); } bool TrashProtocol::createUDSEntry(const QString &physicalPath, const QString &displayFileName, const QString &internalFileName, KIO::UDSEntry &entry, const TrashedFileInfo &info) { QByteArray physicalPath_c = QFile::encodeName(physicalPath); QT_STATBUF buff; if (QT_LSTAT(physicalPath_c.constData(), &buff) == -1) { qCWarning(KIO_TRASH) << "couldn't stat " << physicalPath; return false; } if (S_ISLNK(buff.st_mode)) { char buffer2[ 1000 ]; int n = ::readlink(physicalPath_c.constData(), buffer2, 999); if (n != -1) { buffer2[ n ] = 0; } entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(buffer2)); // Follow symlink // That makes sense in kio_file, but not in the trash, especially for the size // #136876 #if 0 if (KDE_stat(physicalPath_c, &buff) == -1) { // It is a link pointing to nowhere buff.st_mode = S_IFLNK | S_IRWXU | S_IRWXG | S_IRWXO; buff.st_mtime = 0; buff.st_atime = 0; buff.st_size = 0; } #endif } mode_t type = buff.st_mode & S_IFMT; // extract file type mode_t access = buff.st_mode & 07777; // extract permissions access &= 07555; // make it readonly, since it's in the trashcan Q_ASSERT(!internalFileName.isEmpty()); entry.fastInsert(KIO::UDSEntry::UDS_NAME, internalFileName); // internal filename, like "0-foo" entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayFileName); // user-visible filename, like "foo" entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type); //if ( !url.isEmpty() ) // entry.insert( KIO::UDSEntry::UDS_URL, url ); QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(physicalPath); if (mt.isValid()) { entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, mt.name()); } entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, buff.st_size); entry.fastInsert(KIO::UDSEntry::UDS_USER, m_userName); // assumption entry.fastInsert(KIO::UDSEntry::UDS_GROUP, m_groupName); // assumption entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, buff.st_mtime); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, buff.st_atime); // ## or use it for deletion time? entry.fastInsert(KIO::UDSEntry::UDS_EXTRA, info.origPath); entry.fastInsert(KIO::UDSEntry::UDS_EXTRA + 1, info.deletionDate.toString(Qt::ISODate)); return true; } void TrashProtocol::listRoot() { INIT_IMPL; const TrashedFileInfoList lst = impl.list(); totalSize(lst.count()); KIO::UDSEntry entry; createTopLevelDirEntry(entry); listEntry(entry); - for (TrashedFileInfoList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { - const QUrl url = TrashImpl::makeURL((*it).trashId, (*it).fileId, QString()); + for (const TrashedFileInfo &fileInfo : lst) { + const QUrl url = TrashImpl::makeURL(fileInfo.trashId, fileInfo.fileId, QString()); entry.clear(); - const QString fileDisplayName = (*it).fileId; + const QString fileDisplayName = fileInfo.fileId; - if (createUDSEntry((*it).physicalPath, fileDisplayName, url.fileName(), entry, *it)) { + if (createUDSEntry(fileInfo.physicalPath, fileDisplayName, url.fileName(), entry, fileInfo)) { listEntry(entry); } } entry.clear(); finished(); } void TrashProtocol::special(const QByteArray &data) { INIT_IMPL; QDataStream stream(data); int cmd; stream >> cmd; switch (cmd) { case 1: if (impl.emptyTrash()) { finished(); } else { error(impl.lastErrorCode(), impl.lastErrorMessage()); } break; case 2: impl.migrateOldTrash(); finished(); break; case 3: { QUrl url; stream >> url; restore(url); break; } default: qCWarning(KIO_TRASH) << "Unknown command in special(): " << cmd; error(KIO::ERR_UNSUPPORTED_ACTION, QString::number(cmd)); break; } } void TrashProtocol::put(const QUrl &url, int /*permissions*/, KIO::JobFlags) { INIT_IMPL; qCDebug(KIO_TRASH) << "put: " << url; // create deleted file. We need to get the mtime and original location from metadata... // Maybe we can find the info file for url.fileName(), in case ::rename() was called first, and failed... error(KIO::ERR_ACCESS_DENIED, url.toString()); } void TrashProtocol::get(const QUrl &url) { INIT_IMPL; qCDebug(KIO_TRASH) << "get() : " << url; if (!url.isValid()) { //qCDebug(KIO_TRASH) << kBacktrace(); error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.url())); return; } if (url.path().length() <= 1) { error(KIO::ERR_IS_DIRECTORY, url.toString()); return; } int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } const QString physicalPath = impl.physicalPath(trashId, fileId, relativePath); if (physicalPath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } // Usually we run jobs in TrashImpl (for e.g. future kdedmodule) // But for this one we wouldn't use DCOP for every bit of data... QUrl fileURL = QUrl::fromLocalFile(physicalPath); KIO::TransferJob *job = KIO::get(fileURL, KIO::NoReload, KIO::HideProgressInfo); connect(job, &KIO::TransferJob::data, this, &TrashProtocol::slotData); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &TrashProtocol::slotMimetype); connect(job, &KJob::result, this, &TrashProtocol::jobFinished); enterLoop(); } void TrashProtocol::slotData(KIO::Job *, const QByteArray &arr) { data(arr); } void TrashProtocol::slotMimetype(KIO::Job *, const QString &mt) { mimeType(mt); } void TrashProtocol::jobFinished(KJob *job) { if (job->error()) { error(job->error(), job->errorText()); } else { finished(); } emit leaveModality(); } #if 0 void TrashProtocol::mkdir(const QUrl &url, int /*permissions*/) { INIT_IMPL; // create info about deleted dir // ############ Problem: we don't know the original path. // Let's try to avoid this case (we should get to copy() instead, for local files) qCDebug(KIO_TRASH) << "mkdir: " << url; QString dir = url.adjusted(QUrl::RemoveFilename).path(); if (dir.length() <= 1) { // new toplevel entry // ## we should use TrashImpl::parseURL to give the right filename to createInfo int trashId; QString fileId; if (!impl.createInfo(url.path(), trashId, fileId)) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { if (!impl.mkdir(trashId, fileId, permissions)) { (void)impl.deleteInfo(trashId, fileId); error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { finished(); } } } else { // Well it's not allowed to add a directory to an existing deleted directory. error(KIO::ERR_ACCESS_DENIED, url.toString()); } } #endif void TrashProtocol::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); } } void TrashProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_TRASH) << "fileSystemFreeSpace:" << url; INIT_IMPL; TrashImpl::TrashSpaceInfo spaceInfo; if (!impl.trashSpaceInfo(url.path(), spaceInfo)) { error(KIO::ERR_COULD_NOT_STAT, url.toDisplayString()); return; } setMetaData(QStringLiteral("total"), QString::number(spaceInfo.totalSize)); setMetaData(QStringLiteral("available"), QString::number(spaceInfo.availableSize)); finished(); } #include "kio_trash.moc" diff --git a/src/ioslaves/trash/tests/testtrash.cpp b/src/ioslaves/trash/tests/testtrash.cpp index 77c86592..39605bf6 100644 --- a/src/ioslaves/trash/tests/testtrash.cpp +++ b/src/ioslaves/trash/tests/testtrash.cpp @@ -1,1337 +1,1336 @@ /* This file is part of the KDE project Copyright (C) 2004 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) 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 "testtrash.h" #include #include "kio_trash.h" #include "../../../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // There are two ways to test encoding things: // * with utf8 filenames // * with latin1 filenames -- not sure this still works. // #define UTF8TEST 1 int initLocale() { #ifdef UTF8TEST // Assume utf8 system setenv("LC_ALL", "C.utf-8", 1); setenv("KDE_UTF8_FILENAMES", "true", 1); #else // Ensure a known QFile::encodeName behavior for trashUtf8FileFromHome // However this assume your $HOME doesn't use characters from other locales... setenv("LC_ALL", "en_US.ISO-8859-1", 1); unsetenv("KDE_UTF8_FILENAMES"); #endif setenv("KIOSLAVE_ENABLE_TESTMODE", "1", 1); // ensure the ioslaves call QStandardPaths::setTestModeEnabled(true) too setenv("KDE_SKIP_KDERC", "1", 1); unsetenv("KDE_COLOR_DEBUG"); return 0; } Q_CONSTRUCTOR_FUNCTION(initLocale) QString TestTrash::homeTmpDir() const { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/testtrash/"); } QString TestTrash::readOnlyDirPath() const { return homeTmpDir() + QLatin1String("readonly"); } QString TestTrash::otherTmpDir() const { // This one needs to be on another partition for the test to be meaningful QString tempDir = m_tempDir.path(); if (!tempDir.endsWith(QLatin1Char('/'))) { tempDir.append(QLatin1Char('/')); } return tempDir; } QString TestTrash::utf8FileName() const { return QLatin1String("test") + QChar(0x2153); // "1/3" character, not part of latin1 } QString TestTrash::umlautFileName() const { return QLatin1String("umlaut") + QChar(0xEB); } static void removeFile(const QString &trashDir, const QString &fileName) { QDir dir; dir.remove(trashDir + fileName); QVERIFY(!QDir(trashDir + fileName).exists()); } static void removeDir(const QString &trashDir, const QString &dirName) { QDir dir; dir.rmdir(trashDir + dirName); QVERIFY(!QDir(trashDir + dirName).exists()); } static void removeDirRecursive(const QString &dir) { if (QFile::exists(dir)) { // Make it work even with readonly dirs, like trashReadOnlyDirFromHome() creates QUrl u = QUrl::fromLocalFile(dir); //qDebug() << "chmod +0200 on" << u; KFileItem fileItem(u, QStringLiteral("inode/directory"), KFileItem::Unknown); KFileItemList fileItemList; fileItemList.append(fileItem); KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo); chmodJob->exec(); KIO::Job *delJob = KIO::del(u, KIO::HideProgressInfo); if (!delJob->exec()) { qFatal("Couldn't delete %s", qPrintable(dir)); } } } void TestTrash::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); QStandardPaths::setTestModeEnabled(true); QVERIFY(m_tempDir.isValid()); #ifndef Q_OS_OSX m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash"); qDebug() << "setup: using trash directory " << m_trashDir; #endif // Look for another writable partition than $HOME (not mandatory) TrashImpl impl; impl.init(); TrashImpl::TrashDirMap trashDirs = impl.trashDirectories(); #ifdef Q_OS_OSX QVERIFY(trashDirs.contains(0)); m_trashDir = trashDirs.value(0); qDebug() << "setup: using trash directory " << m_trashDir; #endif TrashImpl::TrashDirMap topDirs = impl.topDirectories(); bool foundTrashDir = false; m_otherPartitionId = 0; m_tmpIsWritablePartition = false; m_tmpTrashId = -1; QVector writableTopDirs; for (TrashImpl::TrashDirMap::ConstIterator it = trashDirs.constBegin(); it != trashDirs.constEnd(); ++it) { if (it.key() == 0) { QVERIFY(it.value() == m_trashDir); QVERIFY(topDirs.find(0) == topDirs.end()); foundTrashDir = true; } else { QVERIFY(topDirs.find(it.key()) != topDirs.end()); const QString topdir = topDirs[it.key()]; if (QFileInfo(topdir).isWritable()) { writableTopDirs.append(it.key()); if (topdir == QLatin1String("/tmp/")) { m_tmpIsWritablePartition = true; m_tmpTrashId = it.key(); qDebug() << "/tmp is on its own partition (trashid=" << m_tmpTrashId << "), some tests will be skipped"; removeFile(it.value(), QStringLiteral("/info/fileFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/fileFromOther")); removeFile(it.value(), QStringLiteral("/info/symlinkFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/symlinkFromOther")); removeFile(it.value(), QStringLiteral("/info/trashDirFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/trashDirFromOther/testfile")); removeDir(it.value(), QStringLiteral("/files/trashDirFromOther")); } } } } for (QVector::const_iterator it = writableTopDirs.constBegin(); it != writableTopDirs.constEnd(); ++it) { const QString topdir = topDirs[ *it ]; const QString trashdir = trashDirs[ *it ]; QVERIFY(!topdir.isEmpty()); QVERIFY(!trashDirs.isEmpty()); if (topdir != QLatin1String("/tmp/") || // we'd prefer not to use /tmp here, to separate the tests (writableTopDirs.count() > 1)) { // but well, if we have no choice, take it m_otherPartitionTopDir = topdir; m_otherPartitionTrashDir = trashdir; m_otherPartitionId = *it; qDebug() << "OK, found another writable partition: topDir=" << m_otherPartitionTopDir << " trashDir=" << m_otherPartitionTrashDir << " id=" << m_otherPartitionId << endl; break; } } // Check that m_trashDir got listed QVERIFY(foundTrashDir); if (m_otherPartitionTrashDir.isEmpty()) { qWarning() << "No writable partition other than $HOME found, some tests will be skipped"; } // Start with a clean base dir qDebug() << "initial cleanup"; removeDirRecursive(homeTmpDir()); QDir dir; // TT: why not a static method? bool ok = dir.mkdir(homeTmpDir()); if (!ok) { qFatal("Couldn't create directory: %s", qPrintable(homeTmpDir())); } QVERIFY(QFileInfo(otherTmpDir()).isDir()); // Start with a clean trash too qDebug() << "removing trash dir"; removeDirRecursive(m_trashDir); } void TestTrash::cleanupTestCase() { // Clean up removeDirRecursive(homeTmpDir()); removeDirRecursive(otherTmpDir()); removeDirRecursive(m_trashDir); } void TestTrash::urlTestFile() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QString()); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(QString::number(trashId), QStringLiteral("1")); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QString()); } void TestTrash::urlTestDirectory() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile")); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(trashId, 1); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QStringLiteral("subfile")); } void TestTrash::urlTestSubDirectory() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile/foobar")); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile/foobar")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(trashId, 1); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QStringLiteral("subfile/foobar")); } static void checkInfoFile(const QString &infoPath, const QString &origFilePath) { qDebug() << infoPath; QFileInfo info(infoPath); QVERIFY2(info.exists(), qPrintable(infoPath)); QVERIFY(info.isFile()); KConfig infoFile(info.absoluteFilePath()); KConfigGroup group = infoFile.group("Trash Info"); if (!group.exists()) { qFatal("no Trash Info group in %s", qPrintable(info.absoluteFilePath())); } const QString origPath = group.readEntry("Path"); QVERIFY(!origPath.isEmpty()); QCOMPARE(origPath.toUtf8(), QUrl::toPercentEncoding(origFilePath, "/")); if (origFilePath.contains(QChar(0x2153)) || origFilePath.contains(QLatin1Char('%')) || origFilePath.contains(QLatin1String("umlaut"))) { QVERIFY(origPath.contains(QLatin1Char('%'))); } else { QVERIFY(!origPath.contains(QLatin1Char('%'))); } const QString date = group.readEntry("DeletionDate"); QVERIFY(!date.isEmpty()); QVERIFY(date.contains(QLatin1String("T"))); } static void createTestFile(const QString &path) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { qFatal("Can't create %s", qPrintable(path)); } f.write("Hello world\n", 12); f.close(); QVERIFY(QFile::exists(path)); } void TestTrash::trashFile(const QString &origFilePath, const QString &fileId) { // setup if (!QFile::exists(origFilePath)) { createTestFile(origFilePath); } QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); bool ok = job->exec(); if (!ok) { qCritical() << "moving " << u << " to trash failed with error " << job->error() << " " << job->errorString() << endl; } QVERIFY(ok); if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; } else { checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(files.isFile()); QVERIFY(files.size() == 12); } // coolo suggests testing that the original file is actually gone, too :) QVERIFY(!QFile::exists(origFilePath)); QMap metaData = job->metaData(); QVERIFY(!metaData.isEmpty()); bool found = false; QMap::ConstIterator it = metaData.constBegin(); for (; it != metaData.constEnd(); ++it) { if (it.key().startsWith(QLatin1String("trashURL"))) { QUrl trashURL(it.value()); qDebug() << trashURL; QVERIFY(!trashURL.isEmpty()); QVERIFY(trashURL.scheme() == QLatin1String("trash")); int trashId = 0; if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { trashId = m_tmpTrashId; } QCOMPARE(trashURL.path(), QString(QStringLiteral("/") + QString::number(trashId) + QLatin1Char('-') + fileId)); found = true; } } QVERIFY(found); } void TestTrash::trashFileFromHome() { const QString fileName = QStringLiteral("fileFromHome"); trashFile(homeTmpDir() + fileName, fileName); // Do it again, check that we got a different id trashFile(homeTmpDir() + fileName, fileName + QLatin1String(" (1)")); } void TestTrash::trashPercentFileFromHome() { const QString fileName = QStringLiteral("file%2f"); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::trashUtf8FileFromHome() { #ifdef UTF8TEST const QString fileName = utf8FileName(); trashFile(homeTmpDir() + fileName, fileName); #endif } void TestTrash::trashUmlautFileFromHome() { const QString fileName = umlautFileName(); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::testTrashNotEmpty() { KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); const KConfigGroup group = cfg.group("Status"); QVERIFY(group.exists()); QVERIFY(group.readEntry("Empty", true) == false); } void TestTrash::trashFileFromOther() { const QString fileName = QStringLiteral("fileFromOther"); trashFile(otherTmpDir() + fileName, fileName); } void TestTrash::trashFileIntoOtherPartition() { if (m_otherPartitionTrashDir.isEmpty()) { qDebug() << " - SKIPPED"; return; } const QString fileName = QStringLiteral("testtrash-file"); const QString origFilePath = m_otherPartitionTopDir + fileName; const QString fileId = fileName; // cleanup QFile::remove(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QFile::remove(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId); // setup if (!QFile::exists(origFilePath)) { createTestFile(origFilePath); } QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QMap metaData = job->metaData(); // Note that the Path stored in the info file is relative, on other partitions (#95652) checkInfoFile(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), fileName); QFileInfo files(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId); QVERIFY(files.isFile()); QVERIFY(files.size() == 12); // coolo suggests testing that the original file is actually gone, too :) QVERIFY(!QFile::exists(origFilePath)); QVERIFY(!metaData.isEmpty()); bool found = false; QMap::ConstIterator it = metaData.constBegin(); for (; it != metaData.constEnd(); ++it) { if (it.key().startsWith(QLatin1String("trashURL"))) { QUrl trashURL(it.value()); qDebug() << trashURL; QVERIFY(!trashURL.isEmpty()); QVERIFY(trashURL.scheme() == QLatin1String("trash")); QVERIFY(trashURL.path() == QStringLiteral("/%1-%2").arg(m_otherPartitionId).arg(fileId)); found = true; } } QVERIFY(found); } void TestTrash::trashFileOwnedByRoot() { QUrl u(QStringLiteral("file:///etc/passwd")); const QString fileId = QStringLiteral("passwd"); if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) { QSKIP("Test must not be run by root."); } KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); job->setUiDelegate(nullptr); // no skip dialog, thanks bool ok = job->exec(); QVERIFY(!ok); QVERIFY(job->error() == KIO::ERR_ACCESS_DENIED); const QString infoPath(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QVERIFY(!QFile::exists(infoPath)); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(!files.exists()); QVERIFY(QFile::exists(u.path())); } void TestTrash::trashSymlink(const QString &origFilePath, const QString &fileId, bool broken) { // setup const char *target = broken ? "/nonexistent" : "/tmp"; bool ok = ::symlink(target, QFile::encodeName(origFilePath).constData()) == 0; QVERIFY(ok); QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); ok = job->exec(); QVERIFY(ok); if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; return; } checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(files.isSymLink()); QVERIFY(files.symLinkTarget() == QFile::decodeName(target)); QVERIFY(!QFile::exists(origFilePath)); } void TestTrash::trashSymlinkFromHome() { const QString fileName = QStringLiteral("symlinkFromHome"); trashSymlink(homeTmpDir() + fileName, fileName, false); } void TestTrash::trashSymlinkFromOther() { const QString fileName = QStringLiteral("symlinkFromOther"); trashSymlink(otherTmpDir() + fileName, fileName, false); } void TestTrash::trashBrokenSymlinkFromHome() { const QString fileName = QStringLiteral("brokenSymlinkFromHome"); trashSymlink(homeTmpDir() + fileName, fileName, true); } void TestTrash::trashDirectory(const QString &origPath, const QString &fileId) { qDebug() << fileId; // setup if (!QFileInfo::exists(origPath)) { QDir dir; bool ok = dir.mkdir(origPath); QVERIFY(ok); } createTestFile(origPath + QLatin1String("/testfile")); QVERIFY(QDir().mkdir(origPath + QStringLiteral("/subdir"))); createTestFile(origPath + QLatin1String("/subdir/subfile")); QUrl u = QUrl::fromLocalFile(origPath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); QVERIFY(job->exec()); if (origPath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; return; } checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origPath); QFileInfo filesDir(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(filesDir.isDir()); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId + QLatin1String("/testfile")); QVERIFY(files.exists()); QVERIFY(files.isFile()); QVERIFY(files.size() == 12); QVERIFY(!QFile::exists(origPath)); QVERIFY(QFile::exists(m_trashDir + QStringLiteral("/files/") + fileId + QStringLiteral("/subdir/subfile"))); QFile dirCache(m_trashDir + QLatin1String("/directorysizes")); QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName())); QByteArray lines; bool found = false; while (!dirCache.atEnd()) { const QByteArray line = dirCache.readLine(); if (line.endsWith(' ' + QFile::encodeName(fileId).toPercentEncoding() + '\n')) { QVERIFY(!found); // should be there only once! found = true; } lines += line; } QVERIFY2(found, lines.constData()); //qDebug() << lines; checkDirCacheValidity(); } void TestTrash::checkDirCacheValidity() { QFile dirCache(m_trashDir + QLatin1String("/directorysizes")); QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName())); QSet seenDirs; while (!dirCache.atEnd()) { QByteArray line = dirCache.readLine(); QVERIFY(line.endsWith('\n')); line.chop(1); qDebug() << "LINE" << line; const int lastSpace = line.lastIndexOf(' '); const QByteArray dir = QByteArray::fromPercentEncoding(line.mid(lastSpace + 1)); QVERIFY2(!seenDirs.contains(dir), dir.constData()); seenDirs.insert(dir); const QString localDir = m_trashDir + QLatin1String("/files/") + QFile::decodeName(dir); QVERIFY2(QFile::exists(localDir), qPrintable(localDir)); QVERIFY(QFileInfo(localDir).isDir()); } } void TestTrash::trashDirectoryFromHome() { QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); // Do it again, check that we got a different id trashDirectory(homeTmpDir() + dirName, dirName + QLatin1String(" (1)")); } void TestTrash::trashDotDirectory() { QString dirName = QStringLiteral(".dotTrashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); // Do it again, check that we got a different id // TODO trashDirectory(homeTmpDir() + dirName, dirName + QString::fromLatin1(" (1)")); } void TestTrash::trashReadOnlyDirFromHome() { const QString dirName = readOnlyDirPath(); QDir dir; bool ok = dir.mkdir(dirName); QVERIFY(ok); // #130780 const QString subDirPath = dirName + QLatin1String("/readonly_subdir"); ok = dir.mkdir(subDirPath); QVERIFY(ok); createTestFile(subDirPath + QLatin1String("/testfile_in_subdir")); ::chmod(QFile::encodeName(subDirPath).constData(), 0500); trashDirectory(dirName, QStringLiteral("readonly")); } void TestTrash::trashDirectoryFromOther() { QString dirName = QStringLiteral("trashDirFromOther"); trashDirectory(otherTmpDir() + dirName, dirName); } void TestTrash::trashDirectoryWithTrailingSlash() { QString dirName = QStringLiteral("dirwithslash/"); trashDirectory(homeTmpDir() + dirName, QStringLiteral("dirwithslash")); } void TestTrash::trashBrokenSymlinkIntoSubdir() { QString origPath = homeTmpDir() + QStringLiteral("subDirBrokenSymlink"); if (!QFileInfo::exists(origPath)) { QDir dir; bool ok = dir.mkdir(origPath); QVERIFY(ok); } bool ok = ::symlink("/nonexistent", QFile::encodeName(origPath + QStringLiteral("/link")).constData()) == 0; QVERIFY(ok); trashDirectory(origPath, QStringLiteral("subDirBrokenSymlink")); } void TestTrash::delRootFile() { // test deleting a trashed file KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileFromHome")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(ok); QFileInfo file(m_trashDir + QLatin1String("/files/fileFromHome")); QVERIFY(!file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/fileFromHome.trashinfo")); QVERIFY(!info.exists()); // trash it again, we might need it later const QString fileName = QStringLiteral("fileFromHome"); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::delFileInDirectory() { // test deleting a file inside a trashed directory -> not allowed KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome/testfile")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(!ok); QVERIFY(delJob->error() == KIO::ERR_ACCESS_DENIED); QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome")); QVERIFY(dir.exists()); QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile")); QVERIFY(file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo")); QVERIFY(info.exists()); } void TestTrash::delDirectory() { // test deleting a trashed directory KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(ok); QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome")); QVERIFY(!dir.exists()); QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile")); QVERIFY(!file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo")); QVERIFY(!info.exists()); // trash it again, we'll need it later QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); } static bool MyNetAccess_stat(const QUrl &url, KIO::UDSEntry &entry) { KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); bool ok = statJob->exec(); if (ok) { entry = statJob->statResult(); } return ok; } static bool MyNetAccess_exists(const QUrl &url) { KIO::UDSEntry dummy; return MyNetAccess_stat(url, dummy); } void TestTrash::statRoot() { QUrl url(QStringLiteral("trash:/")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.name(), QStringLiteral(".")); } void TestTrash::statFileInRoot() { QUrl url(QStringLiteral("trash:/0-fileFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isFile()); QVERIFY(!item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("fileFromHome")); } void TestTrash::statDirectoryInRoot() { QUrl url(QStringLiteral("trash:/0-trashDirFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("trashDirFromHome")); } void TestTrash::statSymlinkInRoot() { QUrl url(QStringLiteral("trash:/0-symlinkFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isLink()); QCOMPARE(item.linkDest(), QStringLiteral("/tmp")); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("symlinkFromHome")); } void TestTrash::statFileInDirectory() { QUrl url(QStringLiteral("trash:/0-trashDirFromHome/testfile")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isFile()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("testfile")); } void TestTrash::statBrokenSymlinkInSubdir() { QUrl url(QStringLiteral("trash:/0-subDirBrokenSymlink/link")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.linkDest(), QLatin1String("/nonexistent")); } void TestTrash::copyFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath) { QUrl src(QLatin1String("trash:/0-") + fileId); if (!relativePath.isEmpty()) { src.setPath(concatPaths(src.path(), relativePath)); } QUrl dest = QUrl::fromLocalFile(destPath); QVERIFY(MyNetAccess_exists(src)); // A dnd would use copy(), but we use copyAs to ensure the final filename //qDebug() << "copyAs:" << src << " -> " << dest; KIO::Job *job = KIO::copyAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); QString infoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QVERIFY(QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(filesItem.exists()); QVERIFY(QFile::exists(destPath)); } void TestTrash::copyFileFromTrash() { // To test case of already-existing destination, uncomment this. // This brings up the "rename" dialog though, so it can't be fully automated #if 0 const QString destPath = otherTmpDir() + QString::fromLatin1("fileFromHome_copied"); copyFromTrash("fileFromHome", destPath); QVERIFY(QFileInfo(destPath).isFile()); QVERIFY(QFileInfo(destPath).size() == 12); #endif } void TestTrash::copyFileInDirectoryFromTrash() { const QString destPath = otherTmpDir() + QLatin1String("testfile_copied"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile")); QVERIFY(QFileInfo(destPath).isFile()); QVERIFY(QFileInfo(destPath).size() == 12); QVERIFY(QFileInfo(destPath).isWritable()); } void TestTrash::copyDirectoryFromTrash() { const QString destPath = otherTmpDir() + QLatin1String("trashDirFromHome_copied"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath); QVERIFY(QFileInfo(destPath).isDir()); QVERIFY(QFile::exists(destPath + QStringLiteral("/testfile"))); QVERIFY(QFile::exists(destPath + QStringLiteral("/subdir/subfile"))); } void TestTrash::copySymlinkFromTrash() // relies on trashSymlinkFromHome() being called first { const QString destPath = otherTmpDir() + QLatin1String("symlinkFromHome_copied"); copyFromTrash(QStringLiteral("symlinkFromHome"), destPath); QVERIFY(QFileInfo(destPath).isSymLink()); } void TestTrash::moveInTrash(const QString &fileId, const QString &destFileId) { const QUrl src(QLatin1String("trash:/0-") + fileId); const QUrl dest(QLatin1String("trash:/") + destFileId); QVERIFY(MyNetAccess_exists(src)); QVERIFY(!MyNetAccess_exists(dest)); KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); // Check old doesn't exist anymore QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!filesItem.exists()); // Check new exists now QString newInfoFile(m_trashDir + QStringLiteral("/info/") + destFileId + QStringLiteral(".trashinfo")); QVERIFY(QFile::exists(newInfoFile)); QFileInfo newFilesItem(m_trashDir + QStringLiteral("/files/") + destFileId); QVERIFY(newFilesItem.exists()); } void TestTrash::renameFileInTrash() { const QString fileName = QStringLiteral("renameFileInTrash"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); trashFile(filePath, fileName); const QString destFileName = QStringLiteral("fileRenamed"); moveInTrash(fileName, destFileName); // cleanup KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileRenamed")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY2(ok, qPrintable(delJob->errorString())); } void TestTrash::renameDirInTrash() { const QString dirName = QStringLiteral("trashDirFromHome"); const QString destDirName = QStringLiteral("dirRenamed"); moveInTrash(dirName, destDirName); moveInTrash(destDirName, dirName); } void TestTrash::moveFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath) { QUrl src(QLatin1String("trash:/0-") + fileId); if (!relativePath.isEmpty()) { src.setPath(concatPaths(src.path(), relativePath)); } QUrl dest = QUrl::fromLocalFile(destPath); QVERIFY(MyNetAccess_exists(src)); // A dnd would use move(), but we use moveAs to ensure the final filename KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!filesItem.exists()); QVERIFY(QFile::exists(destPath)); QVERIFY(QFileInfo(destPath).isWritable()); } void TestTrash::moveFileFromTrash() { const QString fileName = QStringLiteral("moveFileFromTrash"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); const QFile::Permissions origPerms = QFileInfo(filePath).permissions(); trashFile(filePath, fileName); const QString destPath = otherTmpDir() + QStringLiteral("fileFromTrash_restored"); moveFromTrash(fileName, destPath); const QFileInfo destInfo(destPath); QVERIFY(destInfo.isFile()); QCOMPARE(destInfo.size(), 12); QVERIFY(destInfo.isWritable()); QCOMPARE(int(destInfo.permissions()), int(origPerms)); QVERIFY(QFile::remove(destPath)); } void TestTrash::moveFileFromTrashToDir_data() { QTest::addColumn("destDir"); QTest::newRow("home_partition") << homeTmpDir(); // this will trigger a direct renaming QTest::newRow("other_partition") << otherTmpDir(); // this will require a real move } void TestTrash::moveFileFromTrashToDir() { // Given a file in the trash const QString fileName = QStringLiteral("moveFileFromTrashToDir"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); const QFile::Permissions origPerms = QFileInfo(filePath).permissions(); trashFile(filePath, fileName); QVERIFY(!QFile::exists(filePath)); // When moving it out to a dir QFETCH(QString, destDir); const QString destPath = destDir + QStringLiteral("moveFileFromTrashToDir"); const QUrl src(QLatin1String("trash:/0-") + fileName); const QUrl dest(QUrl::fromLocalFile(destDir)); KIO::Job *job = KIO::move(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); // Then it should move ;) const QFileInfo destInfo(destPath); QVERIFY(destInfo.isFile()); QCOMPARE(destInfo.size(), 12); QVERIFY(destInfo.isWritable()); QCOMPARE(int(destInfo.permissions()), int(origPerms)); QVERIFY(QFile::remove(destPath)); } void TestTrash::moveFileInDirectoryFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("testfile_restored"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile")); QVERIFY(QFileInfo(destPath).isFile()); QVERIFY(QFileInfo(destPath).size() == 12); } void TestTrash::moveDirectoryFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("trashDirFromHome_restored"); moveFromTrash(QStringLiteral("trashDirFromHome"), destPath); QVERIFY(QFileInfo(destPath).isDir()); checkDirCacheValidity(); // trash it again, we'll need it later QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); } void TestTrash::trashDirectoryOwnedByRoot() { QUrl u(QStringLiteral("file:///"));; if (QFile::exists(QStringLiteral("/etc/cups"))) { u.setPath(QStringLiteral("/etc/cups")); } else if (QFile::exists(QStringLiteral("/boot"))) { u.setPath(QStringLiteral("/boot")); } else { u.setPath(QStringLiteral("/etc")); } const QString fileId = u.path(); qDebug() << "fileId=" << fileId; if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) { QSKIP("Test must not be run by root."); } KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); job->setUiDelegate(nullptr); // no skip dialog, thanks bool ok = job->exec(); QVERIFY(!ok); const int err = job->error(); QVERIFY(err == KIO::ERR_ACCESS_DENIED || err == KIO::ERR_CANNOT_OPEN_FOR_READING); const QString infoPath(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoPath)); QFileInfo files(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!files.exists()); QVERIFY(QFile::exists(u.path())); } void TestTrash::moveSymlinkFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("symlinkFromHome_restored"); moveFromTrash(QStringLiteral("symlinkFromHome"), destPath); QVERIFY(QFileInfo(destPath).isSymLink()); } void TestTrash::getFile() { const QString fileId = QStringLiteral("fileFromHome (1)"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); QTemporaryFile tmpFile; QVERIFY(tmpFile.open()); const QString tmpFilePath = tmpFile.fileName(); KIO::Job *getJob = KIO::file_copy(url, QUrl::fromLocalFile(tmpFilePath), -1, KIO::Overwrite | KIO::HideProgressInfo); bool ok = getJob->exec(); QVERIFY2(ok, qPrintable(getJob->errorString())); // Don't use tmpFile.close()+tmpFile.open() here, the size would still be 0 in the QTemporaryFile object // (due to the use of fstat on the old fd). Arguably a bug (I even have a testcase), but probably // not fixable without breaking the security of QTemporaryFile... QFile reader(tmpFilePath); QVERIFY(reader.open(QIODevice::ReadOnly)); QByteArray str = reader.readAll(); QCOMPARE(str, QByteArray("Hello world\n")); } void TestTrash::restoreFile() { const QString fileId = QStringLiteral("fileFromHome (1)"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(infoFile)); QVERIFY(!QFile::exists(filesItem)); const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome"); QVERIFY(QFile::exists(destPath)); } void TestTrash::restoreFileFromSubDir() { const QString fileId = QStringLiteral("trashDirFromHome (1)/testfile"); QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)"))); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/trashDirFromHome (1).trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/trashDirFromHome (1)/testfile")); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(!ok); // dest dir doesn't exist -> error message QVERIFY(job->error() == KIO::ERR_SLAVE_DEFINED); // check that nothing happened QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)"))); } void TestTrash::restoreFileToDeletedDirectory() { // Ensure we'll get "fileFromHome" as fileId removeFile(m_trashDir, QStringLiteral("/info/fileFromHome.trashinfo")); removeFile(m_trashDir, QStringLiteral("/files/fileFromHome")); trashFileFromHome(); // Delete orig dir KIO::Job *delJob = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo); bool delOK = delJob->exec(); QVERIFY(delOK); const QString fileId = QStringLiteral("fileFromHome"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(!ok); // dest dir doesn't exist -> error message QVERIFY(job->error() == KIO::ERR_SLAVE_DEFINED); // check that nothing happened QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome"); QVERIFY(!QFile::exists(destPath)); } void TestTrash::listRootDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QVERIFY(m_entryCount > 1); //qDebug() << m_listResult; //qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1); } void TestTrash::listRecursiveRootDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listRecursive(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QVERIFY(m_entryCount > 1); qDebug() << m_listResult; qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome (1)")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-trashDirFromHome/testfile")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-readonly/readonly_subdir/testfile_in_subdir")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-subDirBrokenSymlink/link")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("trashDirFromHome/testfile")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("readonly/readonly_subdir/testfile_in_subdir")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("subDirBrokenSymlink/link")), 1); } void TestTrash::listSubDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QCOMPARE(m_entryCount, 3); //qDebug() << m_listResult; //qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("testfile")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("subdir")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("testfile")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("subdir")), 1); } void TestTrash::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst) { - for (KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { - const KIO::UDSEntry &entry(*it); + for (const KIO::UDSEntry &entry : lst) { QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME); QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); QUrl url(entry.stringValue(KIO::UDSEntry::UDS_URL)); qDebug() << "name" << name << "displayName" << displayName << " UDS_URL=" << url; if (!url.isEmpty()) { QVERIFY(url.scheme() == QStringLiteral("trash")); } m_listResult << name; m_displayNameListResult << displayName; } m_entryCount += lst.count(); } void TestTrash::emptyTrash() { // ## Even though we use a custom XDG_DATA_HOME value, emptying the // trash would still empty the other trash directories in other partitions. // So we can't activate this test by default. #if 0 // To make this test standalone trashFileFromHome(); // #167051: orphaned files createTestFile(m_trashDir + "/files/testfile_nometadata"); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)1; KIO::Job *job = KIO::special(QUrl("trash:/"), packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); KConfig cfg("trashrc", KConfig::SimpleConfig); QVERIFY(cfg.hasGroup("Status")); QVERIFY(cfg.group("Status").readEntry("Empty", false) == true); QVERIFY(!QFile::exists(m_trashDir + "/files/fileFromHome")); QVERIFY(!QFile::exists(m_trashDir + "/files/readonly")); QVERIFY(!QFile::exists(m_trashDir + "/info/readonly.trashinfo")); QVERIFY(QDir(m_trashDir + "/info").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()); QVERIFY(QDir(m_trashDir + "/files").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()); #else qDebug() << " : SKIPPED"; #endif } static bool isTrashEmpty() { KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); const KConfigGroup group = cfg.group("Status"); return group.readEntry("Empty", true); } void TestTrash::testEmptyTrashSize() { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl(QStringLiteral("trash:/"))); QVERIFY(job->exec()); if (isTrashEmpty()) { QCOMPARE(job->totalSize(), 0ULL); } else { QVERIFY(job->totalSize() < 1000000000 /*1GB*/); // #157023 } } static void checkIcon(const QUrl &url, const QString &expectedIcon) { QString icon = KIO::iconNameForUrl(url); // #100321 QCOMPARE(icon, expectedIcon); } void TestTrash::testIcons() { // The JSON file says "user-trash-full" in all cases, whether the trash is full or not QCOMPARE(KProtocolInfo::icon(QStringLiteral("trash")), QStringLiteral("user-trash-full")); // #100321 if (isTrashEmpty()) { checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash")); } else { checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash-full")); } checkIcon(QUrl(QStringLiteral("trash:/foo/")), QStringLiteral("inode-directory")); } QTEST_GUILESS_MAIN(TestTrash) diff --git a/src/ioslaves/trash/trashimpl.cpp b/src/ioslaves/trash/trashimpl.cpp index 4d288a0e..8615cb03 100644 --- a/src/ioslaves/trash/trashimpl.cpp +++ b/src/ioslaves/trash/trashimpl.cpp @@ -1,1422 +1,1420 @@ /* This file is part of the KDE project Copyright (C) 2004 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) 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 "trashimpl.h" #include "discspaceutil.h" #include "trashsizecache.h" #include "kiotrashdebug.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 TrashImpl::TrashImpl() : QObject(), m_lastErrorCode(0), m_initStatus(InitToBeDone), m_homeDevice(0), m_trashDirectoriesScanned(false), // not using kio_trashrc since KIO uses that one already for kio_trash // so better have a separate one, for faster parsing by e.g. kmimetype.cpp m_config(QStringLiteral("trashrc"), KConfig::SimpleConfig) { QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(QDir::homePath()).constData(), &buff) == 0) { m_homeDevice = buff.st_dev; } else { qCWarning(KIO_TRASH) << "Should never happen: couldn't stat $HOME" << strerror(errno); } } /** * Test if a directory exists, create otherwise * @param _name full path of the directory * @return errorcode, or 0 if the dir was created or existed already * Warning, don't use return value like a bool */ int TrashImpl::testDir(const QString &_name) const { DIR *dp = ::opendir(QFile::encodeName(_name).constData()); if (!dp) { QString name = _name; if (name.endsWith(QLatin1Char('/'))) { name.chop(1); } bool ok = QDir().mkdir(name); if (!ok && QFile::exists(name)) { #if 0 // this would require to use SlaveBase's method to ask the question //int ret = KMessageBox::warningYesNo( 0, i18n("%1 is a file, but KDE needs it to be a directory. Move it to %2.orig and create directory?").arg(name).arg(name) ); //if ( ret == KMessageBox::Yes ) { #endif QString new_name = name; name.append(QStringLiteral(".orig")); if (QFile::rename(name, new_name)) { ok = QDir().mkdir(name); } else { // foo.orig existed already. How likely is that? ok = false; } if (!ok) { return KIO::ERR_DIR_ALREADY_EXIST; } #if 0 //} else { // return 0; //} #endif } if (!ok) { //KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) ); qCWarning(KIO_TRASH) << "could not create" << name; return KIO::ERR_COULD_NOT_MKDIR; } else { //qCDebug(KIO_TRASH) << name << "created."; } } else { // exists already closedir(dp); } return 0; // success } void TrashImpl::deleteEmptyTrashInfrastructure() { #ifdef Q_OS_OSX // For each known trash directory... if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd() ; ++it) { const QString trashPath = it.value(); QString infoPath = trashPath + QLatin1String("/info"); //qCDebug(KIO_TRASH) << "empty Trash" << trashPath << "; removing infrastructure"; synchronousDel(infoPath, false, true); synchronousDel(trashPath + QLatin1String("/files"), false, true); if (trashPath.endsWith(QLatin1String("/KDE.trash"))) { synchronousDel(trashPath, false, true); } } #endif } bool TrashImpl::createTrashInfrastructure(int trashId, const QString &path) { int err; QString trashDir = path.isEmpty() ? trashDirectoryPath(trashId) : path; if ((err = testDir(trashDir))) { error(err, trashDir); return false; } if ((err = testDir(trashDir + QLatin1String("/info")))) { error(err, trashDir + QLatin1String("/info")); return false; } if ((err = testDir(trashDir + QLatin1String("/files")))) { error(err, trashDir + QLatin1String("/files")); return false; } return true; } bool TrashImpl::init() { if (m_initStatus == InitOK) { return true; } if (m_initStatus == InitError) { return false; } // Check the trash directory and its info and files subdirs // see also kdesktop/init.cc for first time initialization m_initStatus = InitError; #ifndef Q_OS_OSX // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default. const QString xdgDataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/'); if (!QDir().mkpath(xdgDataDir)) { qCWarning(KIO_TRASH) << "failed to create" << xdgDataDir; return false; } const QString trashDir = xdgDataDir + QLatin1String("Trash"); if (!createTrashInfrastructure(0, trashDir)) { return false; } #else // we DO NOT create ~/.Trash on OS X, that's the operating system's privilege QString trashDir = QDir::homePath() + QLatin1String("/.Trash"); if (!QFileInfo(trashDir).isDir()) { error(KIO::ERR_DOES_NOT_EXIST, trashDir); return false; } trashDir += QLatin1String("/KDE.trash"); // we don't have to call createTrashInfrastructure() here because it'll be called when needed. #endif m_trashDirectories.insert(0, trashDir); m_initStatus = InitOK; //qCDebug(KIO_TRASH) << "initialization OK, home trash dir:" << trashDir; return true; } void TrashImpl::migrateOldTrash() { qCDebug(KIO_TRASH); KConfigGroup g(KSharedConfig::openConfig(), "Paths"); const QString oldTrashDir = g.readPathEntry("Trash", QString()); if (oldTrashDir.isEmpty()) { return; } const QStringList entries = listDir(oldTrashDir); bool allOK = true; - for (QStringList::const_iterator entryIt = entries.begin(), entryEnd = entries.end(); - entryIt != entryEnd; ++entryIt) { - QString srcPath = *entryIt; + for (QString srcPath : entries) { if (srcPath == QLatin1Char('.') || srcPath == QLatin1String("..") || srcPath == QLatin1String(".directory")) { continue; } srcPath.prepend(oldTrashDir); // make absolute int trashId; QString fileId; if (!createInfo(srcPath, trashId, fileId)) { qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath; allOK = false; } else { bool ok = moveToTrash(srcPath, trashId, fileId); if (!ok) { (void)deleteInfo(trashId, fileId); qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath; allOK = false; } else { qCDebug(KIO_TRASH) << "Trash migration: moved" << srcPath; } } } if (allOK) { // We need to remove the old one, otherwise the desktop will have two trashcans... qCDebug(KIO_TRASH) << "Trash migration: all OK, removing old trash directory"; synchronousDel(oldTrashDir, false, true); } } bool TrashImpl::createInfo(const QString &origPath, int &trashId, QString &fileId) { //qCDebug(KIO_TRASH) << origPath; // Check source const QByteArray origPath_c(QFile::encodeName(origPath)); // off_t should be 64bit on Unix systems to have large file support // FIXME: on windows this gets disabled until trash gets integrated // BUG: 165449 #ifndef Q_OS_WIN Q_STATIC_ASSERT(sizeof(off_t) >= 8); #endif QT_STATBUF buff_src; if (QT_LSTAT(origPath_c.data(), &buff_src) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, origPath); } else { error(KIO::ERR_DOES_NOT_EXIST, origPath); } return false; } // Choose destination trash trashId = findTrashDirectory(origPath); if (trashId < 0) { qCWarning(KIO_TRASH) << "OUCH - internal error, TrashImpl::findTrashDirectory returned" << trashId; return false; // ### error() needed? } //qCDebug(KIO_TRASH) << "trashing to" << trashId; // Grab original filename QUrl url = QUrl::fromLocalFile(origPath); url = url.adjusted(QUrl::StripTrailingSlash); const QString origFileName = url.fileName(); // Make destination file in info/ #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif url.setPath(infoPath(trashId, origFileName)); // we first try with origFileName QUrl baseDirectory = QUrl::fromLocalFile(url.path()); // Here we need to use O_EXCL to avoid race conditions with other kioslave processes int fd = 0; QString fileName; do { //qCDebug(KIO_TRASH) << "trying to create" << url.path(); fd = ::open(QFile::encodeName(url.path()).constData(), O_WRONLY | O_CREAT | O_EXCL, 0600); if (fd < 0) { if (errno == EEXIST) { fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + KFileUtils::suggestName(baseDirectory, fileName)); // and try again on the next iteration } else { error(KIO::ERR_COULD_NOT_WRITE, url.path()); return false; } } } while (fd < 0); const QString infoPath = url.path(); fileId = url.fileName(); Q_ASSERT(fileId.endsWith(QLatin1String(".trashinfo"))); fileId.chop(10); // remove .trashinfo from fileId FILE *file = ::fdopen(fd, "w"); if (!file) { // can't see how this would happen error(KIO::ERR_COULD_NOT_WRITE, infoPath); return false; } // Contents of the info file. We could use KSimpleConfig, but that would // mean closing and reopening fd, i.e. opening a race condition... QByteArray info = "[Trash Info]\n"; info += "Path="; // Escape filenames according to the way they are encoded on the filesystem // All this to basically get back to the raw 8-bit representation of the filename... if (trashId == 0) { // home trash: absolute path info += QUrl::toPercentEncoding(origPath, "/"); } else { info += QUrl::toPercentEncoding(makeRelativePath(topDirectoryPath(trashId), origPath), "/"); } info += '\n'; info += "DeletionDate=" + QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1() + '\n'; size_t sz = info.size(); size_t written = ::fwrite(info.data(), 1, sz, file); if (written != sz) { ::fclose(file); QFile::remove(infoPath); error(KIO::ERR_DISK_FULL, infoPath); return false; } ::fclose(file); //qCDebug(KIO_TRASH) << "info file created in trashId=" << trashId << ":" << fileId; return true; } QString TrashImpl::makeRelativePath(const QString &topdir, const QString &path) { QString realPath = QFileInfo(path).canonicalFilePath(); if (realPath.isEmpty()) { // shouldn't happen realPath = path; } // topdir ends with '/' #ifndef Q_OS_WIN if (realPath.startsWith(topdir)) { #else if (realPath.startsWith(topdir, Qt::CaseInsensitive)) { #endif const QString rel = realPath.mid(topdir.length()); Q_ASSERT(rel[0] != QLatin1Char('/')); return rel; } else { // shouldn't happen... qCWarning(KIO_TRASH) << "Couldn't make relative path for" << realPath << "(" << path << "), with topdir=" << topdir; return realPath; } } void TrashImpl::enterLoop() { QEventLoop eventLoop; connect(this, &TrashImpl::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } QString TrashImpl::infoPath(int trashId, const QString &fileId) const { const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"); return trashPath; } QString TrashImpl::filesPath(int trashId, const QString &fileId) const { const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/") + fileId; return trashPath; } bool TrashImpl::deleteInfo(int trashId, const QString &fileId) { #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif bool ok = QFile::remove(infoPath(trashId, fileId)); if (ok) { fileRemoved(); } return ok; } bool TrashImpl::moveToTrash(const QString &origPath, int trashId, const QString &fileId) { //qCDebug(KIO_TRASH) << "Trashing" << origPath << trashId << fileId; if (!adaptTrashSize(origPath, trashId)) { return false; } const qulonglong pathSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif const QString dest = filesPath(trashId, fileId); if (!move(origPath, dest)) { // Maybe the move failed due to no permissions to delete source. // In that case, delete dest to keep things consistent, since KIO doesn't do it. if (QFileInfo(dest).isFile()) { QFile::remove(dest); } else { synchronousDel(dest, false, true); } return false; } if (QFileInfo(dest).isDir()) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.add(fileId, pathSize); } fileAdded(); return true; } bool TrashImpl::moveFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath) { QString src = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { src += QLatin1Char('/') + relativePath; } if (!move(src, dest)) { return false; } TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.remove(fileId); return true; } bool TrashImpl::move(const QString &src, const QString &dest) { if (directRename(src, dest)) { // This notification is done by KIO::moveAs when using the code below // But if we do a direct rename we need to do the notification ourselves org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(dest)); return true; } if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) { return false; } QUrl urlSrc = QUrl::fromLocalFile(src); QUrl urlDest = QUrl::fromLocalFile(dest); //qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest; KIO::CopyJob *job = KIO::moveAs(urlSrc, urlDest, KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); return m_lastErrorCode == 0; } void TrashImpl::jobFinished(KJob *job) { //qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText(); error(job->error(), job->errorText()); emit leaveModality(); } bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId) { //qCDebug(KIO_TRASH); if (!adaptTrashSize(origPath, trashId)) { return false; } const qulonglong pathSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif const QString dest = filesPath(trashId, fileId); if (!copy(origPath, dest)) { return false; } if (QFileInfo(dest).isDir()) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.add(fileId, pathSize); } fileAdded(); return true; } bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath) { QString src = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { src += QLatin1Char('/') + relativePath; } return copy(src, dest); } bool TrashImpl::copy(const QString &src, const QString &dest) { // kio_file's copy() method is quite complex (in order to be fast), let's just call it... m_lastErrorCode = 0; QUrl urlSrc = QUrl::fromLocalFile(src); QUrl urlDest = QUrl::fromLocalFile(dest); //qCDebug(KIO_TRASH) << "copying" << src << "to" << dest; KIO::CopyJob *job = KIO::copyAs(urlSrc, urlDest, KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); return m_lastErrorCode == 0; } bool TrashImpl::directRename(const QString &src, const QString &dest) { //qCDebug(KIO_TRASH) << src << "->" << dest; // Do not use QFile::rename here, we need to be able to move broken symlinks too // (and we need to make sure errno is set) if (::rename(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) != 0) { if (errno == EXDEV) { error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); } else { if ((errno == EACCES) || (errno == EPERM)) { error(KIO::ERR_ACCESS_DENIED, dest); } else if (errno == EROFS) { // The file is on a read-only filesystem error(KIO::ERR_CANNOT_DELETE, src); } else { error(KIO::ERR_CANNOT_RENAME, src); } } return false; } return true; } bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId) { m_lastErrorCode = 0; const QString oldInfo = infoPath(trashId, oldFileId); const QString oldFile = filesPath(trashId, oldFileId); const QString newInfo = infoPath(trashId, newFileId); const QString newFile = filesPath(trashId, newFileId); if (directRename(oldInfo, newInfo)) { if (directRename(oldFile, newFile)) { // success return true; } else { // rollback directRename(newInfo, oldInfo); } } return false; } #if 0 bool TrashImpl::mkdir(int trashId, const QString &fileId, int permissions) { const QString path = filesPath(trashId, fileId); if (KDE_mkdir(QFile::encodeName(path), permissions) != 0) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); return false; } else if (errno == ENOSPC) { error(KIO::ERR_DISK_FULL, path); return false; } else { error(KIO::ERR_COULD_NOT_MKDIR, path); return false; } } else { if (permissions != -1) { ::chmod(QFile::encodeName(path), permissions); } } return true; } #endif bool TrashImpl::del(int trashId, const QString &fileId) { #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif QString info = infoPath(trashId, fileId); QString file = filesPath(trashId, fileId); QByteArray info_c = QFile::encodeName(info); QT_STATBUF buff; if (QT_LSTAT(info_c.data(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, file); } else { error(KIO::ERR_DOES_NOT_EXIST, file); } return false; } const bool isDir = QFileInfo(file).isDir(); if (!synchronousDel(file, true, isDir)) { return false; } if (isDir) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.remove(fileId); } QFile::remove(info); fileRemoved(); return true; } bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir) { const int oldErrorCode = m_lastErrorCode; const QString oldErrorMsg = m_lastErrorMessage; QUrl url = QUrl::fromLocalFile(path); // First ensure that all dirs have u+w permissions, // otherwise we won't be able to delete files in them (#130780). if (isDir) { // qCDebug(KIO_TRASH) << "chmod'ing" << url; KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown); KFileItemList fileItemList; fileItemList.append(fileItem); KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo); connect(chmodJob, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); } KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); bool ok = m_lastErrorCode == 0; if (!setLastErrorCode) { m_lastErrorCode = oldErrorCode; m_lastErrorMessage = oldErrorMsg; } return ok; } bool TrashImpl::emptyTrash() { //qCDebug(KIO_TRASH); // The naive implementation "delete info and files in every trash directory" // breaks when deleted directories contain files owned by other users. // We need to ensure that the .trashinfo file is only removed when the // corresponding files could indeed be removed (#116371) // On the other hand, we certainly want to remove any file that has no associated // .trashinfo file for some reason (#167051) QSet unremovableFiles; int myErrorCode = 0; QString myErrorMsg; const TrashedFileInfoList fileInfoList = list(); TrashedFileInfoList::const_iterator it = fileInfoList.begin(); const TrashedFileInfoList::const_iterator end = fileInfoList.end(); for (; it != end; ++it) { const TrashedFileInfo &info = *it; const QString filesPath = info.physicalPath; if (synchronousDel(filesPath, true, true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) { QFile::remove(infoPath(info.trashId, info.fileId)); } else { // error code is set by synchronousDel, let's remember it // (so that successfully removing another file doesn't erase the error) myErrorCode = m_lastErrorCode; myErrorMsg = m_lastErrorMessage; // and remember not to remove this file unremovableFiles.insert(filesPath); qCDebug(KIO_TRASH) << "Unremovable:" << filesPath; } TrashSizeCache trashSize(trashDirectoryPath(info.trashId)); trashSize.clear(); } // Now do the orphaned-files cleanup TrashDirMap::const_iterator trit = m_trashDirectories.constBegin(); for (; trit != m_trashDirectories.constEnd(); ++trit) { //const int trashId = trit.key(); QString filesDir = trit.value(); filesDir += QLatin1String("/files"); const QStringList list = listDir(filesDir); for (const QString &fileName : list) { if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) { continue; } const QString filePath = filesDir + QLatin1Char('/') + fileName; if (!unremovableFiles.contains(filePath)) { qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath; QFile::remove(filePath); } } } m_lastErrorCode = myErrorCode; m_lastErrorMessage = myErrorMsg; fileRemoved(); return m_lastErrorCode == 0; } TrashImpl::TrashedFileInfoList TrashImpl::list() { // Here we scan for trash directories unconditionally. This allows // noticing plugged-in [e.g. removable] devices, or new mounts etc. scanTrashDirectories(); TrashedFileInfoList lst; // For each known trash directory... TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { const int trashId = it.key(); QString infoPath = it.value(); infoPath += QLatin1String("/info"); // Code taken from kio_file const QStringList entryNames = listDir(infoPath); //char path_buffer[PATH_MAX]; //getcwd(path_buffer, PATH_MAX - 1); //if ( chdir( infoPathEnc ) ) // continue; for (QStringList::const_iterator entryIt = entryNames.constBegin(), entryEnd = entryNames.constEnd(); entryIt != entryEnd; ++entryIt) { QString fileName = *entryIt; if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) { continue; } if (!fileName.endsWith(QLatin1String(".trashinfo"))) { qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName; continue; } fileName.chop(10); TrashedFileInfo info; if (infoForFile(trashId, fileName, info)) { lst << info; } } } return lst; } // Returns the entries in a given directory - including "." and ".." QStringList TrashImpl::listDir(const QString &physicalPath) { QDir dir(physicalPath); return dir.entryList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System); } bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info) { //qCDebug(KIO_TRASH) << trashId << fileId; info.trashId = trashId; // easy :) info.fileId = fileId; // equally easy info.physicalPath = filesPath(trashId, fileId); return readInfoFile(infoPath(trashId, fileId), info, trashId); } bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info) { const int trashId = findTrashDirectory(path); if (trashId < 0) { qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId; return false; } const KConfig config(QStringLiteral("ktrashrc")); const QString trashPath = trashDirectoryPath(trashId); const auto group = config.group(trashPath); const bool useSizeLimit = group.readEntry("UseSizeLimit", true); const double percent = group.readEntry("Percent", 10.0); DiscSpaceUtil util(trashPath + QLatin1String("/files/")); qulonglong total = util.size(); if (useSizeLimit) { total *= percent / 100.0; } TrashSizeCache trashSize(trashPath); const qulonglong used = trashSize.calculateSize(); info.totalSize = total; info.availableSize = total - used; return true; } bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId) { KConfig cfg(infoPath, KConfig::SimpleConfig); if (!cfg.hasGroup("Trash Info")) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath); return false; } const KConfigGroup group = cfg.group("Trash Info"); info.origPath = QUrl::fromPercentEncoding(group.readEntry("Path").toLatin1()); if (info.origPath.isEmpty()) { return false; // path is mandatory... } if (trashId == 0) { Q_ASSERT(info.origPath[0] == QLatin1Char('/')); } else { const QString topdir = topDirectoryPath(trashId); // includes trailing slash info.origPath.prepend(topdir); } const QString line = group.readEntry("DeletionDate"); if (!line.isEmpty()) { info.deletionDate = QDateTime::fromString(line, Qt::ISODate); } return true; } QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath) { QString filePath = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { filePath += QLatin1Char('/') + relativePath; } return filePath; } void TrashImpl::error(int e, const QString &s) { if (e) { qCDebug(KIO_TRASH) << e << s; } m_lastErrorCode = e; m_lastErrorMessage = s; } bool TrashImpl::isEmpty() const { // For each known trash directory... if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { QString infoPath = it.value(); infoPath += QLatin1String("/info"); DIR *dp = ::opendir(QFile::encodeName(infoPath).constData()); if (dp) { struct dirent *ep; ep = readdir(dp); ep = readdir(dp); // ignore '.' and '..' dirent ep = readdir(dp); // look for third file closedir(dp); if (ep != nullptr) { //qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty"; return false; // not empty } } } return true; } void TrashImpl::fileAdded() { m_config.reparseConfiguration(); KConfigGroup group = m_config.group("Status"); if (group.readEntry("Empty", true) == true) { group.writeEntry("Empty", false); m_config.sync(); } // The apps showing the trash (e.g. kdesktop) will be notified // of this change when KDirNotify::FilesAdded("trash:/") is emitted, // which will be done by the job soon after this. } void TrashImpl::fileRemoved() { if (isEmpty()) { deleteEmptyTrashInfrastructure(); KConfigGroup group = m_config.group("Status"); group.writeEntry("Empty", true); m_config.sync(); org::kde::KDirNotify::emitFilesChanged({QUrl::fromEncoded("trash:/")}); } // The apps showing the trash (e.g. kdesktop) will be notified // of this change when KDirNotify::FilesRemoved(...) is emitted, // which will be done by the job soon after this. } #ifdef Q_OS_OSX #include #include #include int TrashImpl::idForMountPoint(const QString &mountPoint) const { DADiskRef disk; CFDictionaryRef descDict; DASessionRef session = DASessionCreate(NULL); int devId = -1; if (session) { QByteArray mp = QFile::encodeName(mountPoint); struct statfs statFS; statfs(mp.constData(), &statFS); disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname); if (disk) { descDict = DADiskCopyDescription(disk); if (descDict) { CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey); CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey); int major, minor; if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) { qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor; devId = 1000 * major + minor; } CFRelease(cfMajor); CFRelease(cfMinor); } else { qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk; } CFRelease(disk); } else { qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp; } CFRelease(session); } else { qCWarning(KIO_TRASH) << "couldn't create DASession"; } return devId; } #else int TrashImpl::idForDevice(const Solid::Device &device) const { const Solid::Block *block = device.as(); if (block) { //qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor(); return block->deviceMajor() * 1000 + block->deviceMinor(); } else { const Solid::NetworkShare *netshare = device.as(); if (netshare) { QString url = netshare->url().url(); QLockFile configLock(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock")); if (!configLock.lock()) { return -1; } m_config.reparseConfiguration(); KConfigGroup group = m_config.group("NetworkShares"); int id = group.readEntry(url, -1); if (id == -1) { id = group.readEntry("NextID", 0); //qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id; group.writeEntry(url, id); group.writeEntry("NextID", id + 1); group.sync(); } return 6000000 + id; } // Not a block device nor a network share return -1; } } void TrashImpl::refreshDevices() const { // this is needed because Solid's fstab backend uses QSocketNotifier // to get notifications about changes to mtab // otherwise we risk getting old device list qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } #endif int TrashImpl::findTrashDirectory(const QString &origPath) { //qCDebug(KIO_TRASH) << origPath; // First check if same device as $HOME, then we use the home trash right away. QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff) == 0 && buff.st_dev == m_homeDevice) { return 0; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(origPath); if (!mp) { //qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath; return 0; } QString mountPoint = mp->mountPoint(); const QString trashDir = trashForMountPoint(mountPoint, true); //qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir; #ifndef Q_OS_OSX if (trashDir.isEmpty()) { return 0; // no trash available on partition } #endif int id = idForTrashDirectory(trashDir); if (id > -1) { //qCDebug(KIO_TRASH) << "known with id" << id; return id; } // new trash dir found, register it // but we need stability in the trash IDs, so that restoring or asking // for properties works even kio_trash gets killed because idle. #if 0 qCDebug(KIO_TRASH) << "found" << trashDir; m_trashDirectories.insert(++m_lastId, trashDir); if (!mountPoint.endsWith('/')) { mountPoint += '/'; } m_topDirectories.insert(m_lastId, mountPoint); return m_lastId; #endif #ifdef Q_OS_OSX id = idForMountPoint(mountPoint); #else refreshDevices(); const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '") + mountPoint + QLatin1String("']"); //qCDebug(KIO_TRASH) << "doing solid query:" << query; const QList lst = Solid::Device::listFromQuery(query); //qCDebug(KIO_TRASH) << "got" << lst.count() << "devices"; if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance. return 0; // use the home trash instead } // Pretend we got exactly one... const Solid::Device device = lst[0]; // new trash dir found, register it id = idForDevice(device); #endif if (id == -1) { return 0; } m_trashDirectories.insert(id, trashDir); //qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id; if (!mountPoint.endsWith(QLatin1Char('/'))) { mountPoint += QLatin1Char('/'); } m_topDirectories.insert(id, mountPoint); return idForTrashDirectory(trashDir); } void TrashImpl::scanTrashDirectories() const { #ifndef Q_OS_OSX refreshDevices(); #endif const QList lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true")); - for (QList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { - QString topdir = (*it).as()->filePath(); + for (const Solid::Device &device : lst) { + QString topdir = device.as()->filePath(); QString trashDir = trashForMountPoint(topdir, false); if (!trashDir.isEmpty()) { // OK, trashDir is a valid trash directory. Ensure it's registered. int trashId = idForTrashDirectory(trashDir); if (trashId == -1) { // new trash dir found, register it #ifdef Q_OS_OSX trashId = idForMountPoint(topdir); #else - trashId = idForDevice(*it); + trashId = idForDevice(device); #endif if (trashId == -1) { continue; } m_trashDirectories.insert(trashId, trashDir); //qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << trashId; if (!topdir.endsWith(QLatin1Char('/'))) { topdir += QLatin1Char('/'); } m_topDirectories.insert(trashId, topdir); } } } m_trashDirectoriesScanned = true; } TrashImpl::TrashDirMap TrashImpl::trashDirectories() const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } return m_trashDirectories; } TrashImpl::TrashDirMap TrashImpl::topDirectories() const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } return m_topDirectories; } QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const { // (1) Administrator-created $topdir/.Trash directory #ifndef Q_OS_OSX const QString rootTrashDir = topdir + QLatin1String("/.Trash"); #else const QString rootTrashDir = topdir + QLatin1String("/.Trashes"); #endif const QByteArray rootTrashDir_c = QFile::encodeName(rootTrashDir); // Can't use QFileInfo here since we need to test for the sticky bit uid_t uid = getuid(); QT_STATBUF buff; const unsigned int requiredBits = S_ISVTX; // Sticky bit required if (QT_LSTAT(rootTrashDir_c.constData(), &buff) == 0) { if ((S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && ((buff.st_mode & requiredBits) == requiredBits) && (::access(rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable ) { #ifndef Q_OS_OSX const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid); #else QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid); #endif const QByteArray trashDir_c = QFile::encodeName(trashDir); if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) { if ((buff.st_uid == uid) // must be owned by user && (S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && (buff.st_mode & 0777) == 0700) { // rwx for user #ifdef Q_OS_OSX trashDir += QStringLiteral("/KDE.trash"); #endif return trashDir; } qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it"; } else if (createIfNeeded && initTrashDirectory(trashDir_c)) { return trashDir; } } else { qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it"; } } #ifndef Q_OS_OSX // (2) $topdir/.Trash-$uid const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid); const QByteArray trashDir_c = QFile::encodeName(trashDir); if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) { if ((buff.st_uid == uid) // must be owned by user && (S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && ((buff.st_mode & 0777) == 0700)) { // rwx for user, ------ for group and others if (checkTrashSubdirs(trashDir_c)) { return trashDir; } } qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it"; // Exists, but not useable return QString(); } if (createIfNeeded && initTrashDirectory(trashDir_c)) { return trashDir; } #endif return QString(); } int TrashImpl::idForTrashDirectory(const QString &trashDir) const { // If this is too slow we can always use a reverse map... TrashDirMap::ConstIterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { if (it.value() == trashDir) { return it.key(); } } return -1; } bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const { //qCDebug(KIO_TRASH) << trashDir_c; if (mkdir(trashDir_c.constData(), 0700) != 0) { return false; } //qCDebug(KIO_TRASH); // This trash dir will be useable only if the directory is owned by user. // In theory this is the case, but not on e.g. USB keys... uid_t uid = getuid(); QT_STATBUF buff; if (QT_LSTAT(trashDir_c.constData(), &buff) != 0) { return false; // huh? } if ((buff.st_uid == uid) // must be owned by user && ((buff.st_mode & 0777) == 0700)) { // rwx for user, --- for group and others return checkTrashSubdirs(trashDir_c); } else { qCWarning(KIO_TRASH) << trashDir_c << "just created, by it doesn't have the right permissions, probably some strange unsupported filesystem"; ::rmdir(trashDir_c.constData()); return false; } return true; } bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const { // testDir currently works with a QString - ## optimize QString trashDir = QFile::decodeName(trashDir_c); const QString info = trashDir + QLatin1String("/info"); if (testDir(info) != 0) { return false; } const QString files = trashDir + QLatin1String("/files"); if (testDir(files) != 0) { return false; } return true; } QString TrashImpl::trashDirectoryPath(int trashId) const { // Never scanned for trash dirs? (This can happen after killing kio_trash // and reusing a directory listing from the earlier instance.) if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } Q_ASSERT(m_trashDirectories.contains(trashId)); return m_trashDirectories[trashId]; } QString TrashImpl::topDirectoryPath(int trashId) const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } assert(trashId != 0); Q_ASSERT(m_topDirectories.contains(trashId)); return m_topDirectories[trashId]; } // Helper method. Creates a URL with the format trash:/trashid-fileid or // trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory. QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath) { QUrl url; url.setScheme(QStringLiteral("trash")); QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId; if (!relativePath.isEmpty()) { path += QLatin1Char('/') + relativePath; } url.setPath(path); return url; } // Helper method. Parses a trash URL with the URL scheme defined in makeURL. // The trash:/ URL itself isn't parsed here, must be caught by the caller before hand. bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath) { if (url.scheme() != QLatin1String("trash")) { return false; } const QString path = url.path(); if (path.isEmpty()) { return false; } int start = 0; if (path[0] == QLatin1Char('/')) { // always true I hope start = 1; } int slashPos = path.indexOf(QLatin1Char('-'), 0); // don't match leading slash if (slashPos <= 0) { return false; } bool ok = false; trashId = path.midRef(start, slashPos - start).toInt(&ok); Q_ASSERT(ok); if (!ok) { return false; } start = slashPos + 1; slashPos = path.indexOf(QLatin1Char('/'), start); if (slashPos <= 0) { fileId = path.mid(start); relativePath.clear(); return true; } fileId = path.mid(start, slashPos - start); relativePath = path.mid(slashPos + 1); return true; } bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId) { KConfig config(QStringLiteral("ktrashrc")); const QString trashPath = trashDirectoryPath(trashId); KConfigGroup group = config.group(trashPath); bool useTimeLimit = group.readEntry("UseTimeLimit", false); bool useSizeLimit = group.readEntry("UseSizeLimit", true); double percent = group.readEntry("Percent", 10.0); int actionType = group.readEntry("LimitReachedAction", 0); if (useTimeLimit) { // delete all files in trash older than X days const int maxDays = group.readEntry("Days", 7); const QDateTime currentDate = QDateTime::currentDateTime(); const TrashedFileInfoList trashedFiles = list(); for (int i = 0; i < trashedFiles.count(); ++i) { struct TrashedFileInfo info = trashedFiles.at(i); if (info.trashId != trashId) { continue; } if (info.deletionDate.daysTo(currentDate) > maxDays) { del(info.trashId, info.fileId); } } } if (useSizeLimit) { // check if size limit exceeded // calculate size of the files to be put into the trash qulonglong additionalSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif TrashSizeCache trashSize(trashPath); DiscSpaceUtil util(trashPath + QLatin1String("/files/")); if (util.usage(trashSize.calculateSize() + additionalSize) >= percent) { // before we start to remove any files from the trash, // check whether the new file will fit into the trash // at all... qulonglong partitionSize = util.size(); if ((((double)additionalSize / (double)partitionSize) * 100) >= percent) { m_lastErrorCode = KIO::ERR_SLAVE_DEFINED; m_lastErrorMessage = i18n("The file is too large to be trashed."); return false; } if (actionType == 0) { // warn the user only m_lastErrorCode = KIO::ERR_SLAVE_DEFINED; m_lastErrorMessage = i18n("The trash has reached its maximum size!\nCleanup the trash manually."); return false; } else { // lets start removing some other files from the trash QDir dir(trashPath + QLatin1String("/files")); QFileInfoList infoList; if (actionType == 1) { // delete oldest files first infoList = dir.entryInfoList(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Time | QDir::Reversed); } else if (actionType == 2) { // delete biggest files first infoList = dir.entryInfoList(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Size); } else { qWarning("Should never happen!"); } bool deleteFurther = true; for (int i = 0; (i < infoList.count()) && deleteFurther; ++i) { const QFileInfo info = infoList.at(i); del(trashId, info.fileName()); // delete trashed file TrashSizeCache trashSize(trashPath); if (util.usage(trashSize.calculateSize() + additionalSize) < percent) { // check whether we have enough space now deleteFurther = false; } } } } } return true; } #include "moc_trashimpl.cpp" diff --git a/src/kcms/kio/smbrodlg.cpp b/src/kcms/kio/smbrodlg.cpp index 12ee1140..22e20090 100644 --- a/src/kcms/kio/smbrodlg.cpp +++ b/src/kcms/kio/smbrodlg.cpp @@ -1,187 +1,185 @@ /* This file is part of the KDE project Copyright (C) 2000 Alexander Neundorf 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. */ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII // Own #include "smbrodlg.h" // Qt #include #include #include // KDE #include #include #include #include #include //#include #include K_PLUGIN_FACTORY_DECLARATION(KioConfigFactory) SMBRoOptions::SMBRoOptions(QWidget *parent, const QVariantList &) : KCModule(parent) { QGridLayout *layout = new QGridLayout(this ); QLabel *label=new QLabel(i18n("These settings apply to network browsing only."),this); layout->addWidget(label,0,0, 1, 2 ); m_userLe=new QLineEdit(this); label=new QLabel(i18n("Default user name:"),this); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); label->setBuddy( m_userLe ); layout->addWidget(label,1,0); layout->addWidget(m_userLe,1,1); m_passwordLe=new QLineEdit(this); m_passwordLe->setEchoMode(QLineEdit::Password); label=new QLabel(i18n("Default password:"),this); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); label->setBuddy( m_passwordLe ); layout->addWidget(label,2,0); layout->addWidget(m_passwordLe,2,1); /* m_workgroupLe=new QLineEdit(this); label=new QLabel(m_workgroupLe,i18n("Workgroup:"),this); layout->addWidget(label,3,0); layout->addWidget(m_workgroupLe,3,1); m_showHiddenShares=new QCheckBox(i18n("Show hidden shares"),this); layout->addWidget(m_showHiddenShares,4,0, 1, 2 ); m_encodingList = new KComboBox( false, this ); QStringList _strList = KCharsets::charsets()->availableEncodingNames(); m_encodingList->addItems( _strList ); label = new QLabel( i18n( "MS Windows encoding:" ), this ); label->setBuddy( m_encodingList ); layout->addWidget( label, 3, 0 ); layout->addWidget( m_encodingList, 3, 1 ); */ layout->addWidget(new QWidget(this),4,0); // connect(m_showHiddenShares, SIGNAL(toggled(bool)), this, SLOT(changed())); connect(m_userLe, &QLineEdit::textChanged, this, &SMBRoOptions::changed); connect(m_passwordLe, &QLineEdit::textChanged, this, &SMBRoOptions::changed); // connect(m_workgroupLe, SIGNAL(textChanged(QString)), this, SLOT(changed())); // connect( m_encodingList, SIGNAL(activated(QString)), this , SLOT(changed()) ); layout->setRowStretch(4, 1); } SMBRoOptions::~SMBRoOptions() { } void SMBRoOptions::load() { KConfig *cfg = new KConfig(QStringLiteral("kioslaverc")); KConfigGroup group = cfg->group("Browser Settings/SMBro" ); m_userLe->setText(group.readEntry("User")); // m_workgroupLe->setText(group.readEntry("Workgroup")); // m_showHiddenShares->setChecked(group.readEntry("ShowHiddenShares", QVariant(false)).toBool()); //QStringList _strList = KCharsets::charsets()->availableEncodingNames(); //QString m_encoding = QTextCodec::codecForLocale()->name(); //m_encodingList->setCurrentIndex( _strList.indexOf( group.readEntry( "Encoding", m_encoding.toLower() ) ) ); // unscramble QString scrambled = group.readEntry( "Password" ); QString password; const int passwordLength = scrambled.length() / 3; password.reserve(passwordLength); for (int i=0; isetText(password); delete cfg; } void SMBRoOptions::save() { KConfig *cfg = new KConfig(QStringLiteral("kioslaverc")); KConfigGroup group = cfg->group("Browser Settings/SMBro" ); group.writeEntry( "User", m_userLe->text()); // group.writeEntry( "Workgroup", m_workgroupLe->text()); // group.writeEntry( "ShowHiddenShares", m_showHiddenShares->isChecked()); // group.writeEntry( "Encoding", m_encodingList->currentText() ); //taken from Nicola Brodu's smb ioslave //it's not really secure, but at //least better than storing the plain password QString password(m_passwordLe->text()); QString scrambled; - for (int i=0; i> 10; unsigned int a2 = (num & 0x3E0) >> 5; unsigned int a3 = (num & 0x1F); scrambled += (char)(a1+'0'); scrambled += (char)(a2+'A'); scrambled += (char)(a3+'0'); } group.writeEntry( "Password", scrambled); delete cfg; } void SMBRoOptions::defaults() { m_userLe->setText(QString()); m_passwordLe->setText(QString()); // m_workgroupLe->setText(""); // m_showHiddenShares->setChecked(false); } void SMBRoOptions::changed() { emit KCModule::changed(true); } QString SMBRoOptions::quickHelp() const { return i18n("

Windows Shares

Applications using the " "SMB kioslave (like Konqueror) are able to access shared Microsoft " "Windows file systems, if properly configured.

You can specify " "here the credentials used to access the shared resources. " "Passwords will be stored locally, and scrambled so as to render them " "unreadable to the human eye. For security reasons, you may not want to " "do that, as entries with passwords are clearly indicated as such.

"); } diff --git a/src/kntlm/kntlm.cpp b/src/kntlm/kntlm.cpp index 006326c5..61545745 100644 --- a/src/kntlm/kntlm.cpp +++ b/src/kntlm/kntlm.cpp @@ -1,416 +1,415 @@ /* This file is part of the KDE libraries Copyright (c) 2004 Szombathelyi Gy�gy The implementation is based on the documentation and sample code at http://davenport.sourceforge.net/ntlm.html The DES encryption functions are from libntlm at http://josefsson.org/libntlm/ 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 "kntlm.h" #include "des.h" #include #include #include #include #include static const char NTLM_SIGNATURE[] = "NTLMSSP"; static Q_CONSTEXPR int NTLM_BLOB_SIZE = 28; static QByteArray QString2UnicodeLE(const QString &target) { QByteArray unicode(target.length() * 2, 0); for (int i = 0; i < target.length(); i++) { ((quint16 *) unicode.data()) [ i ] = qToLittleEndian(target[i].unicode()); } return unicode; } static QString UnicodeLE2QString(const QChar *data, uint len) { QString ret; for (uint i = 0; i < len; i++) { ret += qFromLittleEndian(data[ i ].unicode()); } return ret; } static QByteArray getBuf(const QByteArray &buf, const KNTLM::SecBuf &secbuf) { quint32 offset = qFromLittleEndian(secbuf.offset); quint16 len = qFromLittleEndian(secbuf.len); //watch for buffer overflows if (offset > (quint32) buf.size() || offset + len > (quint32) buf.size()) { return QByteArray(); } return QByteArray(buf.data() + offset, len); } static void addBuf(QByteArray &buf, KNTLM::SecBuf &secbuf, const QByteArray &data) { quint32 offset = (buf.size() + 1) & 0xfffffffe; quint16 len = data.size(); quint16 maxlen = data.size(); secbuf.offset = qToLittleEndian((quint32) offset); secbuf.len = qToLittleEndian(len); secbuf.maxlen = qToLittleEndian(maxlen); buf.resize(offset + len); memcpy(buf.data() + offset, data.data(), data.size()); } static QString getString(const QByteArray &buf, const KNTLM::SecBuf &secbuf, bool unicode) { //watch for buffer overflows quint32 offset = qFromLittleEndian((quint32) secbuf.offset); quint16 len = qFromLittleEndian(secbuf.len); if (offset > (quint32) buf.size() || offset + len > (quint32) buf.size()) { return QString(); } const char *c = buf.data() + offset; if (unicode) { return UnicodeLE2QString((QChar *) c, len >> 1); } return QString::fromLatin1(c, len); } static void addString(QByteArray &buf, KNTLM::SecBuf &secbuf, const QString &str, bool unicode = false) { if (unicode) { addBuf(buf, secbuf, QString2UnicodeLE(str)); return; } addBuf(buf, secbuf, str.toLatin1()); } /* * turns a 56 bit key into the 64 bit, odd parity key and sets the key. * The key schedule ks is also set. */ static void convertKey(unsigned char *key_56, void *ks) { unsigned char key[8]; key[0] = key_56[0]; key[1] = ((key_56[0] << 7) & 0xFF) | (key_56[1] >> 1); key[2] = ((key_56[1] << 6) & 0xFF) | (key_56[2] >> 2); key[3] = ((key_56[2] << 5) & 0xFF) | (key_56[3] >> 3); key[4] = ((key_56[3] << 4) & 0xFF) | (key_56[4] >> 4); key[5] = ((key_56[4] << 3) & 0xFF) | (key_56[5] >> 5); key[6] = ((key_56[5] << 2) & 0xFF) | (key_56[6] >> 6); key[7] = (key_56[6] << 1) & 0xFF; - for (uint i = 0; i < 8; i++) { - unsigned char b = key[i]; - bool needsParity = ((((b >> 7) ^ (b >> 6) ^ (b >> 5) ^ (b >> 4) ^ (b >> 3) ^ (b >> 2) ^ (b >> 1)) & 0x01) == 0); + for (unsigned char &b : key) { + const bool needsParity = ((((b >> 7) ^ (b >> 6) ^ (b >> 5) ^ (b >> 4) ^ (b >> 3) ^ (b >> 2) ^ (b >> 1)) & 0x01) == 0); if (needsParity) { - key[i] |= 0x01; + b |= 0x01; } else { - key[i] &= 0xfe; + b &= 0xfe; } } ntlm_des_set_key((DES_KEY *) ks, (char *) &key, sizeof(key)); memset(&key, 0, sizeof(key)); } static QByteArray createBlob(const QByteArray &targetinfo) { QByteArray blob(NTLM_BLOB_SIZE + 4 + targetinfo.size(), 0); KNTLM::Blob *bl = (KNTLM::Blob *) blob.data(); bl->signature = qToBigEndian((quint32) 0x01010000); quint64 now = QDateTime::currentSecsSinceEpoch(); now += (quint64) 3600 * (quint64) 24 * (quint64) 134774; now *= (quint64) 10000000; bl->timestamp = qToLittleEndian(now); - for (uint i = 0; i < 8; i++) { - bl->challenge[i] = KRandom::random() % 0xff; + for (unsigned char &b : bl->challenge) { + b = KRandom::random() % 0xff; } memcpy(blob.data() + NTLM_BLOB_SIZE, targetinfo.data(), targetinfo.size()); return blob; } static QByteArray hmacMD5(const QByteArray &data, const QByteArray &key) { QByteArray ipad(64, 0x36); QByteArray opad(64, 0x5c); Q_ASSERT(key.size() <= 64); for (int i = qMin(key.size(), 64) - 1; i >= 0; i--) { ipad.data()[i] ^= key[i]; opad.data()[i] ^= key[i]; } QByteArray content(ipad + data); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(content); content = opad + md5.result(); md5.reset(); md5.addData(content); return md5.result(); } /*************************************** KNTLM implementation ***************************************/ bool KNTLM::getNegotiate(QByteArray &negotiate, const QString &domain, const QString &workstation, quint32 flags) { QByteArray rbuf(sizeof(Negotiate), 0); memcpy(rbuf.data(), NTLM_SIGNATURE, sizeof(NTLM_SIGNATURE)); ((Negotiate *) rbuf.data())->msgType = qToLittleEndian((quint32) 1); if (!domain.isEmpty()) { flags |= Negotiate_Domain_Supplied; addString(rbuf, ((Negotiate *) rbuf.data())->domain, domain); } if (!workstation.isEmpty()) { flags |= Negotiate_WS_Supplied; addString(rbuf, ((Negotiate *) rbuf.data())->workstation, workstation); } ((Negotiate *) rbuf.data())->flags = qToLittleEndian(flags); negotiate = rbuf; return true; } bool KNTLM::getAuth(QByteArray &auth, const QByteArray &challenge, const QString &user, const QString &password, const QString &domain, const QString &workstation, AuthFlags authflags) { QByteArray rbuf(sizeof(Auth), 0); Challenge *ch = (Challenge *) challenge.data(); QByteArray response; const uint chsize = challenge.size(); bool unicode = false; QString dom; //challenge structure too small if (chsize < 32) { return false; } unicode = qFromLittleEndian(ch->flags) & Negotiate_Unicode; // If the domain is NULL (i.e. QString()) use the target domain. If the domain is empty // (i.e. QString("")) use an empty domain. if (domain.isNull()) { dom = getString(challenge, ch->targetName, unicode); } else { dom = domain; } memcpy(rbuf.data(), NTLM_SIGNATURE, sizeof(NTLM_SIGNATURE)); ((Auth *) rbuf.data())->msgType = qToLittleEndian((quint32) 3); ((Auth *) rbuf.data())->flags = ch->flags; QByteArray targetInfo; if (chsize >= sizeof(Challenge)) { targetInfo = getBuf(challenge, ch->targetInfo); } if (!(authflags & Force_V1) && ((authflags & Force_V2) || (!targetInfo.isEmpty() && (qFromLittleEndian(ch->flags) & Negotiate_Target_Info))) /* may support NTLMv2 */) { bool ret = false; if (qFromLittleEndian(ch->flags) & Negotiate_NTLM) { if (targetInfo.isEmpty()) { return false; } response = getNTLMv2Response(dom, user, password, targetInfo, ch->challengeData); addBuf(rbuf, ((Auth *) rbuf.data())->ntResponse, response); ret = true; } if (authflags & Add_LM) { response = getLMv2Response(dom, user, password, ch->challengeData); addBuf(rbuf, ((Auth *) rbuf.data())->lmResponse, response); ret = true; } if (!ret) { return false; } } else { //if no targetinfo structure and NTLMv2 or LMv2 not forced, or v1 forced, try the older methods bool ret = false; if (qFromLittleEndian(ch->flags) & Negotiate_NTLM) { response = getNTLMResponse(password, ch->challengeData); addBuf(rbuf, ((Auth *) rbuf.data())->ntResponse, response); ret = true; } if (authflags & Add_LM) { response = getLMResponse(password, ch->challengeData); addBuf(rbuf, ((Auth *) rbuf.data())->lmResponse, response); ret = true; } if (!ret) { return false; } } if (!dom.isEmpty()) { addString(rbuf, ((Auth *) rbuf.data())->domain, dom, unicode); } addString(rbuf, ((Auth *) rbuf.data())->user, user, unicode); if (!workstation.isEmpty()) { addString(rbuf, ((Auth *) rbuf.data())->workstation, workstation, unicode); } auth = rbuf; return true; } QByteArray KNTLM::getLMResponse(const QString &password, const unsigned char *challenge) { QByteArray hash, answer; hash = lmHash(password); hash.resize(21); memset(hash.data() + 16, 0, 5); answer = lmResponse(hash, challenge); hash.fill(0); return answer; } QByteArray KNTLM::lmHash(const QString &password) { QByteArray keyBytes(14, 0); QByteArray hash(16, 0); DES_KEY ks; const char *magic = "KGS!@#$%"; strncpy(keyBytes.data(), password.toUpper().toLocal8Bit().constData(), 14); convertKey((unsigned char *) keyBytes.data(), &ks); ntlm_des_ecb_encrypt(magic, 8, &ks, (unsigned char *) hash.data()); convertKey((unsigned char *) keyBytes.data() + 7, &ks); ntlm_des_ecb_encrypt(magic, 8, &ks, (unsigned char *) hash.data() + 8); keyBytes.fill(0); memset(&ks, 0, sizeof(ks)); return hash; } QByteArray KNTLM::lmResponse(const QByteArray &hash, const unsigned char *challenge) { DES_KEY ks; QByteArray answer(24, 0); convertKey((unsigned char *) hash.data(), &ks); ntlm_des_ecb_encrypt(challenge, 8, &ks, (unsigned char *) answer.data()); convertKey((unsigned char *) hash.data() + 7, &ks); ntlm_des_ecb_encrypt(challenge, 8, &ks, (unsigned char *) answer.data() + 8); convertKey((unsigned char *) hash.data() + 14, &ks); ntlm_des_ecb_encrypt(challenge, 8, &ks, (unsigned char *) answer.data() + 16); memset(&ks, 0, sizeof(ks)); return answer; } QByteArray KNTLM::getNTLMResponse(const QString &password, const unsigned char *challenge) { QByteArray hash = ntlmHash(password); hash.resize(21); memset(hash.data() + 16, 0, 5); QByteArray answer = lmResponse(hash, challenge); hash.fill(0); return answer; } QByteArray KNTLM::ntlmHash(const QString &password) { QByteArray unicode; unicode = QString2UnicodeLE(password); return QCryptographicHash::hash(unicode, QCryptographicHash::Md4); } QByteArray KNTLM::getNTLMv2Response(const QString &target, const QString &user, const QString &password, const QByteArray &targetInformation, const unsigned char *challenge) { QByteArray hash = ntlmv2Hash(target, user, password); QByteArray blob = createBlob(targetInformation); return lmv2Response(hash, blob, challenge); } QByteArray KNTLM::getLMv2Response(const QString &target, const QString &user, const QString &password, const unsigned char *challenge) { QByteArray hash = ntlmv2Hash(target, user, password); QByteArray clientChallenge(8, 0); for (uint i = 0; i < 8; i++) { clientChallenge.data() [i] = KRandom::random() % 0xff; } return lmv2Response(hash, clientChallenge, challenge); } QByteArray KNTLM::ntlmv2Hash(const QString &target, const QString &user, const QString &password) { const QByteArray hash = ntlmHash(password); const QByteArray key = QString2UnicodeLE(user.toUpper() + target); return hmacMD5(key, hash); } QByteArray KNTLM::lmv2Response(const QByteArray &hash, const QByteArray &clientData, const unsigned char *challenge) { QByteArray data(8 + clientData.size(), 0); memcpy(data.data(), challenge, 8); memcpy(data.data() + 8, clientData.data(), clientData.size()); QByteArray mac = hmacMD5(data, hash); mac.resize(16 + clientData.size()); memcpy(mac.data() + 16, clientData.data(), clientData.size()); return mac; } diff --git a/src/kpac/proxyscout.cpp b/src/kpac/proxyscout.cpp index f6e82af0..3547d5f5 100644 --- a/src/kpac/proxyscout.cpp +++ b/src/kpac/proxyscout.cpp @@ -1,363 +1,363 @@ /* Copyright (c) 2003 Malte Starostik Copyright (c) 2011 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 "proxyscout.h" #include "config-kpac.h" #include "discovery.h" #include "script.h" #include #include #include #include #include #ifdef HAVE_KF5NOTIFICATIONS #include #endif #include #include #include #include #include namespace KPAC { K_PLUGIN_CLASS_WITH_JSON(ProxyScout, "proxyscout.json") enum ProxyType { Unknown = -1, Proxy, Socks, Direct }; static ProxyType proxyTypeFor(const QString &mode) { if (mode.compare(QLatin1String("PROXY"), Qt::CaseInsensitive) == 0) { return Proxy; } if (mode.compare(QLatin1String("DIRECT"), Qt::CaseInsensitive) == 0) { return Direct; } if (mode.compare(QLatin1String("SOCKS"), Qt::CaseInsensitive) == 0 || mode.compare(QLatin1String("SOCKS5"), Qt::CaseInsensitive) == 0) { return Socks; } return Unknown; } ProxyScout::QueuedRequest::QueuedRequest(const QDBusMessage &reply, const QUrl &u, bool sendall) : transaction(reply), url(u), sendAll(sendall) { } ProxyScout::ProxyScout(QObject *parent, const QList &) : KDEDModule(parent), m_componentName(QStringLiteral("proxyscout")), m_downloader(nullptr), m_script(nullptr), m_suspendTime(0), m_watcher(nullptr), m_networkConfig(new QNetworkConfigurationManager(this)) { connect(m_networkConfig, &QNetworkConfigurationManager::configurationChanged, this, &ProxyScout::disconnectNetwork); } ProxyScout::~ProxyScout() { delete m_script; } QStringList ProxyScout::proxiesForUrl(const QString &checkUrl, const QDBusMessage &msg) { QUrl url(checkUrl); if (m_suspendTime) { if (std::time(nullptr) - m_suspendTime < 300) { return QStringList(QStringLiteral("DIRECT")); } m_suspendTime = 0; } // Never use a proxy for the script itself if (m_downloader && url.matches(m_downloader->scriptUrl(), QUrl::StripTrailingSlash)) { return QStringList(QStringLiteral("DIRECT")); } if (m_script) { return handleRequest(url); } if (m_downloader || startDownload()) { msg.setDelayedReply(true); m_requestQueue.append(QueuedRequest(msg, url, true)); return QStringList(); // return value will be ignored } return QStringList(QStringLiteral("DIRECT")); } QString ProxyScout::proxyForUrl(const QString &checkUrl, const QDBusMessage &msg) { QUrl url(checkUrl); if (m_suspendTime) { if (std::time(nullptr) - m_suspendTime < 300) { return QStringLiteral("DIRECT"); } m_suspendTime = 0; } // Never use a proxy for the script itself if (m_downloader && url.matches(m_downloader->scriptUrl(), QUrl::StripTrailingSlash)) { return QStringLiteral("DIRECT"); } if (m_script) { return handleRequest(url).constFirst(); } if (m_downloader || startDownload()) { msg.setDelayedReply(true); m_requestQueue.append(QueuedRequest(msg, url)); return QString(); // return value will be ignored } return QStringLiteral("DIRECT"); } void ProxyScout::blackListProxy(const QString &proxy) { m_blackList[ proxy ] = std::time(nullptr); } void ProxyScout::reset() { delete m_script; m_script = nullptr; delete m_downloader; m_downloader = nullptr; delete m_watcher; m_watcher = nullptr; m_blackList.clear(); m_suspendTime = 0; KProtocolManager::reparseConfiguration(); } bool ProxyScout::startDownload() { switch (KProtocolManager::proxyType()) { case KProtocolManager::WPADProxy: if (m_downloader && !qobject_cast(m_downloader)) { delete m_downloader; m_downloader = nullptr; } if (!m_downloader) { m_downloader = new Discovery(this); connect(m_downloader, QOverload::of(&Downloader::result), this, &ProxyScout::downloadResult); } break; case KProtocolManager::PACProxy: { if (m_downloader && !qobject_cast(m_downloader)) { delete m_downloader; m_downloader = nullptr; } if (!m_downloader) { m_downloader = new Downloader(this); connect(m_downloader, QOverload::of(&Downloader::result), this, &ProxyScout::downloadResult); } const QUrl url(KProtocolManager::proxyConfigScript()); if (url.isLocalFile()) { if (!m_watcher) { m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ProxyScout::proxyScriptFileChanged); } proxyScriptFileChanged(url.path()); } else { delete m_watcher; m_watcher = nullptr; m_downloader->download(url); } break; } default: return false; } return true; } void ProxyScout::disconnectNetwork(const QNetworkConfiguration &config) { // NOTE: We only care of Defined state because we only want //to redo WPAD when a network interface is brought out of //hibernation or restarted for whatever reason. if (config.state() == QNetworkConfiguration::Defined) { reset(); } } void ProxyScout::downloadResult(bool success) { if (success) { try { if (!m_script) { m_script = new Script(m_downloader->script()); } } catch (const Script::Error &e) { qWarning() << "Error:" << e.message(); #ifdef HAVE_KF5NOTIFICATIONS KNotification *notify = new KNotification(QStringLiteral("script-error")); notify->setText(i18n("The proxy configuration script is invalid:\n%1", e.message())); notify->setComponentName(m_componentName); notify->sendEvent(); #endif success = false; } } else { #ifdef HAVE_KF5NOTIFICATIONS KNotification *notify = new KNotification(QStringLiteral("download-error")); notify->setText(m_downloader->error()); notify->setComponentName(m_componentName); notify->sendEvent(); #endif } if (success) { - for (RequestQueue::Iterator it = m_requestQueue.begin(), itEnd = m_requestQueue.end(); it != itEnd; ++it) { - if ((*it).sendAll) { - const QVariant result(handleRequest((*it).url)); - QDBusConnection::sessionBus().send((*it).transaction.createReply(result)); + for (const QueuedRequest &request : qAsConst(m_requestQueue)) { + if (request.sendAll) { + const QVariant result(handleRequest(request.url)); + QDBusConnection::sessionBus().send(request.transaction.createReply(result)); } else { - const QVariant result(handleRequest((*it).url).constFirst()); - QDBusConnection::sessionBus().send((*it).transaction.createReply(result)); + const QVariant result(handleRequest(request.url).constFirst()); + QDBusConnection::sessionBus().send(request.transaction.createReply(result)); } } } else { - for (RequestQueue::Iterator it = m_requestQueue.begin(), itEnd = m_requestQueue.end(); it != itEnd; ++it) { - QDBusConnection::sessionBus().send((*it).transaction.createReply(QLatin1String("DIRECT"))); + for (const QueuedRequest &request : qAsConst(m_requestQueue)) { + QDBusConnection::sessionBus().send(request.transaction.createReply(QLatin1String("DIRECT"))); } } m_requestQueue.clear(); // Suppress further attempts for 5 minutes if (!success) { m_suspendTime = std::time(nullptr); } } void ProxyScout::proxyScriptFileChanged(const QString &path) { // Should never get called if we do not have a watcher... Q_ASSERT(m_watcher); // Remove the current file being watched... if (!m_watcher->files().isEmpty()) { m_watcher->removePaths(m_watcher->files()); } // NOTE: QFileSystemWatcher only adds a path if it either exists or // is not already being monitored. m_watcher->addPath(path); // Reload... m_downloader->download(QUrl::fromLocalFile(path)); } QStringList ProxyScout::handleRequest(const QUrl &url) { try { QStringList proxyList; const QString result = m_script->evaluate(url).trimmed(); const QStringList proxies = result.split(QLatin1Char(';'), QString::SkipEmptyParts); const int size = proxies.count(); for (int i = 0; i < size; ++i) { QString mode, address; const QString proxy = proxies.at(i).trimmed(); const int index = proxy.indexOf(QLatin1Char(' ')); if (index == -1) { // Only "DIRECT" should match this! mode = proxy; address = proxy; } else { mode = proxy.left(index); address = proxy.mid(index + 1).trimmed(); } const ProxyType type = proxyTypeFor(mode); if (type == Unknown) { continue; } if (type == Proxy || type == Socks) { const int index = address.indexOf(QLatin1Char(':')); if (index == -1 || !KProtocolInfo::isKnownProtocol(address.left(index))) { const QString protocol((type == Proxy ? QStringLiteral("http://") : QStringLiteral("socks://"))); const QUrl url(protocol + address); if (url.isValid()) { address = url.toString(); } else { continue; } } } if (type == Direct || !m_blackList.contains(address)) { proxyList << address; } else if (std::time(nullptr) - m_blackList[address] > 1800) { // 30 minutes // black listing expired m_blackList.remove(address); proxyList << address; } } if (!proxyList.isEmpty()) { // qDebug() << proxyList; return proxyList; } // FIXME: blacklist } catch (const Script::Error &e) { qCritical() << e.message(); #ifdef HAVE_KF5NOTIFICATIONS KNotification *n = new KNotification(QStringLiteral("evaluation-error")); n->setText(i18n("The proxy configuration script returned an error:\n%1", e.message())); n->setComponentName(m_componentName); n->sendEvent(); #endif } return QStringList(QStringLiteral("DIRECT")); } } #include "proxyscout.moc" diff --git a/src/kssld/kssld.cpp b/src/kssld/kssld.cpp index 9d3d4738..72ab3ad2 100644 --- a/src/kssld/kssld.cpp +++ b/src/kssld/kssld.cpp @@ -1,269 +1,268 @@ /* This file is part of the KDE libraries Copyright (c) 2007, 2008, 2010 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 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 "kssld.h" #include "ksslcertificatemanager.h" #include "kssld_adaptor.h" #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(KSSLD, "kssld.json") class KSSLDPrivate { public: KSSLDPrivate() : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig) { struct strErr { const char *str; KSslError::Error err; }; //hmmm, looks like these are all of the errors where it is possible to continue. const static strErr strError[] = { {"NoError", KSslError::NoError}, {"UnknownError", KSslError::UnknownError}, {"InvalidCertificateAuthority", KSslError::InvalidCertificateAuthorityCertificate}, {"InvalidCertificate", KSslError::InvalidCertificate}, {"CertificateSignatureFailed", KSslError::CertificateSignatureFailed}, {"SelfSignedCertificate", KSslError::SelfSignedCertificate}, {"RevokedCertificate", KSslError::RevokedCertificate}, {"InvalidCertificatePurpose", KSslError::InvalidCertificatePurpose}, {"RejectedCertificate", KSslError::RejectedCertificate}, {"UntrustedCertificate", KSslError::UntrustedCertificate}, {"ExpiredCertificate", KSslError::ExpiredCertificate}, {"HostNameMismatch", KSslError::HostNameMismatch} }; - for (int i = 0; i < int(sizeof(strError) / sizeof(strErr)); i++) { - QString s = QString::fromLatin1(strError[i].str); - KSslError::Error e = strError[i].err; - stringToSslError.insert(s, e); - sslErrorToString.insert(e, s); + for (const strErr &row : strError) { + QString s = QString::fromLatin1(row.str); + stringToSslError.insert(s, row.err); + sslErrorToString.insert(row.err, s); } } KConfig config; QHash stringToSslError; QHash sslErrorToString; }; KSSLD::KSSLD(QObject *parent, const QVariantList &) : KDEDModule(parent), d(new KSSLDPrivate()) { new KSSLDAdaptor(this); pruneExpiredRules(); } KSSLD::~KSSLD() { delete d; } void KSSLD::setRule(const KSslCertificateRule &rule) { if (rule.hostName().isEmpty()) { return; } KConfigGroup group = d->config.group(rule.certificate().digest().toHex()); QStringList sl; QString dtString = QStringLiteral("ExpireUTC "); dtString.append(rule.expiryDateTime().toString(Qt::ISODate)); sl.append(dtString); if (rule.isRejected()) { sl.append(QStringLiteral("Reject")); } else { foreach (KSslError::Error e, rule.ignoredErrors()) { sl.append(d->sslErrorToString.value(e)); } } if (!group.hasKey("CertificatePEM")) { group.writeEntry("CertificatePEM", rule.certificate().toPem()); } #ifdef PARANOIA else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) { return; } #endif group.writeEntry(rule.hostName(), sl); group.sync(); } void KSSLD::clearRule(const KSslCertificateRule &rule) { clearRule(rule.certificate(), rule.hostName()); } void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName) { KConfigGroup group = d->config.group(cert.digest().toHex()); group.deleteEntry(hostName); if (group.keyList().size() < 2) { group.deleteGroup(); } group.sync(); } void KSSLD::pruneExpiredRules() { // expired rules are deleted when trying to load them, so we just try to load all rules. // be careful about iterating over KConfig(Group) while changing it foreach (const QString &groupName, d->config.groupList()) { QByteArray certDigest = groupName.toLatin1(); foreach (const QString &key, d->config.group(groupName).keyList()) { if (key == QLatin1String("CertificatePEM")) { continue; } KSslCertificateRule r = rule(QSslCertificate(certDigest), key); } } } // check a domain name with subdomains for well-formedness and count the dot-separated parts static QString normalizeSubdomains(const QString &hostName, int *namePartsCount) { QString ret; int partsCount = 0; bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part const int length = hostName.length(); for (int i = 0; i < length; i++) { const QChar c = hostName.at(i); if (c == QLatin1Char('.')) { if (wasPrevDot || (i + 1 == hostName.length())) { // consecutive dots or a dot at the end are forbidden partsCount = 0; ret.clear(); break; } wasPrevDot = true; } else { if (wasPrevDot) { partsCount++; } wasPrevDot = false; } ret.append(c); } *namePartsCount = partsCount; return ret; } KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const { const QByteArray certDigest = cert.digest().toHex(); KConfigGroup group = d->config.group(certDigest); KSslCertificateRule ret(cert, hostName); bool foundHostName = false; int needlePartsCount; QString needle = normalizeSubdomains(hostName, &needlePartsCount); // Find a rule for the hostname, either... if (group.hasKey(needle)) { // directly (host, site.tld, a.site.tld etc) if (needlePartsCount >= 1) { foundHostName = true; } } else { // or with wildcards // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches, // "a.site.tld" <- "*.site.tld" is while (--needlePartsCount >= 2) { const int dotIndex = needle.indexOf(QLatin1Char('.')); Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed needle.remove(0, dotIndex - 1); needle[0] = QChar::fromLatin1('*'); if (group.hasKey(needle)) { foundHostName = true; break; } needle.remove(0, 2); // remove "*." } } if (!foundHostName) { //Don't make a rule with the failed wildcard pattern - use the original hostname. return KSslCertificateRule(cert, hostName); } //parse entry of the format "ExpireUTC , Reject" or //"ExpireUTC , HostNameMismatch, ExpiredCertificate, ..." QStringList sl = group.readEntry(needle, QStringList()); QDateTime expiryDt; // the rule is well-formed if it contains at least the expire date and one directive if (sl.size() >= 2) { QString dtString = sl.takeFirst(); if (dtString.startsWith(QLatin1String("ExpireUTC "))) { dtString.remove(0, 10/* length of "ExpireUTC " */); expiryDt = QDateTime::fromString(dtString, Qt::ISODate); } } if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) { //the entry is malformed or expired so we remove it group.deleteEntry(needle); //the group is useless once only the CertificatePEM entry left if (group.keyList().size() < 2) { group.deleteGroup(); } return ret; } QList ignoredErrors; bool isRejected = false; for (const QString &s : qAsConst(sl)) { if (s == QLatin1String("Reject")) { isRejected = true; ignoredErrors.clear(); break; } if (!d->stringToSslError.contains(s)) { continue; } ignoredErrors.append(d->stringToSslError.value(s)); } //Everything is checked and we can make ret valid ret.setExpiryDateTime(expiryDt); ret.setRejected(isRejected); ret.setIgnoredErrors(ignoredErrors); return ret; } #include "moc_kssld.cpp" #include "moc_kssld_adaptor.cpp" #include "kssld.moc" diff --git a/src/widgets/kfileitemdelegate.cpp b/src/widgets/kfileitemdelegate.cpp index d9e887e6..f3a37a60 100644 --- a/src/widgets/kfileitemdelegate.cpp +++ b/src/widgets/kfileitemdelegate.cpp @@ -1,1614 +1,1608 @@ /* This file is part of the KDE project Copyright (C) 2009 Shaun Reich Copyright © 2006-2007, 2008 Fredrik Höglund 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 "kfileitemdelegate.h" #include "imagefilter_p.h" #include // was 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 "delegateanimationhandler_p.h" #if 0 // was: HAVE_X11 && HAVE_XRENDER # include # include # include # undef KeyPress # undef FocusOut #endif struct Margin { int left, right, top, bottom; }; class Q_DECL_HIDDEN KFileItemDelegate::Private { public: enum MarginType { ItemMargin = 0, TextMargin, IconMargin, NMargins }; explicit Private(KFileItemDelegate *parent); ~Private() {} QSize decorationSizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; QSize displaySizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; QString replaceNewlines(const QString &string) const; inline KFileItem fileItem(const QModelIndex &index) const; QString elidedText(QTextLayout &layout, const QStyleOptionViewItem &option, const QSize &maxSize) const; QSize layoutText(QTextLayout &layout, const QStyleOptionViewItem &option, const QString &text, const QSize &constraints) const; QSize layoutText(QTextLayout &layout, const QString &text, int maxWidth) const; inline void setLayoutOptions(QTextLayout &layout, const QStyleOptionViewItem &options) const; inline bool verticalLayout(const QStyleOptionViewItem &option) const; inline QBrush brush(const QVariant &value, const QStyleOptionViewItem &option) const; QBrush foregroundBrush(const QStyleOptionViewItem &option, const QModelIndex &index) const; inline void setActiveMargins(Qt::Orientation layout); void setVerticalMargin(MarginType type, int left, int right, int top, int bottom); void setHorizontalMargin(MarginType type, int left, int right, int top, int bottom); inline void setVerticalMargin(MarginType type, int hor, int ver); inline void setHorizontalMargin(MarginType type, int hor, int ver); inline QRect addMargin(const QRect &rect, MarginType type) const; inline QRect subtractMargin(const QRect &rect, MarginType type) const; inline QSize addMargin(const QSize &size, MarginType type) const; inline QSize subtractMargin(const QSize &size, MarginType type) const; QString itemSize(const QModelIndex &index, const KFileItem &item) const; QString information(const QStyleOptionViewItem &option, const QModelIndex &index, const KFileItem &item) const; bool isListView(const QStyleOptionViewItem &option) const; QString display(const QModelIndex &index) const; QIcon decoration(const QStyleOptionViewItem &option, const QModelIndex &index) const; QPoint iconPosition(const QStyleOptionViewItem &option) const; QRect labelRectangle(const QStyleOptionViewItem &option) const; void layoutTextItems(const QStyleOptionViewItem &option, const QModelIndex &index, QTextLayout *labelLayout, QTextLayout *infoLayout, QRect *textBoundingRect) const; void drawTextItems(QPainter *painter, const QTextLayout &labelLayout, const QTextLayout &infoLayout, const QRect &textBoundingRect) const; KIO::AnimationState *animationState(const QStyleOptionViewItem &option, const QModelIndex &index, const QAbstractItemView *view) const; void restartAnimation(KIO::AnimationState *state); QPixmap applyHoverEffect(const QPixmap &icon) const; QPixmap transition(const QPixmap &from, const QPixmap &to, qreal amount) const; void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const; void drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const; void gotNewIcon(const QModelIndex &index); void paintJobTransfers(QPainter *painter, const qreal &jobAnimationAngle, const QPoint &iconPos, const QStyleOptionViewItem &opt); public: KFileItemDelegate::InformationList informationList; QColor shadowColor; QPointF shadowOffset; qreal shadowBlur; QSize maximumSize; bool showToolTipWhenElided; QTextOption::WrapMode wrapMode; bool jobTransfersVisible; QIcon downArrowIcon; private: KIO::DelegateAnimationHandler *animationHandler; Margin verticalMargin[NMargins]; Margin horizontalMargin[NMargins]; Margin *activeMargins; }; KFileItemDelegate::Private::Private(KFileItemDelegate *parent) : shadowColor(Qt::transparent), shadowOffset(1, 1), shadowBlur(2), maximumSize(0, 0), showToolTipWhenElided(true), wrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere), jobTransfersVisible(false), animationHandler(new KIO::DelegateAnimationHandler(parent)), activeMargins(nullptr) { } void KFileItemDelegate::Private::setActiveMargins(Qt::Orientation layout) { activeMargins = (layout == Qt::Horizontal ? horizontalMargin : verticalMargin); } void KFileItemDelegate::Private::setVerticalMargin(MarginType type, int left, int top, int right, int bottom) { verticalMargin[type].left = left; verticalMargin[type].right = right; verticalMargin[type].top = top; verticalMargin[type].bottom = bottom; } void KFileItemDelegate::Private::setHorizontalMargin(MarginType type, int left, int top, int right, int bottom) { horizontalMargin[type].left = left; horizontalMargin[type].right = right; horizontalMargin[type].top = top; horizontalMargin[type].bottom = bottom; } void KFileItemDelegate::Private::setVerticalMargin(MarginType type, int horizontal, int vertical) { setVerticalMargin(type, horizontal, vertical, horizontal, vertical); } void KFileItemDelegate::Private::setHorizontalMargin(MarginType type, int horizontal, int vertical) { setHorizontalMargin(type, horizontal, vertical, horizontal, vertical); } QRect KFileItemDelegate::Private::addMargin(const QRect &rect, MarginType type) const { Q_ASSERT(activeMargins != nullptr); const Margin &m = activeMargins[type]; return rect.adjusted(-m.left, -m.top, m.right, m.bottom); } QRect KFileItemDelegate::Private::subtractMargin(const QRect &rect, MarginType type) const { Q_ASSERT(activeMargins != nullptr); const Margin &m = activeMargins[type]; return rect.adjusted(m.left, m.top, -m.right, -m.bottom); } QSize KFileItemDelegate::Private::addMargin(const QSize &size, MarginType type) const { Q_ASSERT(activeMargins != nullptr); const Margin &m = activeMargins[type]; return QSize(size.width() + m.left + m.right, size.height() + m.top + m.bottom); } QSize KFileItemDelegate::Private::subtractMargin(const QSize &size, MarginType type) const { Q_ASSERT(activeMargins != nullptr); const Margin &m = activeMargins[type]; return QSize(size.width() - m.left - m.right, size.height() - m.top - m.bottom); } // Returns the size of a file, or the number of items in a directory, as a QString QString KFileItemDelegate::Private::itemSize(const QModelIndex &index, const KFileItem &item) const { // Return a formatted string containing the file size, if the item is a file if (item.isFile()) { return KIO::convertSize(item.size()); } // Return the number of items in the directory const QVariant value = index.data(KDirModel::ChildCountRole); const int count = value.type() == QVariant::Int ? value.toInt() : KDirModel::ChildCountUnknown; if (count == KDirModel::ChildCountUnknown) { // was: i18nc("Items in a folder", "? items"); // but this just looks useless in a remote directory listing, // better not show anything. return QString(); } return i18ncp("Items in a folder", "1 item", "%1 items", count); } // Returns the additional information string, if one should be shown, or an empty string otherwise QString KFileItemDelegate::Private::information(const QStyleOptionViewItem &option, const QModelIndex &index, const KFileItem &item) const { QString string; if (informationList.isEmpty() || item.isNull() || !isListView(option)) { return string; } foreach (KFileItemDelegate::Information info, informationList) { if (info == KFileItemDelegate::NoInformation) { continue; } if (!string.isEmpty()) { string += QChar::LineSeparator; } switch (info) { case KFileItemDelegate::Size: string += itemSize(index, item); break; case KFileItemDelegate::Permissions: string += item.permissionsString(); break; case KFileItemDelegate::OctalPermissions: string += QLatin1Char('0') + QString::number(item.permissions(), 8); break; case KFileItemDelegate::Owner: string += item.user(); break; case KFileItemDelegate::OwnerAndGroup: string += item.user() + QLatin1Char(':') + item.group(); break; case KFileItemDelegate::CreationTime: string += item.timeString(KFileItem::CreationTime); break; case KFileItemDelegate::ModificationTime: string += item.timeString(KFileItem::ModificationTime); break; case KFileItemDelegate::AccessTime: string += item.timeString(KFileItem::AccessTime); break; case KFileItemDelegate::MimeType: string += item.isMimeTypeKnown() ? item.mimetype() : i18nc("@info mimetype", "Unknown"); break; case KFileItemDelegate::FriendlyMimeType: string += item.isMimeTypeKnown() ? item.mimeComment() : i18nc("@info mimetype", "Unknown"); break; case KFileItemDelegate::LinkDest: string += item.linkDest(); break; case KFileItemDelegate::LocalPathOrUrl: if (!item.localPath().isEmpty()) { string += item.localPath(); } else { string += item.url().toDisplayString(); } break; case KFileItemDelegate::Comment: string += item.comment(); break; default: break; } // switch (info) } // foreach (info, list) return string; } // Returns the KFileItem for the index KFileItem KFileItemDelegate::Private::fileItem(const QModelIndex &index) const { const QVariant value = index.data(KDirModel::FileItemRole); return qvariant_cast(value); } // Replaces any newline characters in the provided string, with QChar::LineSeparator QString KFileItemDelegate::Private::replaceNewlines(const QString &text) const { QString string = text; - const QChar newline = QLatin1Char('\n'); - - for (int i = 0; i < string.length(); i++) - if (string[i] == newline) { - string[i] = QChar::LineSeparator; - } - + string.replace(QLatin1Char('\n'), QChar::LineSeparator); return string; } // Lays the text out in a rectangle no larger than constraints, eliding it as necessary QSize KFileItemDelegate::Private::layoutText(QTextLayout &layout, const QStyleOptionViewItem &option, const QString &text, const QSize &constraints) const { const QSize size = layoutText(layout, text, constraints.width()); if (size.width() > constraints.width() || size.height() > constraints.height()) { const QString elided = elidedText(layout, option, constraints); return layoutText(layout, elided, constraints.width()); } return size; } // Lays the text out in a rectangle no wider than maxWidth QSize KFileItemDelegate::Private::layoutText(QTextLayout &layout, const QString &text, int maxWidth) const { QFontMetrics metrics(layout.font()); int leading = metrics.leading(); int height = 0; qreal widthUsed = 0; QTextLine line; layout.setText(text); layout.beginLayout(); while ((line = layout.createLine()).isValid()) { line.setLineWidth(maxWidth); height += leading; line.setPosition(QPoint(0, height)); height += int(line.height()); widthUsed = qMax(widthUsed, line.naturalTextWidth()); } layout.endLayout(); return QSize(qCeil(widthUsed), height); } // Elides the text in the layout, by iterating over each line in the layout, eliding // or word breaking the line if it's wider than the max width, and finally adding an // ellipses at the end of the last line, if there are more lines than will fit within // the vertical size constraints. QString KFileItemDelegate::Private::elidedText(QTextLayout &layout, const QStyleOptionViewItem &option, const QSize &size) const { const QString text = layout.text(); int maxWidth = size.width(); int maxHeight = size.height(); qreal height = 0; bool wrapText = (option.features & QStyleOptionViewItem::WrapText); // If the string contains a single line of text that shouldn't be word wrapped if (!wrapText && text.indexOf(QChar::LineSeparator) == -1) { return option.fontMetrics.elidedText(text, option.textElideMode, maxWidth); } // Elide each line that has already been laid out in the layout. QString elided; elided.reserve(text.length()); for (int i = 0; i < layout.lineCount(); i++) { QTextLine line = layout.lineAt(i); int start = line.textStart(); int length = line.textLength(); height += option.fontMetrics.leading(); if (height + line.height() + option.fontMetrics.lineSpacing() > maxHeight) { // Unfortunately, if the line ends because of a line separator, elidedText() will be too // clever and keep adding lines until it finds one that's too wide. if (line.naturalTextWidth() < maxWidth && text[start + length - 1] == QChar::LineSeparator) { elided += text.midRef(start, length - 1); } else { elided += option.fontMetrics.elidedText(text.mid(start), option.textElideMode, maxWidth); } break; } else if (line.naturalTextWidth() > maxWidth) { elided += option.fontMetrics.elidedText(text.mid(start, length), option.textElideMode, maxWidth); if (!elided.endsWith(QChar::LineSeparator)) { elided += QChar::LineSeparator; } } else { elided += text.midRef(start, length); } height += line.height(); } return elided; } void KFileItemDelegate::Private::setLayoutOptions(QTextLayout &layout, const QStyleOptionViewItem &option) const { QTextOption textoption; textoption.setTextDirection(option.direction); textoption.setAlignment(QStyle::visualAlignment(option.direction, option.displayAlignment)); textoption.setWrapMode((option.features & QStyleOptionViewItem::WrapText) ? wrapMode : QTextOption::NoWrap); layout.setFont(option.font); layout.setTextOption(textoption); } QSize KFileItemDelegate::Private::displaySizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QString label = option.text; int maxWidth = 0; if (maximumSize.isEmpty()) { maxWidth = verticalLayout(option) && (option.features & QStyleOptionViewItem::WrapText) ? option.decorationSize.width() + 10 : 32757; } else { const Margin &itemMargin = activeMargins[ItemMargin]; const Margin &textMargin = activeMargins[TextMargin]; maxWidth = maximumSize.width() - (itemMargin.left + itemMargin.right) - (textMargin.left + textMargin.right); } KFileItem item = fileItem(index); // To compute the nominal size for the label + info, we'll just append // the information string to the label const QString info = information(option, index, item); if (!info.isEmpty()) { label += QChar(QChar::LineSeparator) + info; } QTextLayout layout; setLayoutOptions(layout, option); QSize size = layoutText(layout, label, maxWidth); if (!info.isEmpty()) { // As soon as additional information is shown, it might be necessary that // the label and/or the additional information must get elided. To prevent // an expensive eliding in the scope of displaySizeHint, the maximum // width is reserved instead. size.setWidth(maxWidth); } return addMargin(size, TextMargin); } QSize KFileItemDelegate::Private::decorationSizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index) QSize iconSize = option.icon.actualSize(option.decorationSize); if (!verticalLayout(option)) { iconSize.rwidth() = option.decorationSize.width(); } else if (iconSize.width() < option.decorationSize.width()) { iconSize.rwidth() = qMin(iconSize.width() + 10, option.decorationSize.width()); } if (iconSize.height() < option.decorationSize.height()) { iconSize.rheight() = option.decorationSize.height(); } return addMargin(iconSize, IconMargin); } bool KFileItemDelegate::Private::verticalLayout(const QStyleOptionViewItem &option) const { return (option.decorationPosition == QStyleOptionViewItem::Top || option.decorationPosition == QStyleOptionViewItem::Bottom); } // Converts a QVariant of type Brush or Color to a QBrush QBrush KFileItemDelegate::Private::brush(const QVariant &value, const QStyleOptionViewItem &option) const { if (value.userType() == qMetaTypeId()) { return qvariant_cast(value).brush(option.palette); } switch (value.type()) { case QVariant::Color: return QBrush(qvariant_cast(value)); case QVariant::Brush: return qvariant_cast(value); default: return QBrush(Qt::NoBrush); } } QBrush KFileItemDelegate::Private::foregroundBrush(const QStyleOptionViewItem &option, const QModelIndex &index) const { QPalette::ColorGroup cg = QPalette::Active; if (!(option.state & QStyle::State_Enabled)) { cg = QPalette::Disabled; } else if (!(option.state & QStyle::State_Active)) { cg = QPalette::Inactive; } // Always use the highlight color for selected items if (option.state & QStyle::State_Selected) { return option.palette.brush(cg, QPalette::HighlightedText); } // If the model provides its own foreground color/brush for this item const QVariant value = index.data(Qt::ForegroundRole); if (value.isValid()) { return brush(value, option); } return option.palette.brush(cg, QPalette::Text); } bool KFileItemDelegate::Private::isListView(const QStyleOptionViewItem &option) const { if (qobject_cast(option.widget) || verticalLayout(option)) { return true; } return false; } QPixmap KFileItemDelegate::Private::applyHoverEffect(const QPixmap &icon) const { KIconEffect *effect = KIconLoader::global()->iconEffect(); // Note that in KIconLoader terminology, active = hover. // ### We're assuming that the icon group is desktop/filemanager, since this // is KFileItemDelegate. if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) { return effect->apply(icon, KIconLoader::Desktop, KIconLoader::ActiveState); } return icon; } void KFileItemDelegate::Private::gotNewIcon(const QModelIndex &index) { animationHandler->gotNewIcon(index); } void KFileItemDelegate::Private::restartAnimation(KIO::AnimationState *state) { animationHandler->restartAnimation(state); } KIO::AnimationState *KFileItemDelegate::Private::animationState(const QStyleOptionViewItem &option, const QModelIndex &index, const QAbstractItemView *view) const { if (!option.widget->style()->styleHint(QStyle::SH_Widget_Animate, nullptr, option.widget)) { return nullptr; } if (index.column() == KDirModel::Name) { return animationHandler->animationState(option, index, view); } return nullptr; } QPixmap KFileItemDelegate::Private::transition(const QPixmap &from, const QPixmap &to, qreal amount) const { int value = int(0xff * amount); if (value == 0 || to.isNull()) { return from; } if (value == 0xff || from.isNull()) { return to; } QColor color; color.setAlphaF(amount); // FIXME: Somehow this doesn't work on Mac OS.. #if defined(Q_OS_MAC) const bool usePixmap = false; #else const bool usePixmap = from.paintEngine()->hasFeature(QPaintEngine::PorterDuff) && from.paintEngine()->hasFeature(QPaintEngine::BlendModes); #endif // If the native paint engine supports Porter/Duff compositing and CompositionMode_Plus if (usePixmap) { QPixmap under = from; QPixmap over = to; QPainter p; p.begin(&over); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); p.fillRect(over.rect(), color); p.end(); p.begin(&under); p.setCompositionMode(QPainter::CompositionMode_DestinationOut); p.fillRect(under.rect(), color); p.setCompositionMode(QPainter::CompositionMode_Plus); p.drawPixmap(0, 0, over); p.end(); return under; } // Cannot use XRender with QPixmap anymore. #if 0 // HAVE_X11 && HAVE_XRENDER else if (from.paintEngine()->hasFeature(QPaintEngine::PorterDuff)) { // We have Xrender support // QX11PaintEngine doesn't implement CompositionMode_Plus in Qt 4.3, // which we need to be able to do a transition from one pixmap to // another. // // In order to avoid the overhead of converting the pixmaps to images // and doing the operation entirely in software, this function has a // specialized path for X11 that uses Xrender directly to do the // transition. This operation can be fully accelerated in HW. // // This specialization can be removed when QX11PaintEngine supports // CompositionMode_Plus. QPixmap source(to), destination(from); source.detach(); destination.detach(); Display *dpy = QX11Info::display(); XRenderPictFormat *format = XRenderFindStandardFormat(dpy, PictStandardA8); XRenderPictureAttributes pa; pa.repeat = 1; // RepeatNormal // Create a 1x1 8 bit repeating alpha picture Pixmap pixmap = XCreatePixmap(dpy, destination.handle(), 1, 1, 8); Picture alpha = XRenderCreatePicture(dpy, pixmap, format, CPRepeat, &pa); XFreePixmap(dpy, pixmap); // Fill the alpha picture with the opacity value XRenderColor xcolor; xcolor.alpha = quint16(0xffff * amount); XRenderFillRectangle(dpy, PictOpSrc, alpha, &xcolor, 0, 0, 1, 1); // Reduce the alpha of the destination with 1 - opacity XRenderComposite(dpy, PictOpOutReverse, alpha, None, destination.x11PictureHandle(), 0, 0, 0, 0, 0, 0, destination.width(), destination.height()); // Add source * opacity to the destination XRenderComposite(dpy, PictOpAdd, source.x11PictureHandle(), alpha, destination.x11PictureHandle(), 0, 0, 0, 0, 0, 0, destination.width(), destination.height()); XRenderFreePicture(dpy, alpha); return destination; } #endif else { // Fall back to using QRasterPaintEngine to do the transition. QImage under = from.toImage(); QImage over = to.toImage(); QPainter p; p.begin(&over); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); p.fillRect(over.rect(), color); p.end(); p.begin(&under); p.setCompositionMode(QPainter::CompositionMode_DestinationOut); p.fillRect(under.rect(), color); p.setCompositionMode(QPainter::CompositionMode_Plus); p.drawImage(0, 0, over); p.end(); return QPixmap::fromImage(under); } } void KFileItemDelegate::Private::layoutTextItems(const QStyleOptionViewItem &option, const QModelIndex &index, QTextLayout *labelLayout, QTextLayout *infoLayout, QRect *textBoundingRect) const { KFileItem item = fileItem(index); const QString info = information(option, index, item); bool showInformation = false; setLayoutOptions(*labelLayout, option); const QRect textArea = labelRectangle(option); QRect textRect = subtractMargin(textArea, Private::TextMargin); // Sizes and constraints for the different text parts QSize maxLabelSize = textRect.size(); QSize maxInfoSize = textRect.size(); QSize labelSize; QSize infoSize; // If we have additional info text, and there's space for at least two lines of text, // adjust the max label size to make room for at least one line of the info text if (!info.isEmpty() && textRect.height() >= option.fontMetrics.lineSpacing() * 2) { infoLayout->setFont(labelLayout->font()); infoLayout->setTextOption(labelLayout->textOption()); maxLabelSize.rheight() -= option.fontMetrics.lineSpacing(); showInformation = true; } // Lay out the label text, and adjust the max info size based on the label size labelSize = layoutText(*labelLayout, option, option.text, maxLabelSize); maxInfoSize.rheight() -= labelSize.height(); // Lay out the info text if (showInformation) { infoSize = layoutText(*infoLayout, option, info, maxInfoSize); } else { infoSize = QSize(0, 0); } // Compute the bounding rect of the text const QSize size(qMax(labelSize.width(), infoSize.width()), labelSize.height() + infoSize.height()); *textBoundingRect = QStyle::alignedRect(option.direction, option.displayAlignment, size, textRect); // Compute the positions where we should draw the layouts labelLayout->setPosition(QPointF(textRect.x(), textBoundingRect->y())); infoLayout->setPosition(QPointF(textRect.x(), textBoundingRect->y() + labelSize.height())); } void KFileItemDelegate::Private::drawTextItems(QPainter *painter, const QTextLayout &labelLayout, const QTextLayout &infoLayout, const QRect &boundingRect) const { if (shadowColor.alpha() > 0) { QPixmap pixmap(boundingRect.size()); pixmap.fill(Qt::transparent); QPainter p(&pixmap); p.translate(-boundingRect.topLeft()); p.setPen(painter->pen()); labelLayout.draw(&p, QPoint()); if (!infoLayout.text().isEmpty()) { QColor color = p.pen().color(); color.setAlphaF(0.6); p.setPen(color); infoLayout.draw(&p, QPoint()); } p.end(); int padding = qCeil(shadowBlur); int blurFactor = qRound(shadowBlur); QImage image(boundingRect.size() + QSize(padding * 2, padding * 2), QImage::Format_ARGB32_Premultiplied); image.fill(0); p.begin(&image); p.drawImage(padding, padding, pixmap.toImage()); p.end(); KIO::ImageFilter::shadowBlur(image, blurFactor, shadowColor); painter->drawImage(boundingRect.topLeft() - QPoint(padding, padding) + shadowOffset.toPoint(), image); painter->drawPixmap(boundingRect.topLeft(), pixmap); return; } labelLayout.draw(painter, QPoint()); if (!infoLayout.text().isEmpty()) { // TODO - for apps not doing funny things with the color palette, // KColorScheme::InactiveText would be a much more correct choice. We // should provide an API to specify what color to use for information. QColor color = painter->pen().color(); color.setAlphaF(0.6); painter->setPen(color); infoLayout.draw(painter, QPoint()); } } void KFileItemDelegate::Private::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const { const KFileItem item = fileItem(index); bool updateFontMetrics = false; // Try to get the font from the model QVariant value = index.data(Qt::FontRole); if (value.isValid()) { option->font = qvariant_cast(value).resolve(option->font); updateFontMetrics = true; } // Use an italic font for symlinks if (!item.isNull() && item.isLink()) { option->font.setItalic(true); updateFontMetrics = true; } if (updateFontMetrics) { option->fontMetrics = QFontMetrics(option->font); } // Try to get the alignment for the item from the model value = index.data(Qt::TextAlignmentRole); if (value.isValid()) { option->displayAlignment = Qt::Alignment(value.toInt()); } value = index.data(Qt::BackgroundRole); if (value.isValid()) { option->backgroundBrush = brush(value, *option); } option->text = display(index); if (!option->text.isEmpty()) { option->features |= QStyleOptionViewItem::HasDisplay; } option->icon = decoration(*option, index); // Note that even null icons are still drawn for alignment if (!option->icon.isNull()) { option->features |= QStyleOptionViewItem::HasDecoration; } // ### Make sure this value is always true for now option->showDecorationSelected = true; } void KFileItemDelegate::Private::paintJobTransfers(QPainter *painter, const qreal &jobAnimationAngle, const QPoint &iconPos, const QStyleOptionViewItem &opt) { painter->save(); QSize iconSize = opt.icon.actualSize(opt.decorationSize); QPixmap downArrow = downArrowIcon.pixmap(iconSize * 0.30); //corner (less x and y than bottom-right corner) that we will center the painter around QPoint bottomRightCorner = QPoint(iconPos.x() + iconSize.width() * 0.75, iconPos.y() + iconSize.height() * 0.60); QPainter pixmapPainter(&downArrow); //make the icon transparent and such pixmapPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); pixmapPainter.fillRect(downArrow.rect(), QColor(255, 255, 255, 110)); painter->translate(bottomRightCorner); painter->drawPixmap(-downArrow.size().width() * .50, -downArrow.size().height() * .50, downArrow); //animate the circles by rotating the painter around the center point.. painter->rotate(jobAnimationAngle); painter->setPen(QColor(20, 20, 20, 80)); painter->setBrush(QColor(250, 250, 250, 90)); int radius = iconSize.width() * 0.04; int spacing = radius * 4.5; //left painter->drawEllipse(QPoint(-spacing, 0), radius, radius); //right painter->drawEllipse(QPoint(spacing, 0), radius, radius); //up painter->drawEllipse(QPoint(0, -spacing), radius, radius); //down painter->drawEllipse(QPoint(0, spacing), radius, radius); painter->restore(); } // --------------------------------------------------------------------------- KFileItemDelegate::KFileItemDelegate(QObject *parent) : QAbstractItemDelegate(parent), d(new Private(this)) { int focusHMargin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin); int focusVMargin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin); // Margins for horizontal mode (list views, tree views, table views) const int textMargin = focusHMargin * 4; if (QApplication::isRightToLeft()) { d->setHorizontalMargin(Private::TextMargin, textMargin, focusVMargin, focusHMargin, focusVMargin); } else { d->setHorizontalMargin(Private::TextMargin, focusHMargin, focusVMargin, textMargin, focusVMargin); } d->setHorizontalMargin(Private::IconMargin, focusHMargin, focusVMargin); d->setHorizontalMargin(Private::ItemMargin, 0, 0); // Margins for vertical mode (icon views) d->setVerticalMargin(Private::TextMargin, 6, 2); d->setVerticalMargin(Private::IconMargin, focusHMargin, focusVMargin); d->setVerticalMargin(Private::ItemMargin, 0, 0); setShowInformation(NoInformation); } KFileItemDelegate::~KFileItemDelegate() { delete d; } QSize KFileItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { // If the model wants to provide its own size hint for the item const QVariant value = index.data(Qt::SizeHintRole); if (value.isValid()) { return qvariant_cast(value); } QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); d->setActiveMargins(d->verticalLayout(opt) ? Qt::Vertical : Qt::Horizontal); const QSize displaySize = d->displaySizeHint(opt, index); const QSize decorationSize = d->decorationSizeHint(opt, index); QSize size; if (d->verticalLayout(opt)) { size.rwidth() = qMax(displaySize.width(), decorationSize.width()); size.rheight() = decorationSize.height() + displaySize.height() + 1; } else { size.rwidth() = decorationSize.width() + displaySize.width() + 1; size.rheight() = qMax(decorationSize.height(), displaySize.height()); } size = d->addMargin(size, Private::ItemMargin); if (!d->maximumSize.isEmpty()) { size = size.boundedTo(d->maximumSize); } return size; } QString KFileItemDelegate::Private::display(const QModelIndex &index) const { const QVariant value = index.data(Qt::DisplayRole); switch (value.type()) { case QVariant::String: { if (index.column() == KDirModel::Size) { return itemSize(index, fileItem(index)); } else { const QString text = replaceNewlines(value.toString()); return KStringHandler::preProcessWrap(text); } } case QVariant::Double: return QLocale().toString(value.toDouble(), 'f'); case QVariant::Int: case QVariant::UInt: return QLocale().toString(value.toInt()); default: return QString(); } } void KFileItemDelegate::setShowInformation(const InformationList &list) { d->informationList = list; } void KFileItemDelegate::setShowInformation(Information value) { if (value != NoInformation) { d->informationList = InformationList() << value; } else { d->informationList = InformationList(); } } KFileItemDelegate::InformationList KFileItemDelegate::showInformation() const { return d->informationList; } void KFileItemDelegate::setShadowColor(const QColor &color) { d->shadowColor = color; } QColor KFileItemDelegate::shadowColor() const { return d->shadowColor; } void KFileItemDelegate::setShadowOffset(const QPointF &offset) { d->shadowOffset = offset; } QPointF KFileItemDelegate::shadowOffset() const { return d->shadowOffset; } void KFileItemDelegate::setShadowBlur(qreal factor) { d->shadowBlur = factor; } qreal KFileItemDelegate::shadowBlur() const { return d->shadowBlur; } void KFileItemDelegate::setMaximumSize(const QSize &size) { d->maximumSize = size; } QSize KFileItemDelegate::maximumSize() const { return d->maximumSize; } void KFileItemDelegate::setShowToolTipWhenElided(bool showToolTip) { d->showToolTipWhenElided = showToolTip; } bool KFileItemDelegate::showToolTipWhenElided() const { return d->showToolTipWhenElided; } void KFileItemDelegate::setWrapMode(QTextOption::WrapMode wrapMode) { d->wrapMode = wrapMode; } QTextOption::WrapMode KFileItemDelegate::wrapMode() const { return d->wrapMode; } QRect KFileItemDelegate::iconRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); return QRect(d->iconPosition(opt), opt.icon.actualSize(opt.decorationSize)); } void KFileItemDelegate::setJobTransfersVisible(bool jobTransfersVisible) { d->downArrowIcon = QIcon::fromTheme(QStringLiteral("go-down")); d->jobTransfersVisible = jobTransfersVisible; } bool KFileItemDelegate::jobTransfersVisible() const { return d->jobTransfersVisible; } QIcon KFileItemDelegate::Private::decoration(const QStyleOptionViewItem &option, const QModelIndex &index) const { const QVariant value = index.data(Qt::DecorationRole); QIcon icon; switch (value.type()) { case QVariant::Icon: icon = qvariant_cast(value); break; case QVariant::Pixmap: icon.addPixmap(qvariant_cast(value)); break; case QVariant::Color: { QPixmap pixmap(option.decorationSize); pixmap.fill(qvariant_cast(value)); icon.addPixmap(pixmap); break; } default: break; } return icon; } QRect KFileItemDelegate::Private::labelRectangle(const QStyleOptionViewItem &option) const { const QSize decoSize = addMargin(option.decorationSize, Private::IconMargin); const QRect itemRect = subtractMargin(option.rect, Private::ItemMargin); QRect textArea(QPoint(0, 0), itemRect.size()); switch (option.decorationPosition) { case QStyleOptionViewItem::Top: textArea.setTop(decoSize.height() + 1); break; case QStyleOptionViewItem::Bottom: textArea.setBottom(itemRect.height() - decoSize.height() - 1); break; case QStyleOptionViewItem::Left: textArea.setLeft(decoSize.width() + 1); break; case QStyleOptionViewItem::Right: textArea.setRight(itemRect.width() - decoSize.width() - 1); break; } textArea.translate(itemRect.topLeft()); return QStyle::visualRect(option.direction, option.rect, textArea); } QPoint KFileItemDelegate::Private::iconPosition(const QStyleOptionViewItem &option) const { const QRect itemRect = subtractMargin(option.rect, Private::ItemMargin); Qt::Alignment alignment; // Convert decorationPosition to the alignment the decoration will have in option.rect switch (option.decorationPosition) { case QStyleOptionViewItem::Top: alignment = Qt::AlignHCenter | Qt::AlignTop; break; case QStyleOptionViewItem::Bottom: alignment = Qt::AlignHCenter | Qt::AlignBottom; break; case QStyleOptionViewItem::Left: alignment = Qt::AlignVCenter | Qt::AlignLeft; break; case QStyleOptionViewItem::Right: alignment = Qt::AlignVCenter | Qt::AlignRight; break; } // Compute the nominal decoration rectangle const QSize size = addMargin(option.decorationSize, Private::IconMargin); const QRect rect = QStyle::alignedRect(option.direction, alignment, size, itemRect); // Position the icon in the center of the rectangle QRect iconRect = QRect(QPoint(), option.icon.actualSize(option.decorationSize)); iconRect.moveCenter(rect.center()); return iconRect.topLeft(); } void KFileItemDelegate::Private::drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const { if (!(option.state & QStyle::State_HasFocus)) { return; } QStyleOptionFocusRect opt; opt.direction = option.direction; opt.fontMetrics = option.fontMetrics; opt.palette = option.palette; opt.rect = rect; opt.state = option.state | QStyle::State_KeyboardFocusChange | QStyle::State_Item; opt.backgroundColor = option.palette.color(option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base); // Apparently some widget styles expect this hint to not be set painter->setRenderHint(QPainter::Antialiasing, false); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget); painter->setRenderHint(QPainter::Antialiasing); } void KFileItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (!index.isValid()) { return; } QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); d->setActiveMargins(d->verticalLayout(opt) ? Qt::Vertical : Qt::Horizontal); if (!(option.state & QStyle::State_Enabled)) { opt.palette.setCurrentColorGroup(QPalette::Disabled); } // Unset the mouse over bit if we're not drawing the first column if (index.column() > 0) { opt.state &= ~QStyle::State_MouseOver; } else { opt.viewItemPosition = QStyleOptionViewItem::OnlyOne; } const QAbstractItemView *view = qobject_cast(opt.widget); // Check if the item is being animated // ======================================================================== KIO::AnimationState *state = d->animationState(opt, index, view); KIO::CachedRendering *cache = nullptr; qreal progress = ((opt.state & QStyle::State_MouseOver) && index.column() == KDirModel::Name) ? 1.0 : 0.0; const QPoint iconPos = d->iconPosition(opt); QIcon::Mode iconMode; if (!(option.state & QStyle::State_Enabled)) { iconMode = QIcon::Disabled; } else if ((option.state & QStyle::State_Selected) && (option.state & QStyle::State_Active)) { iconMode = QIcon::Selected; } else { iconMode = QIcon::Normal; } QIcon::State iconState = option.state & QStyle::State_Open ? QIcon::On : QIcon::Off; QPixmap icon = opt.icon.pixmap(opt.decorationSize, iconMode, iconState); if (state && !state->hasJobAnimation()) { cache = state->cachedRendering(); progress = state->hoverProgress(); // Clear the mouse over bit temporarily opt.state &= ~QStyle::State_MouseOver; // If we have a cached rendering, draw the item from the cache if (cache) { if (cache->checkValidity(opt.state) && cache->regular.size() == opt.rect.size()) { QPixmap pixmap = d->transition(cache->regular, cache->hover, progress); if (state->cachedRenderingFadeFrom() && state->fadeProgress() != 1.0) { // Apply icon fading animation KIO::CachedRendering *fadeFromCache = state->cachedRenderingFadeFrom(); const QPixmap fadeFromPixmap = d->transition(fadeFromCache->regular, fadeFromCache->hover, progress); pixmap = d->transition(fadeFromPixmap, pixmap, state->fadeProgress()); } painter->drawPixmap(option.rect.topLeft(), pixmap); if (d->jobTransfersVisible && index.column() == 0) { if (index.data(KDirModel::HasJobRole).toBool()) { d->paintJobTransfers(painter, state->jobAnimationAngle(), iconPos, opt); } } return; } if (!cache->checkValidity(opt.state)) { if (opt.widget->style()->styleHint(QStyle::SH_Widget_Animate, nullptr, opt.widget)) { // Fade over from the old icon to the new one // Only start a new fade if the previous one is ready // Else we may start racing when checkValidity() always returns false if (state->fadeProgress() == 1) { state->setCachedRenderingFadeFrom(state->takeCachedRendering()); } } d->gotNewIcon(index); } // If it wasn't valid, delete it state->setCachedRendering(nullptr); } else { // The cache may have been discarded, but the animation handler still needs to know about new icons d->gotNewIcon(index); } } // Compute the metrics, and lay out the text items // ======================================================================== const QPen pen = QPen(d->foregroundBrush(opt, index), 0); //### Apply the selection effect to the icon when the item is selected and // showDecorationSelected is false. QTextLayout labelLayout, infoLayout; QRect textBoundingRect; d->layoutTextItems(opt, index, &labelLayout, &infoLayout, &textBoundingRect); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); int focusHMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); int focusVMargin = style->pixelMetric(QStyle::PM_FocusFrameVMargin); QRect focusRect = textBoundingRect.adjusted(-focusHMargin, -focusVMargin, +focusHMargin, +focusVMargin); // Create a new cached rendering of a hovered and an unhovered item. // We don't create a new cache for a fully hovered item, since we don't // know yet if a hover out animation will be run. // ======================================================================== if (state && (state->hoverProgress() < 1 || state->fadeProgress() < 1)) { const qreal dpr = painter->device()->devicePixelRatioF(); cache = new KIO::CachedRendering(opt.state, option.rect.size(), index, dpr); QPainter p; p.begin(&cache->regular); p.translate(-option.rect.topLeft()); p.setRenderHint(QPainter::Antialiasing); p.setPen(pen); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, &p, opt.widget); p.drawPixmap(iconPos, icon); d->drawTextItems(&p, labelLayout, infoLayout, textBoundingRect); d->drawFocusRect(&p, opt, focusRect); p.end(); opt.state |= QStyle::State_MouseOver; icon = d->applyHoverEffect(icon); p.begin(&cache->hover); p.translate(-option.rect.topLeft()); p.setRenderHint(QPainter::Antialiasing); p.setPen(pen); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, &p, opt.widget); p.drawPixmap(iconPos, icon); d->drawTextItems(&p, labelLayout, infoLayout, textBoundingRect); d->drawFocusRect(&p, opt, focusRect); p.end(); state->setCachedRendering(cache); QPixmap pixmap = d->transition(cache->regular, cache->hover, progress); if (state->cachedRenderingFadeFrom() && state->fadeProgress() == 0) { // Apply icon fading animation KIO::CachedRendering *fadeFromCache = state->cachedRenderingFadeFrom(); const QPixmap fadeFromPixmap = d->transition(fadeFromCache->regular, fadeFromCache->hover, progress); pixmap = d->transition(fadeFromPixmap, pixmap, state->fadeProgress()); d->restartAnimation(state); } painter->drawPixmap(option.rect.topLeft(), pixmap); painter->setRenderHint(QPainter::Antialiasing); if (d->jobTransfersVisible && index.column() == 0) { if (index.data(KDirModel::HasJobRole).toBool()) { d->paintJobTransfers(painter, state->jobAnimationAngle(), iconPos, opt); } } return; } // Render the item directly if we're not using a cached rendering // ======================================================================== painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(pen); if (progress > 0 && !(opt.state & QStyle::State_MouseOver)) { opt.state |= QStyle::State_MouseOver; icon = d->applyHoverEffect(icon); } style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); painter->drawPixmap(iconPos, icon); d->drawTextItems(painter, labelLayout, infoLayout, textBoundingRect); d->drawFocusRect(painter, opt, focusRect); if (d->jobTransfersVisible && index.column() == 0 && state) { if (index.data(KDirModel::HasJobRole).toBool()) { d->paintJobTransfers(painter, state->jobAnimationAngle(), iconPos, opt); } } painter->restore(); } QWidget *KFileItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); QTextEdit *edit = new QTextEdit(parent); edit->setAcceptRichText(false); edit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); edit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); edit->setAlignment(opt.displayAlignment); edit->setEnabled(false); //Disable the text-edit to mark it as un-initialized return edit; } bool KFileItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { Q_UNUSED(event) Q_UNUSED(model) Q_UNUSED(option) Q_UNUSED(index) return false; } void KFileItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QTextEdit *textedit = qobject_cast(editor); Q_ASSERT(textedit != nullptr); //Do not update existing text that the user may already have edited. //The models will call setEditorData(..) whenever the icon has changed, //and this makes the editing work correctly despite that. if (textedit->isEnabled()) { return; } textedit->setEnabled(true); //Enable the text-edit to mark it as initialized const QVariant value = index.data(Qt::EditRole); const QString text = value.toString(); textedit->insertPlainText(text); textedit->selectAll(); QMimeDatabase db; const QString extension = db.suffixForFileName(text); if (!extension.isEmpty()) { // The filename contains an extension. Assure that only the filename // gets selected. const int selectionLength = text.length() - extension.length() - 1; QTextCursor cursor = textedit->textCursor(); cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectionLength); textedit->setTextCursor(cursor); } } void KFileItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QTextEdit *textedit = qobject_cast(editor); Q_ASSERT(textedit != nullptr); model->setData(index, textedit->toPlainText(), Qt::EditRole); } void KFileItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); d->setActiveMargins(d->verticalLayout(opt) ? Qt::Vertical : Qt::Horizontal); QRect r = d->labelRectangle(opt); // Use the full available width for the editor when maximumSize is set if (!d->maximumSize.isEmpty()) { if (d->verticalLayout(option)) { int diff = qMax(r.width(), d->maximumSize.width()) - r.width(); if (diff > 1) { r.adjust(-(diff / 2), 0, diff / 2, 0); } } else { int diff = qMax(r.width(), d->maximumSize.width() - opt.decorationSize.width()) - r.width(); if (diff > 0) { if (opt.decorationPosition == QStyleOptionViewItem::Left) { r.adjust(0, 0, diff, 0); } else { r.adjust(-diff, 0, 0, 0); } } } } QTextEdit *textedit = qobject_cast(editor); Q_ASSERT(textedit != nullptr); const int frame = textedit->frameWidth(); r.adjust(-frame, -frame, frame, frame); editor->setGeometry(r); } bool KFileItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { Q_UNUSED(event) Q_UNUSED(view) // if the tooltip information the model keeps is different from the display information, // show it always const QVariant toolTip = index.data(Qt::ToolTipRole); if (!toolTip.isValid()) { return false; } if (index.data() != toolTip) { return QAbstractItemDelegate::helpEvent(event, view, option, index); } if (!d->showToolTipWhenElided) { return false; } // in the case the tooltip information is the same as the display information, // show it only in the case the display information is elided QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); d->setActiveMargins(d->verticalLayout(opt) ? Qt::Vertical : Qt::Horizontal); QTextLayout labelLayout; QTextLayout infoLayout; QRect textBoundingRect; d->layoutTextItems(opt, index, &labelLayout, &infoLayout, &textBoundingRect); const QString elidedText = d->elidedText(labelLayout, opt, textBoundingRect.size()); if (elidedText != d->display(index)) { return QAbstractItemDelegate::helpEvent(event, view, option, index); } return false; } QRegion KFileItemDelegate::shape(const QStyleOptionViewItem &option, const QModelIndex &index) { QStyleOptionViewItem opt(option); d->initStyleOption(&opt, index); d->setActiveMargins(d->verticalLayout(opt) ? Qt::Vertical : Qt::Horizontal); QTextLayout labelLayout; QTextLayout infoLayout; QRect textBoundingRect; d->layoutTextItems(opt, index, &labelLayout, &infoLayout, &textBoundingRect); const QPoint pos = d->iconPosition(opt); QRect iconRect = QRect(pos, opt.icon.actualSize(opt.decorationSize)); // Extend the icon rect so it touches the text rect switch (opt.decorationPosition) { case QStyleOptionViewItem::Top: if (iconRect.width() < textBoundingRect.width()) { iconRect.setBottom(textBoundingRect.top()); } else { textBoundingRect.setTop(iconRect.bottom()); } break; case QStyleOptionViewItem::Bottom: if (iconRect.width() < textBoundingRect.width()) { iconRect.setTop(textBoundingRect.bottom()); } else { textBoundingRect.setBottom(iconRect.top()); } break; case QStyleOptionViewItem::Left: iconRect.setRight(textBoundingRect.left()); break; case QStyleOptionViewItem::Right: iconRect.setLeft(textBoundingRect.right()); break; } QRegion region; region += iconRect; region += textBoundingRect; return region; } bool KFileItemDelegate::eventFilter(QObject *object, QEvent *event) { QTextEdit *editor = qobject_cast(object); if (!editor) { return false; } switch (event->type()) { case QEvent::KeyPress: { QKeyEvent *keyEvent = static_cast(event); switch (keyEvent->key()) { case Qt::Key_Tab: case Qt::Key_Backtab: emit commitData(editor); emit closeEditor(editor, NoHint); return true; case Qt::Key_Enter: case Qt::Key_Return: { const QString text = editor->toPlainText(); if (text.isEmpty() || (text == QLatin1Char('.')) || (text == QLatin1String(".."))) { return true; // So a newline doesn't get inserted } emit commitData(editor); emit closeEditor(editor, SubmitModelCache); return true; } case Qt::Key_Escape: emit closeEditor(editor, RevertModelCache); return true; default: return false; } // switch (keyEvent->key()) } // case QEvent::KeyPress case QEvent::FocusOut: { const QWidget *w = QApplication::activePopupWidget(); if (!w || w->parent() != editor) { emit commitData(editor); emit closeEditor(editor, NoHint); return true; } else { return false; } } default: return false; } // switch (event->type()) } diff --git a/src/widgets/kopenwithdialog.cpp b/src/widgets/kopenwithdialog.cpp index d77a62b7..35dd4fbe 100644 --- a/src/widgets/kopenwithdialog.cpp +++ b/src/widgets/kopenwithdialog.cpp @@ -1,1163 +1,1161 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Torben Weis Copyright (C) 1999 Dirk Mueller Portions copyright (C) 1999 Preston Brown Copyright (C) 2007 Pino Toscano 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 "kopenwithdialog.h" #include "kopenwithdialog_p.h" #include "kio_widgets_debug.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 inline void writeEntry(KConfigGroup &group, const char *key, const KCompletion::CompletionMode &aValue, KConfigBase::WriteConfigFlags flags = KConfigBase::Normal) { group.writeEntry(key, int(aValue), flags); } namespace KDEPrivate { class AppNode { public: AppNode() : isDir(false), parent(nullptr), fetched(false) { } ~AppNode() { qDeleteAll(children); } AppNode(const AppNode &) = delete; AppNode &operator=(const AppNode &) = delete; QString icon; QString text; QString entryPath; QString exec; bool isDir; AppNode *parent; bool fetched; QList children; }; static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2) { if (n1->isDir) { if (n2->isDir) { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } else { return true; } } else { if (n2->isDir) { return false; } else { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } } } } class KApplicationModelPrivate { public: explicit KApplicationModelPrivate(KApplicationModel *qq) : q(qq), root(new KDEPrivate::AppNode()) { } ~KApplicationModelPrivate() { delete root; } void fillNode(const QString &entryPath, KDEPrivate::AppNode *node); KApplicationModel * const q; KDEPrivate::AppNode *root; }; void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node) { KServiceGroup::Ptr root = KServiceGroup::group(_entryPath); if (!root || !root->isValid()) { return; } const KServiceGroup::List list = root->entries(); - for (KServiceGroup::List::ConstIterator it = list.begin(); - it != list.end(); ++it) { + for (const KSycocaEntry::Ptr &p : list) { QString icon; QString text; QString entryPath; QString exec; bool isDir = false; - const KSycocaEntry::Ptr p = (*it); if (p->isType(KST_KService)) { const KService::Ptr service(static_cast(p.data())); if (service->noDisplay()) { continue; } icon = service->icon(); text = service->name(); exec = service->exec(); entryPath = service->entryPath(); } else if (p->isType(KST_KServiceGroup)) { const KServiceGroup::Ptr serviceGroup(static_cast(p.data())); if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) { continue; } icon = serviceGroup->icon(); text = serviceGroup->caption(); entryPath = serviceGroup->entryPath(); isDir = true; } else { qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!"; continue; } KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode(); newnode->icon = icon; newnode->text = text; newnode->entryPath = entryPath; newnode->exec = exec; newnode->isDir = isDir; newnode->parent = node; node->children.append(newnode); } std::stable_sort(node->children.begin(), node->children.end(), KDEPrivate::AppNodeLessThan); } KApplicationModel::KApplicationModel(QObject *parent) : QAbstractItemModel(parent), d(new KApplicationModelPrivate(this)) { d->fillNode(QString(), d->root); const int nRows = rowCount(); for (int i = 0; i < nRows; i++) { fetchAll(index(i, 0)); } } KApplicationModel::~KApplicationModel() { delete d; } bool KApplicationModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir && !node->fetched; } int KApplicationModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QVariant KApplicationModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return node->text; case Qt::DecorationRole: if (!node->icon.isEmpty()) { return QIcon::fromTheme(node->icon); } break; default: ; } return QVariant(); } void KApplicationModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); if (!node->isDir) { return; } emit layoutAboutToBeChanged(); d->fillNode(node->entryPath, node); node->fetched = true; emit layoutChanged(); } void KApplicationModel::fetchAll(const QModelIndex &parent) { if (!parent.isValid() || !canFetchMore(parent)) { return; } fetchMore(parent); int childCount = rowCount(parent); for (int i = 0; i < childCount; i++) { const QModelIndex &child = index(i, 0, parent); // Recursively call the function for each child node. fetchAll(child); } } bool KApplicationModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir; } QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section != 0) { return QVariant(); } switch (role) { case Qt::DisplayRole: return i18n("Known Applications"); default: return QVariant(); } } QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } KDEPrivate::AppNode *node = d->root; if (parent.isValid()) { node = static_cast(parent.internalPointer()); } if (row >= node->children.count()) { return QModelIndex(); } else { return createIndex(row, 0, node->children.at(row)); } } QModelIndex KApplicationModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); if (node->parent->parent) { int id = node->parent->parent->children.indexOf(node->parent); if (id >= 0 && id < node->parent->parent->children.count()) { return createIndex(id, 0, node->parent); } else { return QModelIndex(); } } else { return QModelIndex(); } } int KApplicationModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->root->children.count(); } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->children.count(); } QString KApplicationModel::entryPathFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->entryPath; } QString KApplicationModel::execFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->exec; } bool KApplicationModel::isDirectory(const QModelIndex &index) const { if (!index.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->isDir; } QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent) : QSortFilterProxyModel(parent) { } bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, parent); if (!index.isValid()) { return false; } // Match the regexp only on leaf nodes if (!sourceModel()->hasChildren(index) && index.data().toString().contains(filterRegExp())) { return true; } return false; } class KApplicationViewPrivate { public: KApplicationViewPrivate() : appModel(nullptr), m_proxyModel(nullptr) { } KApplicationModel *appModel; QSortFilterProxyModel *m_proxyModel; }; KApplicationView::KApplicationView(QWidget *parent) : QTreeView(parent), d(new KApplicationViewPrivate) { setHeaderHidden(true); } KApplicationView::~KApplicationView() { delete d; } void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel) { if (d->appModel) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } QTreeView::setModel(proxyModel); // Here we set the proxy model d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later d->appModel = model; if (d->appModel) { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } } QSortFilterProxyModel* KApplicationView::proxyModel() { return d->m_proxyModel; } bool KApplicationView::isDirSel() const { if (d->appModel) { QModelIndex index = selectionModel()->currentIndex(); index = d->m_proxyModel->mapToSource(index); return d->appModel->isDirectory(index); } return false; } void KApplicationView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTreeView::currentChanged(current, previous); if (d->appModel) { QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(current); if(!d->appModel->isDirectory(sourceCurrent)) { QString exec = d->appModel->execFor(sourceCurrent); if (!exec.isEmpty()) { emit highlighted(d->appModel->entryPathFor(sourceCurrent), exec); } } } } void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected) QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(selected); const QModelIndexList indexes = sourceSelected.indexes(); if (indexes.count() == 1) { QString exec = d->appModel->execFor(indexes.at(0)); emit this->selected(d->appModel->entryPathFor(indexes.at(0)), exec); } } /*************************************************************** * * KOpenWithDialog * ***************************************************************/ class KOpenWithDialogPrivate { public: explicit KOpenWithDialogPrivate(KOpenWithDialog *qq) : q(qq), saveNewApps(false) { } KOpenWithDialog * const q; /** * Determine mime type from URLs */ void setMimeType(const QList &_urls); void addToMimeAppsList(const QString &serviceId); /** * Create a dialog that asks for a application to open a given * URL(s) with. * * @param text appears as a label on top of the entry box. * @param value is the initial value of the line */ void init(const QString &text, const QString &value); /** * Called by checkAccept() in order to save the history of the combobox */ void saveComboboxHistory(); /** * Process the choices made by the user, and return true if everything is OK. * Called by KOpenWithDialog::accept(), i.e. when clicking on OK or typing Return. */ bool checkAccept(); // slots void _k_slotDbClick(); void _k_slotFileSelected(); bool saveNewApps; bool m_terminaldirty; KService::Ptr curService; KApplicationView *view; KUrlRequester *edit; QString m_command; QLabel *label; QString qMimeType; QString qMimeTypeComment; QCheckBox *terminal; QCheckBox *remember; QCheckBox *nocloseonexit; KService::Ptr m_pService; QDialogButtonBox *buttonBox; }; KOpenWithDialog::KOpenWithDialog(const QList &_urls, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Open With")); QString text; if (_urls.count() == 1) { text = i18n("Select the program that should be used to open %1. " "If the program is not listed, enter the name or click " "the browse button.", _urls.first().fileName().toHtmlEscaped()); } else // Should never happen ?? { text = i18n("Choose the name of the program with which to open the selected files."); } d->setMimeType(_urls); d->init(text, QString()); } KOpenWithDialog::KOpenWithDialog(const QList &_urls, const QString &_text, const QString &_value, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); QString text = _text; if (text.isEmpty() && !_urls.isEmpty()) { if (_urls.count() == 1) { const QString fileName = KStringHandler::csqueeze(_urls.first().fileName()); text = i18n("Select the program you want to use to open the file
%1
", fileName.toHtmlEscaped()); } else { text = i18np("Select the program you want to use to open the file.", "Select the program you want to use to open the %1 files.", _urls.count()); } } setWindowTitle(i18n("Choose Application")); d->setMimeType(_urls); d->init(text, _value); } KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application for %1", mimeType)); QString text = i18n("Select the program for the file type: %1. " "If the program is not listed, enter the name or click " "the browse button.", mimeType); d->qMimeType = mimeType; QMimeDatabase db; d->qMimeTypeComment = db.mimeTypeForName(mimeType).comment(); d->init(text, value); if (d->remember) { d->remember->hide(); } } KOpenWithDialog::KOpenWithDialog(QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application")); QString text = i18n("Select a program. " "If the program is not listed, enter the name or click " "the browse button."); d->qMimeType.clear(); d->init(text, QString()); } void KOpenWithDialogPrivate::setMimeType(const QList &_urls) { if (_urls.count() == 1) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(_urls.first()); qMimeType = mime.name(); if (mime.isDefault()) { qMimeType.clear(); } else { qMimeTypeComment = mime.comment(); } } else { qMimeType.clear(); } } void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value) { bool bReadOnly = !KAuthorized::authorize(QStringLiteral("shell_access")); m_terminaldirty = false; view = nullptr; m_pService = nullptr; curService = nullptr; QBoxLayout *topLayout = new QVBoxLayout; q->setLayout(topLayout); label = new QLabel(_text, q); label->setWordWrap(true); topLayout->addWidget(label); if (!bReadOnly) { // init the history combo and insert it into the URL-Requester KHistoryComboBox *combo = new KHistoryComboBox(); combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results.")); KLineEdit *lineEdit = new KLineEdit(q); lineEdit->setClearButtonEnabled(true); combo->setLineEdit(lineEdit); combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); combo->setDuplicatesEnabled(false); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); int max = cg.readEntry("Maximum history", 15); combo->setMaxCount(max); int mode = cg.readEntry("CompletionMode", int(KCompletion::CompletionNone)); combo->setCompletionMode(static_cast(mode)); const QStringList list = cg.readEntry("History", QStringList()); combo->setHistoryItems(list, true); edit = new KUrlRequester(combo, q); edit->installEventFilter(q); } else { edit = new KUrlRequester(q); edit->lineEdit()->setReadOnly(true); edit->button()->hide(); } edit->setText(_value); edit->setWhatsThis(i18n( "Following the command, you can have several place holders which will be replaced " "with the actual values when the actual program is run:\n" "%f - a single file name\n" "%F - a list of files; use for applications that can open several local files at once\n" "%u - a single URL\n" "%U - a list of URLs\n" "%d - the directory of the file to open\n" "%D - a list of directories\n" "%i - the icon\n" "%m - the mini-icon\n" "%c - the comment")); topLayout->addWidget(edit); if (edit->comboBox()) { KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion); edit->comboBox()->setCompletionObject(comp); edit->comboBox()->setAutoDeleteCompletionObject(true); } QObject::connect(edit, &KUrlRequester::textChanged, q, &KOpenWithDialog::slotTextChanged); QObject::connect(edit, SIGNAL(urlSelected(QUrl)), q, SLOT(_k_slotFileSelected())); QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view); KApplicationModel *appModel = new KApplicationModel(proxyModel); proxyModel->setSourceModel(appModel); proxyModel->setFilterKeyColumn(0); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel->setRecursiveFilteringEnabled(true); view = new KApplicationView(q); view->setModels(appModel, proxyModel); topLayout->addWidget(view); topLayout->setStretchFactor(view, 1); QObject::connect(view, &KApplicationView::selected, q, &KOpenWithDialog::slotSelected); QObject::connect(view, &KApplicationView::highlighted, q, &KOpenWithDialog::slotHighlighted); QObject::connect(view, SIGNAL(doubleClicked(QModelIndex)), q, SLOT(_k_slotDbClick())); if (!qMimeType.isNull()) { remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType)); // remember->setChecked(true); topLayout->addWidget(remember); } else { remember = nullptr; } //Advanced options KCollapsibleGroupBox *dialogExtension = new KCollapsibleGroupBox(q); dialogExtension->setTitle(i18n("Terminal options")); QVBoxLayout *dialogExtensionLayout = new QVBoxLayout; dialogExtensionLayout->setContentsMargins(0, 0, 0, 0); terminal = new QCheckBox(i18n("Run in &terminal"), q); if (bReadOnly) { terminal->hide(); } QObject::connect(terminal, &QAbstractButton::toggled, q, &KOpenWithDialog::slotTerminalToggled); dialogExtensionLayout->addWidget(terminal); QStyleOptionButton checkBoxOption; checkBoxOption.initFrom(terminal); int checkBoxIndentation = terminal->style()->pixelMetric(QStyle::PM_IndicatorWidth, &checkBoxOption, terminal); checkBoxIndentation += terminal->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &checkBoxOption, terminal); QBoxLayout *nocloseonexitLayout = new QHBoxLayout(); nocloseonexitLayout->setContentsMargins(0, 0, 0, 0); QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); nocloseonexitLayout->addItem(spacer); nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q); nocloseonexit->setChecked(false); nocloseonexit->setDisabled(true); // check to see if we use konsole if not disable the nocloseonexit // because we don't know how to do this on other terminal applications KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); if (bReadOnly || preferredTerminal != QLatin1String("konsole")) { nocloseonexit->hide(); } nocloseonexitLayout->addWidget(nocloseonexit); dialogExtensionLayout->addLayout(nocloseonexitLayout); dialogExtension->setLayout(dialogExtensionLayout); topLayout->addWidget(dialogExtension); buttonBox = new QDialogButtonBox(q); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); topLayout->addWidget(buttonBox); q->setMinimumSize(q->minimumSizeHint()); //edit->setText( _value ); // The resize is what caused "can't click on items before clicking on Name header" in previous versions. // Probably due to the resizeEvent handler using width(). #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) q->resize( q->minimumWidth(), 0.6 * QApplication::screens().at(0)->availableGeometry().height()); #else q->resize( q->minimumWidth(), 0.6 * q->screen()->availableGeometry().height()); #endif edit->setFocus(); q->slotTextChanged(); } // ---------------------------------------------------------------------- KOpenWithDialog::~KOpenWithDialog() { delete d; } // ---------------------------------------------------------------------- void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec) { d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty()); } // ---------------------------------------------------------------------- void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &) { d->curService = KService::serviceByDesktopPath(entryPath); if (d->curService && !d->m_terminaldirty) { // ### indicate that default value was restored d->terminal->setChecked(d->curService->terminal()); QString terminalOptions = d->curService->terminalOptions(); d->nocloseonexit->setChecked((terminalOptions.contains(QLatin1String("--noclose")))); d->m_terminaldirty = false; // slotTerminalToggled changed it } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTextChanged() { // Forget about the service only when the selection is empty // otherwise changing text but hitting the same result clears curService bool selectionEmpty = !d->view->currentIndex().isValid(); if (d->curService && selectionEmpty) { d->curService = nullptr; } d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService); //Update the filter regexp with the new text in the lineedit d->view->proxyModel()->setFilterFixedString(d->edit->text()); //Expand all the nodes when the search string is 3 characters long //If the search string doesn't match anything there will be no nodes to expand if (d->edit->text().size() > 2) { d->view->expandAll(); QAbstractItemModel *model = d->view->model(); //Automatically select the first result (first leaf node) when the filter has match QModelIndex leafNodeIdx = model->index(0, 0); while (model->hasChildren(leafNodeIdx)) { leafNodeIdx = model->index(0, 0, leafNodeIdx); } d->view->setCurrentIndex(leafNodeIdx); } else { d->view->collapseAll(); d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements d->curService = nullptr; } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTerminalToggled(bool) { // ### indicate that default value was overridden d->m_terminaldirty = true; d->nocloseonexit->setDisabled(!d->terminal->isChecked()); } // ---------------------------------------------------------------------- void KOpenWithDialogPrivate::_k_slotDbClick() { // check if a directory is selected if (view->isDirSel()) { return; } q->accept(); } void KOpenWithDialogPrivate::_k_slotFileSelected() { // quote the path to avoid unescaped whitespace, backslashes, etc. edit->setText(KShell::quoteArg(edit->text())); } void KOpenWithDialog::setSaveNewApplications(bool b) { d->saveNewApps = b; } static QString simplifiedExecLineFromService(const QString &fullExec) { QString exec = fullExec; exec.remove(QStringLiteral("%u"), Qt::CaseInsensitive); exec.remove(QStringLiteral("%f"), Qt::CaseInsensitive); exec.remove(QStringLiteral("-caption %c")); exec.remove(QStringLiteral("-caption \"%c\"")); exec.remove(QStringLiteral("%i")); exec.remove(QStringLiteral("%m")); return exec.simplified(); } void KOpenWithDialogPrivate::addToMimeAppsList(const QString &serviceId /*menu id or storage id*/) { KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); // Save the default application according to mime-apps-spec 1.0 KConfigGroup defaultApp(profile, "Default Applications"); defaultApp.writeXdgListEntry(qMimeType, QStringList(serviceId)); KConfigGroup addedApps(profile, "Added Associations"); QStringList apps = addedApps.readXdgListEntry(qMimeType); apps.removeAll(serviceId); apps.prepend(serviceId); // make it the preferred app addedApps.writeXdgListEntry(qMimeType, apps); profile->sync(); // Also make sure the "auto embed" setting for this mimetype is off KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); fileTypesConfig->group("EmbedSettings").writeEntry(QStringLiteral("embed-") + qMimeType, false); fileTypesConfig->sync(); // qDebug() << "rebuilding ksycoca..."; // kbuildsycoca is the one reading mimeapps.list, so we need to run it now KBuildSycocaProgressDialog::rebuildKSycoca(q); // could be nullptr if the user canceled the dialog... m_pService = KService::serviceByStorageId(serviceId); } bool KOpenWithDialogPrivate::checkAccept() { const QString typedExec(edit->text()); QString fullExec(typedExec); QString serviceName; QString initialServiceName; QString preferredTerminal; QString configPath; QString serviceExec; m_pService = curService; if (!m_pService) { // No service selected - check the command line // Find out the name of the service from the command line, removing args and paths serviceName = KIO::DesktopExecParser::executableName(typedExec); if (serviceName.isEmpty()) { KMessageBox::error(q, i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName)); return false; } initialServiceName = serviceName; // Also remember the executableName with a path, if any, for the // check that the executable exists. // qDebug() << "initialServiceName=" << initialServiceName; int i = 1; // We have app, app-2, app-3... Looks better for the user. bool ok = false; // Check if there's already a service by that name, with the same Exec line do { // qDebug() << "looking for service" << serviceName; KService::Ptr serv = KService::serviceByDesktopName(serviceName); ok = !serv; // ok if no such service yet // also ok if we find the exact same service (well, "kwrite" == "kwrite %U") if (serv && !serv->noDisplay() /* #297720 */) { if (serv->isApplication()) { /*// qDebug() << "typedExec=" << typedExec << "serv->exec=" << serv->exec() << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);*/ serviceExec = simplifiedExecLineFromService(serv->exec()); if (typedExec == serviceExec) { ok = true; m_pService = serv; // qDebug() << "OK, found identical service: " << serv->entryPath(); } else { // qDebug() << "Exec line differs, service says:" << serviceExec; configPath = serv->entryPath(); serviceExec = serv->exec(); } } else { // qDebug() << "Found, but not an application:" << serv->entryPath(); } } if (!ok) { // service was found, but it was different -> keep looking ++i; serviceName = initialServiceName + QLatin1Char('-') + QString::number(i); } } while (!ok); } if (m_pService) { // Existing service selected serviceName = m_pService->name(); initialServiceName = serviceName; fullExec = m_pService->exec(); } else { const QString binaryName = KIO::DesktopExecParser::executablePath(typedExec); // qDebug() << "binaryName=" << binaryName; // Ensure that the typed binary name actually exists (#81190) if (QStandardPaths::findExecutable(binaryName).isEmpty()) { KMessageBox::error(q, i18n("'%1' not found, please type a valid program name.", binaryName)); return false; } } if (terminal->isChecked()) { KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); m_command = preferredTerminal; // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_command += QStringLiteral(" --noclose"); } m_command += QLatin1String(" -e ") + edit->text(); // qDebug() << "Setting m_command to" << m_command; } if (m_pService && terminal->isChecked() != m_pService->terminal()) { m_pService = nullptr; // It's not exactly this service we're running } const bool bRemember = remember && remember->isChecked(); // qDebug() << "bRemember=" << bRemember << "service found=" << m_pService; if (m_pService) { if (bRemember) { // Associate this app with qMimeType in mimeapps.list Q_ASSERT(!qMimeType.isEmpty()); // we don't show the remember checkbox otherwise addToMimeAppsList(m_pService->storageId()); } } else { const bool createDesktopFile = bRemember || saveNewApps; if (!createDesktopFile) { // Create temp service if (configPath.isEmpty()) { m_pService = new KService(initialServiceName, fullExec, QString()); } else { if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) { int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive); if (index == -1) { index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive); } if (index > -1) { fullExec += QLatin1Char(' ') + serviceExec.midRef(index, 2); } } // qDebug() << "Creating service with Exec=" << fullExec; m_pService = new KService(configPath); m_pService->setExec(fullExec); } if (terminal->isChecked()) { m_pService->setTerminal(true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_pService->setTerminalOptions(QStringLiteral("--noclose")); } } } else { // If we got here, we can't seem to find a service for what they wanted. Create one. QString menuId; #ifdef Q_OS_WIN32 // on windows, do not use the complete path, but only the default name. serviceName = QFileInfo(serviceName).fileName(); #endif QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId); // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId; KDesktopFile desktopFile(newPath); KConfigGroup cg = desktopFile.desktopGroup(); cg.writeEntry("Type", "Application"); cg.writeEntry("Name", initialServiceName); cg.writeEntry("Exec", fullExec); cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu if (terminal->isChecked()) { cg.writeEntry("Terminal", true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { cg.writeEntry("TerminalOptions", "--noclose"); } } if (!qMimeType.isEmpty()) { cg.writeXdgListEntry("MimeType", QStringList() << qMimeType); } cg.sync(); if (!qMimeType.isEmpty()) { addToMimeAppsList(menuId); } else { m_pService = new KService(newPath); } } } saveComboboxHistory(); return true; } bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event) { // Detect DownArrow to navigate the results in the QTreeView if (object == d->edit && event->type() == QEvent::ShortcutOverride) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Down) { KHistoryComboBox *combo = static_cast(d->edit->comboBox()); // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown. // When popup completion mode is used the down arrow is used to navigate the dropdown list of results if (combo->completionMode() != KCompletion::CompletionPopup && combo->completionMode() != KCompletion::CompletionPopupAuto) { QModelIndex leafNodeIdx = d->view->model()->index(0, 0); // Check if we have at least one result or the focus is passed to the empty QTreeView if (d->view->model()->hasChildren(leafNodeIdx)) { d->view->setFocus(Qt::OtherFocusReason); QApplication::sendEvent(d->view, keyEvent); return true; } } } } return QDialog::eventFilter(object, event); } void KOpenWithDialog::accept() { if (d->checkAccept()) { QDialog::accept(); } } QString KOpenWithDialog::text() const { if (!d->m_command.isEmpty()) { return d->m_command; } else { return d->edit->text(); } } void KOpenWithDialog::hideNoCloseOnExit() { // uncheck the checkbox because the value could be used when "Run in Terminal" is selected d->nocloseonexit->setChecked(false); d->nocloseonexit->hide(); } void KOpenWithDialog::hideRunInTerminal() { d->terminal->hide(); hideNoCloseOnExit(); } KService::Ptr KOpenWithDialog::service() const { return d->m_pService; } void KOpenWithDialogPrivate::saveComboboxHistory() { KHistoryComboBox *combo = static_cast(edit->comboBox()); if (combo) { combo->addToHistory(edit->text()); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); cg.writeEntry("History", combo->historyItems()); writeEntry(cg, "CompletionMode", combo->completionMode()); // don't store the completion-list, as it contains all of KUrlCompletion's // executables cg.sync(); } } #include "moc_kopenwithdialog.cpp" #include "moc_kopenwithdialog_p.cpp" diff --git a/src/widgets/kpropertiesdialog.cpp b/src/widgets/kpropertiesdialog.cpp index d5560916..67f19e3c 100644 --- a/src/widgets/kpropertiesdialog.cpp +++ b/src/widgets/kpropertiesdialog.cpp @@ -1,3928 +1,3926 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (c) 1999, 2000 Preston Brown Copyright (c) 2000 Simon Hausmann Copyright (c) 2000 David Faure Copyright (c) 2003 Waldo Bastian 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. */ /* * kpropertiesdialog.cpp * View/Edit Properties of files, locally or remotely * * some FilePermissionsPropsPlugin-changes by * Henner Zeller * some layout management by * Bertrand Leconte * the rest of the layout management, bug fixes, adaptation to libkio, * template feature by * David Faure * More layout, cleanups, and fixes by * Preston Brown * Plugin capability, cleanups and port to KDialog by * Simon Hausmann * KDesktopPropsPlugin by * Waldo Bastian */ #include "kpropertiesdialog.h" #include "kpropertiesdialog_p.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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_POSIX_ACL extern "C" { # include # include } #endif #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 "ui_checksumswidget.h" #include "ui_kpropertiesdesktopbase.h" #include "ui_kpropertiesdesktopadvbase.h" #if HAVE_POSIX_ACL #include "kacleditwidget.h" #endif #include #include #ifdef Q_OS_WIN #include #include #include #ifdef __GNUC__ # warning TODO: port completely to win32 #endif #endif using namespace KDEPrivate; static QString nameFromFileName(QString nameStr) { if (nameStr.endsWith(QLatin1String(".desktop"))) { nameStr.chop(8); } if (nameStr.endsWith(QLatin1String(".kdelnk"))) { nameStr.chop(7); } // Make it human-readable (%2F => '/', ...) nameStr = KIO::decodeFileName(nameStr); return nameStr; } const mode_t KFilePermissionsPropsPlugin::fperm[3][4] = { {S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID}, {S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID}, {S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX} }; class Q_DECL_HIDDEN KPropertiesDialog::KPropertiesDialogPrivate { public: explicit KPropertiesDialogPrivate(KPropertiesDialog *qq) : q(qq) , m_aborted(false) , fileSharePage(nullptr) { } ~KPropertiesDialogPrivate() { } /** * Common initialization for all constructors */ void init(); /** * Inserts all pages in the dialog. */ void insertPages(); KPropertiesDialog * const q; bool m_aborted; QWidget *fileSharePage; /** * The URL of the props dialog (when shown for only one file) */ QUrl m_singleUrl; /** * List of items this props dialog is shown for */ KFileItemList m_items; /** * For templates */ QString m_defaultName; QUrl m_currentDir; /** * List of all plugins inserted ( first one first ) */ QList m_pageList; }; KPropertiesDialog::KPropertiesDialog(const KFileItem &item, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(item.name()))); Q_ASSERT(!item.isNull()); d->m_items.append(item); d->m_singleUrl = item.url(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->init(); } KPropertiesDialog::KPropertiesDialog(const QString &title, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", title)); d->init(); } KPropertiesDialog::KPropertiesDialog(const KFileItemList &_items, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { if (_items.count() > 1) { setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", _items.count())); } else { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_items.first().name()))); } Q_ASSERT(!_items.isEmpty()); d->m_singleUrl = _items.first().url(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->m_items = _items; d->init(); } KPropertiesDialog::KPropertiesDialog(const QUrl &_url, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_url.fileName()))); d->m_singleUrl = _url; KIO::StatJob *job = KIO::stat(_url); KJobWidgets::setWindow(job, parent); job->exec(); KIO::UDSEntry entry = job->statResult(); d->m_items.append(KFileItem(entry, _url)); d->init(); } KPropertiesDialog::KPropertiesDialog(const QList& urls, QWidget* parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { if (urls.count() > 1) { setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", urls.count())); } else { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(urls.first().fileName()))); } Q_ASSERT(!urls.isEmpty()); d->m_singleUrl = urls.first(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->m_items.reserve(urls.size()); for (const QUrl& url : urls) { KIO::StatJob *job = KIO::stat(url); KJobWidgets::setWindow(job, parent); job->exec(); KIO::UDSEntry entry = job->statResult(); d->m_items.append(KFileItem(entry, url)); } d->init(); } KPropertiesDialog::KPropertiesDialog(const QUrl &_tempUrl, const QUrl &_currentDir, const QString &_defaultName, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_tempUrl.fileName()))); d->m_singleUrl = _tempUrl; d->m_defaultName = _defaultName; d->m_currentDir = _currentDir; Q_ASSERT(!d->m_singleUrl.isEmpty()); // Create the KFileItem for the _template_ file, in order to read from it. d->m_items.append(KFileItem(d->m_singleUrl)); d->init(); } #ifdef Q_OS_WIN bool showWin32FilePropertyDialog(const QString &fileName) { QString path_ = QDir::toNativeSeparators(QFileInfo(fileName).absoluteFilePath()); #ifndef _WIN32_WCE SHELLEXECUTEINFOW execInfo; #else SHELLEXECUTEINFO execInfo; #endif memset(&execInfo, 0, sizeof(execInfo)); execInfo.cbSize = sizeof(execInfo); #ifndef _WIN32_WCE execInfo.fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI; #else execInfo.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI; #endif const QString verb(QLatin1String("properties")); execInfo.lpVerb = (LPCWSTR)verb.utf16(); execInfo.lpFile = (LPCWSTR)path_.utf16(); #ifndef _WIN32_WCE return ShellExecuteExW(&execInfo); #else return ShellExecuteEx(&execInfo); //There is no native file property dialog in wince // return false; #endif } #endif bool KPropertiesDialog::showDialog(const KFileItem &item, QWidget *parent, bool modal) { // TODO: do we really want to show the win32 property dialog? // This means we lose metainfo, support for .desktop files, etc. (DF) #ifdef Q_OS_WIN QString localPath = item.localPath(); if (!localPath.isEmpty()) { return showWin32FilePropertyDialog(localPath); } #endif KPropertiesDialog *dlg = new KPropertiesDialog(item, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const QUrl &_url, QWidget *parent, bool modal) { #ifdef Q_OS_WIN if (_url.isLocalFile()) { return showWin32FilePropertyDialog(_url.toLocalFile()); } #endif KPropertiesDialog *dlg = new KPropertiesDialog(_url, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const KFileItemList &_items, QWidget *parent, bool modal) { if (_items.count() == 1) { const KFileItem item = _items.first(); if (item.entry().count() == 0 && item.localPath().isEmpty()) // this remote item wasn't listed by a slave // Let's stat to get more info on the file { return KPropertiesDialog::showDialog(item.url(), parent, modal); } else { return KPropertiesDialog::showDialog(_items.first(), parent, modal); } } KPropertiesDialog *dlg = new KPropertiesDialog(_items, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const QList &urls, QWidget* parent, bool modal) { KPropertiesDialog *dlg = new KPropertiesDialog(urls, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } void KPropertiesDialog::KPropertiesDialogPrivate::init() { q->setFaceType(KPageDialog::Tabbed); insertPages(); KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog"); KWindowConfig::restoreWindowSize(q->windowHandle(), group); } void KPropertiesDialog::showFileSharingPage() { if (d->fileSharePage) { // FIXME: this showFileSharingPage thingy looks broken! (tokoe) // showPage( pageIndex( d->fileSharePage)); } } void KPropertiesDialog::setFileSharingPage(QWidget *page) { d->fileSharePage = page; } void KPropertiesDialog::setFileNameReadOnly(bool ro) { foreach (KPropertiesDialogPlugin *it, d->m_pageList) { if (auto *filePropsPlugin = qobject_cast(it)) { filePropsPlugin->setFileNameReadOnly(ro); } else if (auto *urlPropsPlugin = qobject_cast(it)) { urlPropsPlugin->setFileNameReadOnly(ro); } } } KPropertiesDialog::~KPropertiesDialog() { qDeleteAll(d->m_pageList); delete d; KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog"); KWindowConfig::saveWindowSize(windowHandle(), group, KConfigBase::Persistent); } void KPropertiesDialog::insertPlugin(KPropertiesDialogPlugin *plugin) { connect(plugin, &KPropertiesDialogPlugin::changed, plugin, QOverload<>::of(&KPropertiesDialogPlugin::setDirty)); d->m_pageList.append(plugin); } QUrl KPropertiesDialog::url() const { return d->m_singleUrl; } KFileItem &KPropertiesDialog::item() { return d->m_items.first(); } KFileItemList KPropertiesDialog::items() const { return d->m_items; } QUrl KPropertiesDialog::currentDir() const { return d->m_currentDir; } QString KPropertiesDialog::defaultName() const { return d->m_defaultName; } bool KPropertiesDialog::canDisplay(const KFileItemList &_items) { // TODO: cache the result of those calls. Currently we parse .desktop files far too many times return KFilePropsPlugin::supports(_items) || KFilePermissionsPropsPlugin::supports(_items) || KDesktopPropsPlugin::supports(_items) || KUrlPropsPlugin::supports(_items) || KDevicePropsPlugin::supports(_items) /* || KPreviewPropsPlugin::supports( _items )*/; } void KPropertiesDialog::slotOk() { accept(); } void KPropertiesDialog::accept() { QList::const_iterator pageListIt; d->m_aborted = false; KFilePropsPlugin *filePropsPlugin = qobject_cast(d->m_pageList.first()); // If any page is dirty, then set the main one (KFilePropsPlugin) as // dirty too. This is what makes it possible to save changes to a global // desktop file into a local one. In other cases, it doesn't hurt. for (pageListIt = d->m_pageList.constBegin(); pageListIt != d->m_pageList.constEnd(); ++pageListIt) { if ((*pageListIt)->isDirty() && filePropsPlugin) { filePropsPlugin->setDirty(); break; } } // Apply the changes in the _normal_ order of the tabs now // This is because in case of renaming a file, KFilePropsPlugin will call // KPropertiesDialog::rename, so other tab will be ok with whatever order // BUT for file copied from templates, we need to do the renaming first ! for (pageListIt = d->m_pageList.constBegin(); pageListIt != d->m_pageList.constEnd() && !d->m_aborted; ++pageListIt) { if ((*pageListIt)->isDirty()) { // qDebug() << "applying changes for " << (*pageListIt)->metaObject()->className(); (*pageListIt)->applyChanges(); // applyChanges may change d->m_aborted. } else { // qDebug() << "skipping page " << (*pageListIt)->metaObject()->className(); } } if (!d->m_aborted && filePropsPlugin) { filePropsPlugin->postApplyChanges(); } if (!d->m_aborted) { emit applied(); emit propertiesClosed(); deleteLater(); // somewhat like Qt::WA_DeleteOnClose would do. KPageDialog::accept(); } // else, keep dialog open for user to fix the problem. } void KPropertiesDialog::slotCancel() { reject(); } void KPropertiesDialog::reject() { emit canceled(); emit propertiesClosed(); deleteLater(); KPageDialog::reject(); } void KPropertiesDialog::KPropertiesDialogPrivate::insertPages() { if (m_items.isEmpty()) { return; } if (KFilePropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KFilePropsPlugin(q); q->insertPlugin(p); } if (KFilePermissionsPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KFilePermissionsPropsPlugin(q); q->insertPlugin(p); } if (KChecksumsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KChecksumsPlugin(q); q->insertPlugin(p); } if (KDesktopPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KDesktopPropsPlugin(q); q->insertPlugin(p); } if (KUrlPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KUrlPropsPlugin(q); q->insertPlugin(p); } if (KDevicePropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KDevicePropsPlugin(q); q->insertPlugin(p); } // if ( KPreviewPropsPlugin::supports( m_items ) ) { // KPropertiesDialogPlugin *p = new KPreviewPropsPlugin(q); // q->insertPlugin(p); // } //plugins if (m_items.count() != 1) { return; } const KFileItem item = m_items.first(); const QString mimetype = item.mimetype(); if (mimetype.isEmpty()) { return; } QString query = QStringLiteral( "(((not exist [X-KDE-Protocol]) and (not exist [X-KDE-Protocols])) or ([X-KDE-Protocol] == '%1') or ('%1' in [X-KDE-Protocols]))" ).arg(item.url().scheme()); // qDebug() << "trader query: " << query; const KService::List offers = KMimeTypeTrader::self()->query(mimetype, QStringLiteral("KPropertiesDialog/Plugin"), query); for (const KService::Ptr &ptr : offers) { KPropertiesDialogPlugin *plugin = ptr->createInstance(q); if (!plugin) { continue; } plugin->setObjectName(ptr->name()); q->insertPlugin(plugin); } } void KPropertiesDialog::updateUrl(const QUrl &_newUrl) { Q_ASSERT(d->m_items.count() == 1); // qDebug() << "KPropertiesDialog::updateUrl (pre)" << _newUrl; QUrl newUrl = _newUrl; emit saveAs(d->m_singleUrl, newUrl); // qDebug() << "KPropertiesDialog::updateUrl (post)" << newUrl; d->m_singleUrl = newUrl; d->m_items.first().setUrl(newUrl); Q_ASSERT(!d->m_singleUrl.isEmpty()); // If we have an Desktop page, set it dirty, so that a full file is saved locally // Same for a URL page (because of the Name= hack) foreach (KPropertiesDialogPlugin *it, d->m_pageList) { if (qobject_cast(it) || qobject_cast(it)) { //qDebug() << "Setting page dirty"; it->setDirty(); break; } } } void KPropertiesDialog::rename(const QString &_name) { Q_ASSERT(d->m_items.count() == 1); // qDebug() << "KPropertiesDialog::rename " << _name; QUrl newUrl; // if we're creating from a template : use currentdir if (!d->m_currentDir.isEmpty()) { newUrl = d->m_currentDir; newUrl.setPath(concatPaths(newUrl.path(), _name)); } else { // It's a directory, so strip the trailing slash first newUrl = d->m_singleUrl.adjusted(QUrl::StripTrailingSlash); // Now change the filename newUrl = newUrl.adjusted(QUrl::RemoveFilename); // keep trailing slash newUrl.setPath(concatPaths(newUrl.path(), _name)); } updateUrl(newUrl); } void KPropertiesDialog::abortApplying() { d->m_aborted = true; } class Q_DECL_HIDDEN KPropertiesDialogPlugin::KPropertiesDialogPluginPrivate { public: KPropertiesDialogPluginPrivate() { } ~KPropertiesDialogPluginPrivate() { } bool m_bDirty; int fontHeight; }; KPropertiesDialogPlugin::KPropertiesDialogPlugin(KPropertiesDialog *_props) : QObject(_props), d(new KPropertiesDialogPluginPrivate) { properties = _props; d->fontHeight = 2 * properties->fontMetrics().height(); d->m_bDirty = false; } KPropertiesDialogPlugin::~KPropertiesDialogPlugin() { delete d; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KPropertiesDialogPlugin::isDesktopFile(const KFileItem &_item) { return _item.isDesktopFile(); } #endif void KPropertiesDialogPlugin::setDirty(bool b) { d->m_bDirty = b; } void KPropertiesDialogPlugin::setDirty() { d->m_bDirty = true; } bool KPropertiesDialogPlugin::isDirty() const { return d->m_bDirty; } void KPropertiesDialogPlugin::applyChanges() { qCWarning(KIO_WIDGETS) << "applyChanges() not implemented in page !"; } int KPropertiesDialogPlugin::fontHeight() const { return d->fontHeight; } /////////////////////////////////////////////////////////////////////////////// class KFilePropsPlugin::KFilePropsPluginPrivate { public: KFilePropsPluginPrivate() { dirSizeJob = nullptr; dirSizeUpdateTimer = nullptr; m_lined = nullptr; m_capacityBar = nullptr; m_linkTargetLineEdit = nullptr; } ~KFilePropsPluginPrivate() { if (dirSizeJob) { dirSizeJob->kill(); } } KIO::DirectorySizeJob *dirSizeJob; QTimer *dirSizeUpdateTimer; QFrame *m_frame; bool bMultiple; bool bIconChanged; bool bKDesktopMode; bool bDesktopFile; KCapacityBar *m_capacityBar; QString mimeType; QString oldFileName; KLineEdit *m_lined; QLabel *m_fileNameLabel = nullptr; QGridLayout *m_grid = nullptr; QWidget *iconArea; QLabel *m_sizeLabel; QPushButton *m_sizeDetermineButton; QPushButton *m_sizeStopButton; KLineEdit *m_linkTargetLineEdit; QString m_sRelativePath; bool m_bFromTemplate; /** * The initial filename */ QString oldName; }; KFilePropsPlugin::KFilePropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KFilePropsPluginPrivate) { d->bMultiple = (properties->items().count() > 1); d->bIconChanged = false; d->bDesktopFile = KDesktopPropsPlugin::supports(properties->items()); // qDebug() << "KFilePropsPlugin::KFilePropsPlugin bMultiple=" << d->bMultiple; // We set this data from the first item, and we'll // check that the other items match against it, resetting when not. bool isLocal; const KFileItem item = properties->item(); QUrl url = item.mostLocalUrl(isLocal); bool isReallyLocal = item.url().isLocalFile(); bool bDesktopFile = item.isDesktopFile(); mode_t mode = item.mode(); bool hasDirs = item.isDir() && !item.isLink(); bool hasRoot = url.path() == QLatin1String("/"); QString iconStr = item.iconName(); QString directory = properties->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString protocol = properties->url().scheme(); d->bKDesktopMode = protocol == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop"); QString mimeComment = item.mimeComment(); d->mimeType = item.mimetype(); KIO::filesize_t totalSize = item.size(); QString magicMimeComment; QMimeDatabase db; if (isLocal) { QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent); if (magicMimeType.isValid() && !magicMimeType.isDefault()) { magicMimeComment = magicMimeType.comment(); } } #ifdef Q_OS_WIN if (isReallyLocal) { directory = QDir::toNativeSeparators(directory.mid(1)); } #endif // Those things only apply to 'single file' mode QString filename; bool isTrash = false; d->m_bFromTemplate = false; // And those only to 'multiple' mode uint iDirCount = hasDirs ? 1 : 0; uint iFileCount = 1 - iDirCount; d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18nc("@title:tab File properties", "&General")); QVBoxLayout *vbl = new QVBoxLayout(d->m_frame); vbl->setContentsMargins(0, 0, 0, 0); vbl->setObjectName(QStringLiteral("vbl")); QGridLayout *grid = new QGridLayout(); // unknown rows d->m_grid = grid; grid->setColumnStretch(0, 0); grid->setColumnStretch(1, 0); grid->setColumnStretch(2, 1); const int spacingHint = d->m_frame->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); grid->addItem(new QSpacerItem(spacingHint, 0), 0, 1); vbl->addLayout(grid); int curRow = 0; if (!d->bMultiple) { QString path; if (!d->m_bFromTemplate) { isTrash = (properties->url().scheme() == QLatin1String("trash")); // Extract the full name, but without file: for local files path = properties->url().toDisplayString(QUrl::PreferLocalFile); } else { path = concatPaths(properties->currentDir().path(), properties->defaultName()); directory = properties->currentDir().toDisplayString(QUrl::PreferLocalFile); } if (d->bDesktopFile) { determineRelativePath(path); } // Extract the file name only filename = properties->defaultName(); if (filename.isEmpty()) { // no template const QFileInfo finfo(item.name()); // this gives support for UDS_NAME, e.g. for kio_trash or kio_system filename = finfo.fileName(); // Make sure only the file's name is displayed (#160964). } else { d->m_bFromTemplate = true; setDirty(); // to enforce that the copy happens } d->oldFileName = filename; // Make it human-readable filename = nameFromFileName(filename); if (d->bKDesktopMode && d->bDesktopFile) { KDesktopFile config(url.toLocalFile()); if (config.desktopGroup().hasKey("Name")) { filename = config.readName(); } } d->oldName = filename; } else { // Multiple items: see what they have in common const KFileItemList items = properties->items(); KFileItemList::const_iterator kit = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (++kit /*no need to check the first one again*/; kit != kend; ++kit) { const QUrl url = (*kit).url(); // qDebug() << "KFilePropsPlugin::KFilePropsPlugin " << url.toDisplayString(); // The list of things we check here should match the variables defined // at the beginning of this method. if (url.isLocalFile() != isLocal) { isLocal = false; // not all local } if (bDesktopFile && (*kit).isDesktopFile() != bDesktopFile) { bDesktopFile = false; // not all desktop files } if ((*kit).mode() != mode) { mode = (mode_t)0; } if (KIO::iconNameForUrl(url) != iconStr) { iconStr = QStringLiteral("document-multiple"); } if (url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() != directory) { directory.clear(); } if (url.scheme() != protocol) { protocol.clear(); } if (!mimeComment.isNull() && (*kit).mimeComment() != mimeComment) { mimeComment.clear(); } if (isLocal && !magicMimeComment.isNull()) { QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent); if (magicMimeType.isValid() && magicMimeType.comment() != magicMimeComment) { magicMimeComment.clear(); } } if (isLocal && url.path() == QLatin1String("/")) { hasRoot = true; } if ((*kit).isDir() && !(*kit).isLink()) { iDirCount++; hasDirs = true; } else { iFileCount++; totalSize += (*kit).size(); } } } if (!isReallyLocal && !protocol.isEmpty()) { directory += QLatin1String(" (") + protocol + QLatin1Char(')'); } if (!isTrash && (bDesktopFile || ((mode & QT_STAT_MASK) == QT_STAT_DIR)) && !d->bMultiple // not implemented for multiple && enableIconButton()) { // #56857 KIconButton *iconButton = new KIconButton(d->m_frame); int bsize = 66 + 2 * iconButton->style()->pixelMetric(QStyle::PM_ButtonMargin); iconButton->setFixedSize(bsize, bsize); iconButton->setIconSize(48); iconButton->setStrictIconSize(false); if (bDesktopFile && isLocal) { const KDesktopFile config(url.toLocalFile()); if (config.hasDeviceType()) { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Device); } else { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Application); } } else { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Place); } iconButton->setIcon(iconStr); d->iconArea = iconButton; connect(iconButton, &KIconButton::iconChanged, this, &KFilePropsPlugin::slotIconChanged); } else { QLabel *iconLabel = new QLabel(d->m_frame); iconLabel->setAlignment(Qt::AlignCenter); int bsize = 66 + 2 * iconLabel->style()->pixelMetric(QStyle::PM_ButtonMargin); iconLabel->setFixedSize(bsize, bsize); iconLabel->setPixmap(QIcon::fromTheme(iconStr).pixmap(48)); d->iconArea = iconLabel; } grid->addWidget(d->iconArea, curRow, 0, Qt::AlignCenter); KFileItemListProperties itemList(KFileItemList() << item); if (d->bMultiple || isTrash || hasRoot || !(d->m_bFromTemplate || itemList.supportsMoving())) { setFileNameReadOnly(true); if (d->bMultiple) { d->m_fileNameLabel->setText(KIO::itemsSummaryString(iFileCount + iDirCount, iFileCount, iDirCount, 0, false)); } } else { d->m_lined = new KLineEdit(d->m_frame); d->m_lined->setObjectName(QStringLiteral("KFilePropsPlugin::nameLineEdit")); d->m_lined->setText(filename); d->m_lined->setFocus(); // Enhanced rename: Don't highlight the file extension. QString extension = db.suffixForFileName(filename); if (!extension.isEmpty()) { d->m_lined->setSelection(0, filename.length() - extension.length() - 1); } else { int lastDot = filename.lastIndexOf(QLatin1Char('.')); if (lastDot > 0) { d->m_lined->setSelection(0, lastDot); } } connect(d->m_lined, &QLineEdit::textChanged, this, &KFilePropsPlugin::nameFileChanged); grid->addWidget(d->m_lined, curRow, 2); } ++curRow; KSeparator *sep = new KSeparator(Qt::Horizontal, d->m_frame); grid->addWidget(sep, curRow, 0, 1, 3); ++curRow; QLabel *l; if (!mimeComment.isEmpty() && !isTrash) { l = new QLabel(i18n("Type:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight | Qt::AlignTop); QFrame *box = new QFrame(d->m_frame); QVBoxLayout *boxLayout = new QVBoxLayout(box); boxLayout->setSpacing(2); // without that spacing the button literally “sticks” to the label ;) boxLayout->setContentsMargins(0, 0, 0, 0); l = new QLabel(mimeComment, box); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(box, curRow++, 2); QPushButton *button = new QPushButton(box); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // Minimum still makes the button grow to the entire layout width button->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); boxLayout->addWidget(l); boxLayout->addWidget(button); if (d->mimeType == QLatin1String("application/octet-stream")) { button->setText(i18n("Create New File Type")); } else { button->setText(i18n("File Type Options")); } connect(button, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotEditFileType); if (!KAuthorized::authorizeAction(QStringLiteral("editfiletype"))) { button->hide(); } } if (!magicMimeComment.isEmpty() && magicMimeComment != mimeComment) { l = new QLabel(i18n("Contents:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(magicMimeComment, d->m_frame); grid->addWidget(l, curRow++, 2); } if (!directory.isEmpty()) { l = new QLabel(i18n("Location:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new KSqueezedTextLabel(directory, d->m_frame); // force the layout direction to be always LTR l->setLayoutDirection(Qt::LeftToRight); // but if we are in RTL mode, align the text to the right // otherwise the text is on the wrong side of the dialog if (properties->layoutDirection() == Qt::RightToLeft) { l->setAlignment(Qt::AlignRight); } l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } l = new QLabel(i18n("Size:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight | Qt::AlignTop); d->m_sizeLabel = new QLabel(d->m_frame); d->m_sizeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(d->m_sizeLabel, curRow++, 2); if (!hasDirs) { // Only files [and symlinks] d->m_sizeLabel->setText(QStringLiteral("%1 (%2)").arg(KIO::convertSize(totalSize), QLocale().toString(totalSize))); d->m_sizeDetermineButton = nullptr; d->m_sizeStopButton = nullptr; } else { // Directory QHBoxLayout *sizelay = new QHBoxLayout(); grid->addLayout(sizelay, curRow++, 2); // buttons d->m_sizeDetermineButton = new QPushButton(i18n("Calculate"), d->m_frame); d->m_sizeStopButton = new QPushButton(i18n("Stop"), d->m_frame); d->m_sizeDetermineButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); d->m_sizeStopButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel"))); connect(d->m_sizeDetermineButton, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeDetermine); connect(d->m_sizeStopButton, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeStop); sizelay->addWidget(d->m_sizeDetermineButton, 0); sizelay->addWidget(d->m_sizeStopButton, 0); sizelay->addStretch(10); // so that the buttons don't grow horizontally // auto-launch for local dirs only, and not for '/' if (isLocal && !hasRoot) { d->m_sizeDetermineButton->setText(i18n("Refresh")); slotSizeDetermine(); } else { d->m_sizeStopButton->setEnabled(false); } } if (!d->bMultiple && item.isLink()) { l = new QLabel(i18n("Points to:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); d->m_linkTargetLineEdit = new KLineEdit(item.linkDest(), d->m_frame); grid->addWidget(d->m_linkTargetLineEdit, curRow++, 2); connect(d->m_linkTargetLineEdit, &QLineEdit::textChanged, this, QOverload<>::of(&KFilePropsPlugin::setDirty)); } if (!d->bMultiple) { // Dates for multiple don't make much sense... QDateTime dt = item.time(KFileItem::CreationTime); if (!dt.isNull()) { l = new QLabel(i18n("Created:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); grid->addWidget(l, curRow++, 2); } dt = item.time(KFileItem::ModificationTime); if (!dt.isNull()) { l = new QLabel(i18n("Modified:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } dt = item.time(KFileItem::AccessTime); if (!dt.isNull()) { l = new QLabel(i18n("Accessed:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } } if (hasDirs) { // only for directories sep = new KSeparator(Qt::Horizontal, d->m_frame); grid->addWidget(sep, curRow, 0, 1, 3); ++curRow; if (isLocal) { KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(url.toLocalFile()); if (mp) { l = new QLabel(i18n("File System:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(d->m_frame); grid->addWidget(l, curRow++, 2); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); l->setText(mp->mountType()); l = new QLabel(i18n("Mounted on:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new KSqueezedTextLabel(mp->mountPoint(), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); l = new QLabel(i18n("Mounted from:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(mp->mountedFrom(), d->m_frame); grid->addWidget(l, curRow++, 2); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); } } l = new QLabel(i18nc("Amount of used and available space on this device or partition", "Free space:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); d->m_capacityBar = new KCapacityBar(KCapacityBar::DrawTextOutline, d->m_frame); d->m_capacityBar->setText(i18nc("@info:status", "Unknown size")); grid->addWidget(d->m_capacityBar, curRow++, 2); KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url); connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult); } vbl->addStretch(1); } bool KFilePropsPlugin::enableIconButton() const { bool iconEnabled = false; const KFileItem item = properties->item(); // If the current item is a directory, check if it's writable, // so we can create/update a .directory // Current item is a file, same thing: check if it is writable if (item.isWritable()) { iconEnabled = true; } return iconEnabled; } // QString KFilePropsPlugin::tabName () const // { // return i18n ("&General"); // } void KFilePropsPlugin::setFileNameReadOnly(bool ro) { Q_ASSERT(ro); // false isn't supported if (ro && !d->m_fileNameLabel) { Q_ASSERT(!d->m_bFromTemplate); delete d->m_lined; d->m_lined = nullptr; d->m_fileNameLabel = new QLabel(d->m_frame); d->m_fileNameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_fileNameLabel->setText(d->oldName); // will get overwritten if d->bMultiple d->m_grid->addWidget(d->m_fileNameLabel, 0, 2); } } void KFilePropsPlugin::slotEditFileType() { QString mime; if (d->mimeType == QLatin1String("application/octet-stream")) { const int pos = d->oldFileName.lastIndexOf(QLatin1Char('.')); if (pos != -1) { mime = QLatin1Char('*') + d->oldFileName.midRef(pos); } else { mime = QStringLiteral("*"); } } else { mime = d->mimeType; } KMimeTypeEditor::editMimeType(mime, properties->window()); } void KFilePropsPlugin::slotIconChanged() { d->bIconChanged = true; emit changed(); } void KFilePropsPlugin::nameFileChanged(const QString &text) { properties->buttonBox()->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); emit changed(); } static QString relativeAppsLocation(const QString &file) { const QString canonical = QFileInfo(file).canonicalFilePath(); Q_FOREACH (const QString &base, QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) { QDir base_dir = QDir(base); if (base_dir.exists() && canonical.startsWith(base_dir.canonicalPath())) { return canonical.mid(base.length() + 1); } } return QString(); // return empty if the file is not in apps } void KFilePropsPlugin::determineRelativePath(const QString &path) { // now let's make it relative d->m_sRelativePath = relativeAppsLocation(path); } void KFilePropsPlugin::slotFreeSpaceResult(KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { if (!job->error()) { const quint64 used = size - available; const int percentUsed = qRound(100.0 * qreal(used) / qreal(size)); d->m_capacityBar->setText( i18nc("Available space out of total partition size (percent used)", "%1 free of %2 (%3% used)", KIO::convertSize(available), KIO::convertSize(size), percentUsed)); d->m_capacityBar->setValue(percentUsed); } else { d->m_capacityBar->setText(i18nc("@info:status", "Unknown size")); d->m_capacityBar->setValue(0); } } void KFilePropsPlugin::slotDirSizeUpdate() { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles(); KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs(); d->m_sizeLabel->setText( i18n("Calculating... %1 (%2)\n%3, %4", KIO::convertSize(totalSize), QLocale().toString(totalSize), i18np("1 file", "%1 files", totalFiles), i18np("1 sub-folder", "%1 sub-folders", totalSubdirs))); } void KFilePropsPlugin::slotDirSizeFinished(KJob *job) { if (job->error()) { d->m_sizeLabel->setText(job->errorString()); } else { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles(); KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs(); d->m_sizeLabel->setText(QStringLiteral("%1 (%2)\n%3, %4") .arg(KIO::convertSize(totalSize), QLocale().toString(totalSize), i18np("1 file", "%1 files", totalFiles), i18np("1 sub-folder", "%1 sub-folders", totalSubdirs))); } d->m_sizeStopButton->setEnabled(false); // just in case you change something and try again :) d->m_sizeDetermineButton->setText(i18n("Refresh")); d->m_sizeDetermineButton->setEnabled(true); d->dirSizeJob = nullptr; delete d->dirSizeUpdateTimer; d->dirSizeUpdateTimer = nullptr; } void KFilePropsPlugin::slotSizeDetermine() { d->m_sizeLabel->setText(i18n("Calculating...")); // qDebug() << "properties->item()=" << properties->item() << "URL=" << properties->item().url(); d->dirSizeJob = KIO::directorySize(properties->items()); d->dirSizeUpdateTimer = new QTimer(this); connect(d->dirSizeUpdateTimer, &QTimer::timeout, this, &KFilePropsPlugin::slotDirSizeUpdate); d->dirSizeUpdateTimer->start(500); connect(d->dirSizeJob, &KJob::result, this, &KFilePropsPlugin::slotDirSizeFinished); d->m_sizeStopButton->setEnabled(true); d->m_sizeDetermineButton->setEnabled(false); // also update the "Free disk space" display if (d->m_capacityBar) { const KFileItem item = properties->item(); KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(item.url()); connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult); } } void KFilePropsPlugin::slotSizeStop() { if (d->dirSizeJob) { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); d->m_sizeLabel->setText(i18n("At least %1", KIO::convertSize(totalSize))); d->dirSizeJob->kill(); d->dirSizeJob = nullptr; } if (d->dirSizeUpdateTimer) { d->dirSizeUpdateTimer->stop(); } d->m_sizeStopButton->setEnabled(false); d->m_sizeDetermineButton->setEnabled(true); } KFilePropsPlugin::~KFilePropsPlugin() { delete d; } bool KFilePropsPlugin::supports(const KFileItemList & /*_items*/) { return true; } void KFilePropsPlugin::applyChanges() { if (d->dirSizeJob) { slotSizeStop(); } // qDebug() << "KFilePropsPlugin::applyChanges"; if (d->m_lined) { QString n = d->m_lined->text(); // Remove trailing spaces (#4345) while (! n.isEmpty() && n[n.length() - 1].isSpace()) { n.chop(1); } if (n.isEmpty()) { KMessageBox::sorry(properties, i18n("The new file name is empty.")); properties->abortApplying(); return; } // Do we need to rename the file ? // qDebug() << "oldname = " << d->oldName; // qDebug() << "newname = " << n; if (d->oldName != n || d->m_bFromTemplate) { // true for any from-template file KIO::CopyJob *job = nullptr; QUrl oldurl = properties->url(); QString newFileName = KIO::encodeFileName(n); if (d->bDesktopFile && !newFileName.endsWith(QLatin1String(".desktop")) && !newFileName.endsWith(QLatin1String(".kdelnk"))) { newFileName += QLatin1String(".desktop"); } // Tell properties. Warning, this changes the result of properties->url() ! properties->rename(newFileName); // Update also relative path (for apps) if (!d->m_sRelativePath.isEmpty()) { determineRelativePath(properties->url().toLocalFile()); } // qDebug() << "New URL = " << properties->url(); // qDebug() << "old = " << oldurl.url(); // Don't remove the template !! if (!d->m_bFromTemplate) { // (normal renaming) job = KIO::moveAs(oldurl, properties->url()); } else { // Copying a template job = KIO::copyAs(oldurl, properties->url()); } connect(job, &KJob::result, this, &KFilePropsPlugin::slotCopyFinished); connect(job, &KIO::CopyJob::renamed, this, &KFilePropsPlugin::slotFileRenamed); // wait for job QEventLoop eventLoop; connect(this, &KFilePropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); return; } properties->updateUrl(properties->url()); // Update also relative path (for apps) if (!d->m_sRelativePath.isEmpty()) { determineRelativePath(properties->url().toLocalFile()); } } // No job, keep going slotCopyFinished(nullptr); } void KFilePropsPlugin::slotCopyFinished(KJob *job) { // qDebug() << "KFilePropsPlugin::slotCopyFinished"; if (job) { // allow apply() to return emit leaveModality(); if (job->error()) { job->uiDelegate()->showErrorMessage(); // Didn't work. Revert the URL to the old one properties->updateUrl(static_cast(job)->srcUrls().constFirst()); properties->abortApplying(); // Don't apply the changes to the wrong file ! return; } } Q_ASSERT(!properties->item().isNull()); Q_ASSERT(!properties->item().url().isEmpty()); // Save the file locally if (d->bDesktopFile && !d->m_sRelativePath.isEmpty()) { // qDebug() << "KFilePropsPlugin::slotCopyFinished " << d->m_sRelativePath; const QString newPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + d->m_sRelativePath; const QUrl newURL = QUrl::fromLocalFile(newPath); // qDebug() << "KFilePropsPlugin::slotCopyFinished path=" << newURL; properties->updateUrl(newURL); } if (d->bKDesktopMode && d->bDesktopFile) { // Renamed? Update Name field // Note: The desktop ioslave does this as well, but not when // the file is copied from a template. if (d->m_bFromTemplate) { KIO::StatJob *job = KIO::stat(properties->url()); job->exec(); KIO::UDSEntry entry = job->statResult(); KFileItem item(entry, properties->url()); KDesktopFile config(item.localPath()); KConfigGroup cg = config.desktopGroup(); QString nameStr = nameFromFileName(properties->url().fileName()); cg.writeEntry("Name", nameStr); cg.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized); } } if (d->m_linkTargetLineEdit && !d->bMultiple) { const KFileItem item = properties->item(); const QString newTarget = d->m_linkTargetLineEdit->text(); if (newTarget != item.linkDest()) { // qDebug() << "Updating target of symlink to" << newTarget; KIO::Job *job = KIO::symlink(newTarget, item.url(), KIO::Overwrite); job->uiDelegate()->setAutoErrorHandlingEnabled(true); job->exec(); } } // "Link to Application" templates need to be made executable // Instead of matching against a filename we check if the destination // is an Application now. if (d->m_bFromTemplate) { // destination is not necessarily local, use the src template KDesktopFile templateResult(static_cast(job)->srcUrls().constFirst().toLocalFile()); if (templateResult.hasApplicationType()) { // We can either stat the file and add the +x bit or use the larger chmod() job // with a umask designed to only touch u+x. This is only one KIO job, so let's // do that. KFileItem appLink(properties->item()); KFileItemList fileItemList; fileItemList << appLink; // first 0100 adds u+x, second 0100 only allows chmod to change u+x KIO::Job *chmodJob = KIO::chmod(fileItemList, 0100, 0100, QString(), QString(), KIO::HideProgressInfo); chmodJob->exec(); } } } void KFilePropsPlugin::applyIconChanges() { KIconButton *iconButton = qobject_cast(d->iconArea); if (!iconButton || !d->bIconChanged) { return; } // handle icon changes - only local files (or pseudo-local) for now // TODO: Use KTempFile and KIO::file_copy with overwrite = true QUrl url = properties->url(); KIO::StatJob *job = KIO::mostLocalUrl(url); KJobWidgets::setWindow(job, properties); job->exec(); url = job->mostLocalUrl(); if (url.isLocalFile()) { QString path; if ((properties->item().mode() & QT_STAT_MASK) == QT_STAT_DIR) { path = url.toLocalFile() + QLatin1String("/.directory"); // don't call updateUrl because the other tabs (i.e. permissions) // apply to the directory, not the .directory file. } else { path = url.toLocalFile(); } // Get the default image QMimeDatabase db; QString str = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchExtension).iconName(); // Is it another one than the default ? QString sIcon; if (str != iconButton->icon()) { sIcon = iconButton->icon(); } // (otherwise write empty value) // qDebug() << "**" << path << "**"; // If default icon and no .directory file -> don't create one if (!sIcon.isEmpty() || QFile::exists(path)) { KDesktopFile cfg(path); // qDebug() << "sIcon = " << (sIcon); // qDebug() << "str = " << (str); cfg.desktopGroup().writeEntry("Icon", sIcon); cfg.sync(); cfg.reparseConfiguration(); if (cfg.desktopGroup().readEntry("Icon") != sIcon) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not " "have sufficient access to write to %1.", path)); } } } } void KFilePropsPlugin::slotFileRenamed(KIO::Job *, const QUrl &, const QUrl &newUrl) { // This is called in case of an existing local file during the copy/move operation, // if the user chooses Rename. properties->updateUrl(newUrl); } void KFilePropsPlugin::postApplyChanges() { // Save the icon only after applying the permissions changes (#46192) applyIconChanges(); const KFileItemList items = properties->items(); const QList lst = items.urlList(); org::kde::KDirNotify::emitFilesChanged(QList(lst)); } class KFilePermissionsPropsPlugin::KFilePermissionsPropsPluginPrivate { public: KFilePermissionsPropsPluginPrivate() { } ~KFilePermissionsPropsPluginPrivate() { } QFrame *m_frame; QCheckBox *cbRecursive; QLabel *explanationLabel; KComboBox *ownerPermCombo, *groupPermCombo, *othersPermCombo; QCheckBox *extraCheckbox; mode_t partialPermissions; KFilePermissionsPropsPlugin::PermissionsMode pmode; bool canChangePermissions; bool isIrregular; bool hasExtendedACL; KACL extendedACL; KACL defaultACL; bool fileSystemSupportsACLs; KComboBox *grpCombo; KLineEdit *usrEdit; KLineEdit *grpEdit; // Old permissions mode_t permissions; // Old group QString strGroup; // Old owner QString strOwner; }; #define UniOwner (S_IRUSR|S_IWUSR|S_IXUSR) #define UniGroup (S_IRGRP|S_IWGRP|S_IXGRP) #define UniOthers (S_IROTH|S_IWOTH|S_IXOTH) #define UniRead (S_IRUSR|S_IRGRP|S_IROTH) #define UniWrite (S_IWUSR|S_IWGRP|S_IWOTH) #define UniExec (S_IXUSR|S_IXGRP|S_IXOTH) #define UniSpecial (S_ISUID|S_ISGID|S_ISVTX) // synced with PermissionsTarget const mode_t KFilePermissionsPropsPlugin::permissionsMasks[3] = {UniOwner, UniGroup, UniOthers}; const mode_t KFilePermissionsPropsPlugin::standardPermissions[4] = { 0, UniRead, UniRead | UniWrite, (mode_t) - 1 }; // synced with PermissionsMode and standardPermissions const char *const KFilePermissionsPropsPlugin::permissionsTexts[4][4] = { { I18N_NOOP("No Access"), I18N_NOOP("Can Only View"), I18N_NOOP("Can View & Modify"), nullptr }, { I18N_NOOP("No Access"), I18N_NOOP("Can Only View Content"), I18N_NOOP("Can View & Modify Content"), nullptr }, { nullptr, nullptr, nullptr, nullptr}, // no texts for links { I18N_NOOP("No Access"), I18N_NOOP("Can Only View/Read Content"), I18N_NOOP("Can View/Read & Modify/Write"), nullptr } }; KFilePermissionsPropsPlugin::KFilePermissionsPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KFilePermissionsPropsPluginPrivate) { d->cbRecursive = nullptr; d->grpCombo = nullptr; d->grpEdit = nullptr; d->usrEdit = nullptr; bool isLocal = properties->url().isLocalFile(); bool isTrash = (properties->url().scheme() == QLatin1String("trash")); KUser myself(KUser::UseEffectiveUID); const bool IamRoot = myself.isSuperUser(); const KFileItem item = properties->item(); bool isLink = item.isLink(); bool isDir = item.isDir(); // all dirs bool hasDir = item.isDir(); // at least one dir d->permissions = item.permissions(); // common permissions to all files d->partialPermissions = d->permissions; // permissions that only some files have (at first we take everything) d->isIrregular = isIrregular(d->permissions, isDir, isLink); d->strOwner = item.user(); d->strGroup = item.group(); d->hasExtendedACL = item.ACL().isExtended() || item.defaultACL().isValid(); d->extendedACL = item.ACL(); d->defaultACL = item.defaultACL(); d->fileSystemSupportsACLs = false; if (properties->items().count() > 1) { // Multiple items: see what they have in common const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (++it /*no need to check the first one again*/; it != kend; ++it) { if (!d->isIrregular) d->isIrregular |= isIrregular((*it).permissions(), (*it).isDir() == isDir, (*it).isLink() == isLink); d->hasExtendedACL = d->hasExtendedACL || (*it).hasExtendedACL(); if ((*it).isLink() != isLink) { isLink = false; } if ((*it).isDir() != isDir) { isDir = false; } hasDir |= (*it).isDir(); if ((*it).permissions() != d->permissions) { d->permissions &= (*it).permissions(); d->partialPermissions |= (*it).permissions(); } if ((*it).user() != d->strOwner) { d->strOwner.clear(); } if ((*it).group() != d->strGroup) { d->strGroup.clear(); } } } if (isLink) { d->pmode = PermissionsOnlyLinks; } else if (isDir) { d->pmode = PermissionsOnlyDirs; } else if (hasDir) { d->pmode = PermissionsMixed; } else { d->pmode = PermissionsOnlyFiles; } // keep only what's not in the common permissions d->partialPermissions = d->partialPermissions & ~d->permissions; bool isMyFile = false; if (isLocal && !d->strOwner.isEmpty()) { // local files, and all owned by the same person if (myself.isValid()) { isMyFile = (d->strOwner == myself.loginName()); } else { qCWarning(KIO_WIDGETS) << "I don't exist ?! geteuid=" << KUserId::currentEffectiveUserId().toString(); } } else { //We don't know, for remote files, if they are ours or not. //So we let the user change permissions, and //KIO::chmod will tell, if he had no right to do it. isMyFile = true; } d->canChangePermissions = (isMyFile || IamRoot) && (!isLink); // create GUI d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("&Permissions")); QBoxLayout *box = new QVBoxLayout(d->m_frame); box->setContentsMargins(0, 0, 0, 0); QWidget *l; QLabel *lbl; QGroupBox *gb; QGridLayout *gl; QPushButton *pbAdvancedPerm = nullptr; /* Group: Access Permissions */ gb = new QGroupBox(i18n("Access Permissions"), d->m_frame); box->addWidget(gb); gl = new QGridLayout(gb); gl->setColumnStretch(1, 1); l = d->explanationLabel = new QLabel(gb); if (isLink) d->explanationLabel->setText(i18np("This file is a link and does not have permissions.", "All files are links and do not have permissions.", properties->items().count())); else if (!d->canChangePermissions) { d->explanationLabel->setText(i18n("Only the owner can change permissions.")); } gl->addWidget(l, 0, 0, 1, 2); lbl = new QLabel(i18n("O&wner:"), gb); gl->addWidget(lbl, 1, 0, Qt::AlignRight); l = d->ownerPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 1, 1); connect(d->ownerPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that the owner is allowed to do.")); lbl = new QLabel(i18n("Gro&up:"), gb); gl->addWidget(lbl, 2, 0, Qt::AlignRight); l = d->groupPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 2, 1); connect(d->groupPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that the members of the group are allowed to do.")); lbl = new QLabel(i18n("O&thers:"), gb); gl->addWidget(lbl, 3, 0, Qt::AlignRight); l = d->othersPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 3, 1); connect(d->othersPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that all users, who are neither " "owner nor in the group, are allowed to do.")); if (!isLink) { l = d->extraCheckbox = new QCheckBox(hasDir ? i18n("Only own&er can rename and delete folder content") : i18n("Is &executable"), gb); connect(d->extraCheckbox, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed); gl->addWidget(l, 4, 1); l->setWhatsThis(hasDir ? i18n("Enable this option to allow only the folder's owner to " "delete or rename the contained files and folders. Other " "users can only add new files, which requires the 'Modify " "Content' permission.") : i18n("Enable this option to mark the file as executable. This only makes " "sense for programs and scripts. It is required when you want to " "execute them.")); QLayoutItem *spacer = new QSpacerItem(0, 20, QSizePolicy::Minimum, QSizePolicy::Expanding); gl->addItem(spacer, 5, 0, 1, 3); pbAdvancedPerm = new QPushButton(i18n("A&dvanced Permissions"), gb); gl->addWidget(pbAdvancedPerm, 6, 0, 1, 2, Qt::AlignRight); connect(pbAdvancedPerm, &QAbstractButton::clicked, this, &KFilePermissionsPropsPlugin::slotShowAdvancedPermissions); } else { d->extraCheckbox = nullptr; } /**** Group: Ownership ****/ gb = new QGroupBox(i18n("Ownership"), d->m_frame); box->addWidget(gb); gl = new QGridLayout(gb); gl->addItem(new QSpacerItem(0, 10), 0, 0); /*** Set Owner ***/ l = new QLabel(i18n("User:"), gb); gl->addWidget(l, 1, 0, Qt::AlignRight); /* GJ: Don't autocomplete more than 1000 users. This is a kind of random * value. Huge sites having 10.000+ user have a fair chance of using NIS, * (possibly) making this unacceptably slow. * OTOH, it is nice to offer this functionality for the standard user. */ int maxEntries = 1000; /* File owner: For root, offer a KLineEdit with autocompletion. * For a user, who can never chown() a file, offer a QLabel. */ if (IamRoot && isLocal) { d->usrEdit = new KLineEdit(gb); KCompletion *kcom = d->usrEdit->completionObject(); kcom->setOrder(KCompletion::Sorted); QStringList userNames = KUser::allUserNames(maxEntries); kcom->setItems(userNames); d->usrEdit->setCompletionMode((userNames.size() < maxEntries) ? KCompletion::CompletionAuto : KCompletion::CompletionNone); d->usrEdit->setText(d->strOwner); gl->addWidget(d->usrEdit, 1, 1); connect(d->usrEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); } else { l = new QLabel(d->strOwner, gb); gl->addWidget(l, 1, 1); } /*** Set Group ***/ KUser user(KUser::UseEffectiveUID); QStringList groupList = user.groupNames(); const bool isMyGroup = groupList.contains(d->strGroup); /* add the group the file currently belongs to .. * .. if it is not there already */ if (!isMyGroup) { groupList += d->strGroup; } l = new QLabel(i18n("Group:"), gb); gl->addWidget(l, 2, 0, Qt::AlignRight); /* Set group: if possible to change: * - Offer a KLineEdit for root, since he can change to any group. * - Offer a KComboBox for a normal user, since he can change to a fixed * (small) set of groups only. * If not changeable: offer a QLabel. */ if (IamRoot && isLocal) { d->grpEdit = new KLineEdit(gb); KCompletion *kcom = new KCompletion; kcom->setItems(groupList); d->grpEdit->setCompletionObject(kcom, true); d->grpEdit->setAutoDeleteCompletionObject(true); d->grpEdit->setCompletionMode(KCompletion::CompletionAuto); d->grpEdit->setText(d->strGroup); gl->addWidget(d->grpEdit, 2, 1); connect(d->grpEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); } else if ((groupList.count() > 1) && isMyFile && isLocal) { d->grpCombo = new KComboBox(gb); d->grpCombo->setObjectName(QStringLiteral("combogrouplist")); d->grpCombo->addItems(groupList); d->grpCombo->setCurrentIndex(groupList.indexOf(d->strGroup)); gl->addWidget(d->grpCombo, 2, 1); connect(d->grpCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); } else { l = new QLabel(d->strGroup, gb); gl->addWidget(l, 2, 1); } gl->setColumnStretch(2, 10); // "Apply recursive" checkbox if (hasDir && !isLink && !isTrash) { d->cbRecursive = new QCheckBox(i18n("Apply changes to all subfolders and their contents"), d->m_frame); connect(d->cbRecursive, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed); box->addWidget(d->cbRecursive); } updateAccessControls(); if (isTrash) { //don't allow to change properties for file into trash enableAccessControls(false); if (pbAdvancedPerm) { pbAdvancedPerm->setEnabled(false); } } box->addStretch(10); } #if HAVE_POSIX_ACL static bool fileSystemSupportsACL(const QByteArray &path) { bool fileSystemSupportsACLs = false; #ifdef Q_OS_FREEBSD struct statfs buf; fileSystemSupportsACLs = (statfs(path.data(), &buf) == 0) && (buf.f_flags & MNT_ACLS); #elif defined Q_OS_MACOS fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0, 0, XATTR_NOFOLLOW) >= 0 || errno == ENODATA; #else fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0) >= 0 || errno == ENODATA; #endif return fileSystemSupportsACLs; } #endif void KFilePermissionsPropsPlugin::slotShowAdvancedPermissions() { bool isDir = (d->pmode == PermissionsOnlyDirs) || (d->pmode == PermissionsMixed); QDialog dlg(properties); dlg.setModal(true); dlg.setWindowTitle(i18n("Advanced Permissions")); QLabel *l, *cl[3]; QGroupBox *gb; QGridLayout *gl; QVBoxLayout *vbox = new QVBoxLayout; dlg.setLayout(vbox); // Group: Access Permissions gb = new QGroupBox(i18n("Access Permissions"), &dlg); vbox->addWidget(gb); gl = new QGridLayout(gb); gl->addItem(new QSpacerItem(0, 10), 0, 0); QVector theNotSpecials; l = new QLabel(i18n("Class"), gb); gl->addWidget(l, 1, 0); theNotSpecials.append(l); QString readWhatsThis; QString readLabel; if (isDir) { readLabel = i18n("Show\nEntries"); readWhatsThis = i18n("This flag allows viewing the content of the folder."); } else { readLabel = i18n("Read"); readWhatsThis = i18n("The Read flag allows viewing the content of the file."); } QString writeWhatsThis; QString writeLabel; if (isDir) { writeLabel = i18n("Write\nEntries"); writeWhatsThis = i18n("This flag allows adding, renaming and deleting of files. " "Note that deleting and renaming can be limited using the Sticky flag."); } else { writeLabel = i18n("Write"); writeWhatsThis = i18n("The Write flag allows modifying the content of the file."); } QString execLabel; QString execWhatsThis; if (isDir) { execLabel = i18nc("Enter folder", "Enter"); execWhatsThis = i18n("Enable this flag to allow entering the folder."); } else { execLabel = i18n("Exec"); execWhatsThis = i18n("Enable this flag to allow executing the file as a program."); } // GJ: Add space between normal and special modes QSize size = l->sizeHint(); size.setWidth(size.width() + 15); l->setFixedSize(size); gl->addWidget(l, 1, 3); l = new QLabel(i18n("Special"), gb); gl->addWidget(l, 1, 4, 1, 1); QString specialWhatsThis; if (isDir) specialWhatsThis = i18n("Special flag. Valid for the whole folder, the exact " "meaning of the flag can be seen in the right hand column."); else specialWhatsThis = i18n("Special flag. The exact meaning of the flag can be seen " "in the right hand column."); l->setWhatsThis(specialWhatsThis); cl[0] = new QLabel(i18n("User"), gb); gl->addWidget(cl[0], 2, 0); theNotSpecials.append(cl[0]); cl[1] = new QLabel(i18n("Group"), gb); gl->addWidget(cl[1], 3, 0); theNotSpecials.append(cl[1]); cl[2] = new QLabel(i18n("Others"), gb); gl->addWidget(cl[2], 4, 0); theNotSpecials.append(cl[2]); QString setUidWhatsThis; if (isDir) setUidWhatsThis = i18n("If this flag is set, the owner of this folder will be " "the owner of all new files."); else setUidWhatsThis = i18n("If this file is an executable and the flag is set, it will " "be executed with the permissions of the owner."); QString setGidWhatsThis; if (isDir) setGidWhatsThis = i18n("If this flag is set, the group of this folder will be " "set for all new files."); else setGidWhatsThis = i18n("If this file is an executable and the flag is set, it will " "be executed with the permissions of the group."); QString stickyWhatsThis; if (isDir) stickyWhatsThis = i18n("If the Sticky flag is set on a folder, only the owner " "and root can delete or rename files. Otherwise everybody " "with write permissions can do this."); else stickyWhatsThis = i18n("The Sticky flag on a file is ignored on Linux, but may " "be used on some systems"); mode_t aPermissions, aPartialPermissions; mode_t dummy1, dummy2; if (!d->isIrregular) { switch (d->pmode) { case PermissionsOnlyFiles: getPermissionMasks(aPartialPermissions, dummy1, aPermissions, dummy2); break; case PermissionsOnlyDirs: case PermissionsMixed: getPermissionMasks(dummy1, aPartialPermissions, dummy2, aPermissions); break; case PermissionsOnlyLinks: aPermissions = UniRead | UniWrite | UniExec | UniSpecial; aPartialPermissions = 0; break; } } else { aPermissions = d->permissions; aPartialPermissions = d->partialPermissions; } // Draw Checkboxes QCheckBox *cba[3][4]; for (int row = 0; row < 3; ++row) { for (int col = 0; col < 4; ++col) { QCheckBox *cb = new QCheckBox(gb); if (col != 3) { theNotSpecials.append(cb); } cba[row][col] = cb; cb->setChecked(aPermissions & fperm[row][col]); if (aPartialPermissions & fperm[row][col]) { cb->setTristate(); cb->setCheckState(Qt::PartiallyChecked); } else if (d->cbRecursive && d->cbRecursive->isChecked()) { cb->setTristate(); } cb->setEnabled(d->canChangePermissions); gl->addWidget(cb, row + 2, col + 1); switch (col) { case 0: cb->setText(readLabel); cb->setWhatsThis(readWhatsThis); break; case 1: cb->setText(writeLabel); cb->setWhatsThis(writeWhatsThis); break; case 2: cb->setText(execLabel); cb->setWhatsThis(execWhatsThis); break; case 3: switch (row) { case 0: cb->setText(i18n("Set UID")); cb->setWhatsThis(setUidWhatsThis); break; case 1: cb->setText(i18n("Set GID")); cb->setWhatsThis(setGidWhatsThis); break; case 2: cb->setText(i18nc("File permission", "Sticky")); cb->setWhatsThis(stickyWhatsThis); break; } break; } } } gl->setColumnStretch(6, 10); #if HAVE_POSIX_ACL KACLEditWidget *extendedACLs = nullptr; // FIXME make it work with partial entries if (properties->items().count() == 1) { QByteArray path = QFile::encodeName(properties->item().url().toLocalFile()); d->fileSystemSupportsACLs = fileSystemSupportsACL(path); } if (d->fileSystemSupportsACLs) { std::for_each(theNotSpecials.begin(), theNotSpecials.end(), std::mem_fun(&QWidget::hide)); extendedACLs = new KACLEditWidget(&dlg); extendedACLs->setEnabled(d->canChangePermissions); vbox->addWidget(extendedACLs); if (d->extendedACL.isValid() && d->extendedACL.isExtended()) { extendedACLs->setACL(d->extendedACL); } else { extendedACLs->setACL(KACL(aPermissions)); } if (d->defaultACL.isValid()) { extendedACLs->setDefaultACL(d->defaultACL); } if (properties->items().constFirst().isDir()) { extendedACLs->setAllowDefaults(true); } } #endif QDialogButtonBox *buttonBox = new QDialogButtonBox(&dlg); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); vbox->addWidget(buttonBox); if (dlg.exec() != QDialog::Accepted) { return; } mode_t andPermissions = mode_t(~0); mode_t orPermissions = 0; for (int row = 0; row < 3; ++row) for (int col = 0; col < 4; ++col) { switch (cba[row][col]->checkState()) { case Qt::Checked: orPermissions |= fperm[row][col]; //fall through case Qt::Unchecked: andPermissions &= ~fperm[row][col]; break; case Qt::PartiallyChecked: break; } } d->isIrregular = false; const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (; it != kend; ++it) { if (isIrregular(((*it).permissions() & andPermissions) | orPermissions, (*it).isDir(), (*it).isLink())) { d->isIrregular = true; break; } } d->permissions = orPermissions; d->partialPermissions = andPermissions; #if HAVE_POSIX_ACL // override with the acls, if present if (extendedACLs) { d->extendedACL = extendedACLs->getACL(); d->defaultACL = extendedACLs->getDefaultACL(); d->hasExtendedACL = d->extendedACL.isExtended() || d->defaultACL.isValid(); d->permissions = d->extendedACL.basePermissions(); d->permissions |= (andPermissions | orPermissions) & (S_ISUID | S_ISGID | S_ISVTX); } #endif updateAccessControls(); emit changed(); } // QString KFilePermissionsPropsPlugin::tabName () const // { // return i18n ("&Permissions"); // } KFilePermissionsPropsPlugin::~KFilePermissionsPropsPlugin() { delete d; } bool KFilePermissionsPropsPlugin::supports(const KFileItemList & /*_items*/) { return true; } // sets a combo box in the Access Control frame void KFilePermissionsPropsPlugin::setComboContent(QComboBox *combo, PermissionsTarget target, mode_t permissions, mode_t partial) { combo->clear(); if (d->isIrregular) { //#176876 return; } if (d->pmode == PermissionsOnlyLinks) { combo->addItem(i18n("Link")); combo->setCurrentIndex(0); return; } mode_t tMask = permissionsMasks[target]; int textIndex; for (textIndex = 0; standardPermissions[textIndex] != (mode_t) - 1; textIndex++) { if ((standardPermissions[textIndex]&tMask) == (permissions & tMask & (UniRead | UniWrite))) { break; } } Q_ASSERT(standardPermissions[textIndex] != (mode_t) - 1); // must not happen, would be irreglar for (int i = 0; permissionsTexts[(int)d->pmode][i]; i++) { combo->addItem(i18n(permissionsTexts[(int)d->pmode][i])); } if (partial & tMask & ~UniExec) { combo->addItem(i18n("Varying (No Change)")); combo->setCurrentIndex(3); } else { combo->setCurrentIndex(textIndex); } } // permissions are irregular if they cant be displayed in a combo box. bool KFilePermissionsPropsPlugin::isIrregular(mode_t permissions, bool isDir, bool isLink) { if (isLink) { // links are always ok return false; } mode_t p = permissions; if (p & (S_ISUID | S_ISGID)) { // setuid/setgid -> irregular return true; } if (isDir) { p &= ~S_ISVTX; // ignore sticky on dirs // check supported flag combinations mode_t p0 = p & UniOwner; if ((p0 != 0) && (p0 != (S_IRUSR | S_IXUSR)) && (p0 != UniOwner)) { return true; } p0 = p & UniGroup; if ((p0 != 0) && (p0 != (S_IRGRP | S_IXGRP)) && (p0 != UniGroup)) { return true; } p0 = p & UniOthers; if ((p0 != 0) && (p0 != (S_IROTH | S_IXOTH)) && (p0 != UniOthers)) { return true; } return false; } if (p & S_ISVTX) { // sticky on file -> irregular return true; } // check supported flag combinations mode_t p0 = p & UniOwner; bool usrXPossible = !p0; // true if this file could be an executable if (p0 & S_IXUSR) { if ((p0 == S_IXUSR) || (p0 == (S_IWUSR | S_IXUSR))) { return true; } usrXPossible = true; } else if (p0 == S_IWUSR) { return true; } p0 = p & UniGroup; bool grpXPossible = !p0; // true if this file could be an executable if (p0 & S_IXGRP) { if ((p0 == S_IXGRP) || (p0 == (S_IWGRP | S_IXGRP))) { return true; } grpXPossible = true; } else if (p0 == S_IWGRP) { return true; } if (p0 == 0) { grpXPossible = true; } p0 = p & UniOthers; bool othXPossible = !p0; // true if this file could be an executable if (p0 & S_IXOTH) { if ((p0 == S_IXOTH) || (p0 == (S_IWOTH | S_IXOTH))) { return true; } othXPossible = true; } else if (p0 == S_IWOTH) { return true; } // check that there either all targets are executable-compatible, or none return (p & UniExec) && !(usrXPossible && grpXPossible && othXPossible); } // enables/disabled the widgets in the Access Control frame void KFilePermissionsPropsPlugin::enableAccessControls(bool enable) { d->ownerPermCombo->setEnabled(enable); d->groupPermCombo->setEnabled(enable); d->othersPermCombo->setEnabled(enable); if (d->extraCheckbox) { d->extraCheckbox->setEnabled(enable); } if (d->cbRecursive) { d->cbRecursive->setEnabled(enable); } } // updates all widgets in the Access Control frame void KFilePermissionsPropsPlugin::updateAccessControls() { setComboContent(d->ownerPermCombo, PermissionsOwner, d->permissions, d->partialPermissions); setComboContent(d->groupPermCombo, PermissionsGroup, d->permissions, d->partialPermissions); setComboContent(d->othersPermCombo, PermissionsOthers, d->permissions, d->partialPermissions); switch (d->pmode) { case PermissionsOnlyLinks: enableAccessControls(false); break; case PermissionsOnlyFiles: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18np("This file uses advanced permissions", "These files use advanced permissions.", properties->items().count()) : QString()); if (d->partialPermissions & UniExec) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & UniExec); } break; case PermissionsOnlyDirs: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); // if this is a dir, and we can change permissions, don't dis-allow // recursive, we can do that for ACL setting. if (d->cbRecursive) { d->cbRecursive->setEnabled(d->canChangePermissions && !d->isIrregular); } if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18np("This folder uses advanced permissions.", "These folders use advanced permissions.", properties->items().count()) : QString()); if (d->partialPermissions & S_ISVTX) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & S_ISVTX); } break; case PermissionsMixed: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18n("These files use advanced permissions.") : QString()); if (d->partialPermissions & S_ISVTX) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & S_ISVTX); } break; } } // gets masks for files and dirs from the Access Control frame widgets void KFilePermissionsPropsPlugin::getPermissionMasks(mode_t &andFilePermissions, mode_t &andDirPermissions, mode_t &orFilePermissions, mode_t &orDirPermissions) { andFilePermissions = mode_t(~UniSpecial); andDirPermissions = mode_t(~(S_ISUID | S_ISGID)); orFilePermissions = 0; orDirPermissions = 0; if (d->isIrregular) { return; } mode_t m = standardPermissions[d->ownerPermCombo->currentIndex()]; if (m != (mode_t) - 1) { orFilePermissions |= m & UniOwner; if ((m & UniOwner) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IRUSR | S_IWUSR); } else { andFilePermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR); if ((m & S_IRUSR) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXUSR; } } orDirPermissions |= m & UniOwner; if (m & S_IRUSR) { orDirPermissions |= S_IXUSR; } andDirPermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR); } m = standardPermissions[d->groupPermCombo->currentIndex()]; if (m != (mode_t) - 1) { orFilePermissions |= m & UniGroup; if ((m & UniGroup) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IRGRP | S_IWGRP); } else { andFilePermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP); if ((m & S_IRGRP) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXGRP; } } orDirPermissions |= m & UniGroup; if (m & S_IRGRP) { orDirPermissions |= S_IXGRP; } andDirPermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP); } m = d->othersPermCombo->currentIndex() >= 0 ? standardPermissions[d->othersPermCombo->currentIndex()] : (mode_t) - 1; if (m != (mode_t) - 1) { orFilePermissions |= m & UniOthers; if ((m & UniOthers) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IROTH | S_IWOTH); } else { andFilePermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH); if ((m & S_IROTH) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXOTH; } } orDirPermissions |= m & UniOthers; if (m & S_IROTH) { orDirPermissions |= S_IXOTH; } andDirPermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH); } if (((d->pmode == PermissionsMixed) || (d->pmode == PermissionsOnlyDirs)) && (d->extraCheckbox->checkState() != Qt::PartiallyChecked)) { andDirPermissions &= ~S_ISVTX; if (d->extraCheckbox->checkState() == Qt::Checked) { orDirPermissions |= S_ISVTX; } } } void KFilePermissionsPropsPlugin::applyChanges() { mode_t orFilePermissions; mode_t orDirPermissions; mode_t andFilePermissions; mode_t andDirPermissions; if (!d->canChangePermissions) { return; } if (!d->isIrregular) getPermissionMasks(andFilePermissions, andDirPermissions, orFilePermissions, orDirPermissions); else { orFilePermissions = d->permissions; andFilePermissions = d->partialPermissions; orDirPermissions = d->permissions; andDirPermissions = d->partialPermissions; } QString owner, group; if (d->usrEdit) { owner = d->usrEdit->text(); } if (d->grpEdit) { group = d->grpEdit->text(); } else if (d->grpCombo) { group = d->grpCombo->currentText(); } if (owner == d->strOwner) { owner.clear(); // no change } if (group == d->strGroup) { group.clear(); } bool recursive = d->cbRecursive && d->cbRecursive->isChecked(); bool permissionChange = false; KFileItemList files, dirs; const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (; it != kend; ++it) { if ((*it).isDir()) { dirs.append(*it); if ((*it).permissions() != (((*it).permissions() & andDirPermissions) | orDirPermissions)) { permissionChange = true; } } else if ((*it).isFile()) { files.append(*it); if ((*it).permissions() != (((*it).permissions() & andFilePermissions) | orFilePermissions)) { permissionChange = true; } } } const bool ACLChange = (d->extendedACL != properties->item().ACL()); const bool defaultACLChange = (d->defaultACL != properties->item().defaultACL()); if (owner.isEmpty() && group.isEmpty() && !recursive && !permissionChange && !ACLChange && !defaultACLChange) { return; } KIO::Job *job; if (!files.isEmpty()) { job = KIO::chmod(files, orFilePermissions, ~andFilePermissions, owner, group, false); if (ACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("ACL_STRING"), d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE")); } if (defaultACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE")); } connect(job, &KJob::result, this, &KFilePermissionsPropsPlugin::slotChmodResult); QEventLoop eventLoop; connect(this, &KFilePermissionsPropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } if (!dirs.isEmpty()) { job = KIO::chmod(dirs, orDirPermissions, ~andDirPermissions, owner, group, recursive); if (ACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("ACL_STRING"), d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE")); } if (defaultACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE")); } connect(job, &KJob::result, this, &KFilePermissionsPropsPlugin::slotChmodResult); QEventLoop eventLoop; connect(this, &KFilePermissionsPropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } } void KFilePermissionsPropsPlugin::slotChmodResult(KJob *job) { // qDebug() << "KFilePermissionsPropsPlugin::slotChmodResult"; if (job->error()) { job->uiDelegate()->showErrorMessage(); } // allow apply() to return emit leaveModality(); } class KChecksumsPlugin::KChecksumsPluginPrivate { public: KChecksumsPluginPrivate() { } ~KChecksumsPluginPrivate() { } QWidget m_widget; Ui::ChecksumsWidget m_ui; QFileSystemWatcher fileWatcher; QString m_md5; QString m_sha1; QString m_sha256; }; KChecksumsPlugin::KChecksumsPlugin(KPropertiesDialog *dialog) : KPropertiesDialogPlugin(dialog), d(new KChecksumsPluginPrivate) { d->m_ui.setupUi(&d->m_widget); properties->addPage(&d->m_widget, i18nc("@title:tab", "C&hecksums")); d->m_ui.md5CopyButton->hide(); d->m_ui.sha1CopyButton->hide(); d->m_ui.sha256CopyButton->hide(); connect(d->m_ui.lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) { slotVerifyChecksum(text.toLower()); }); connect(d->m_ui.md5Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowMd5); connect(d->m_ui.sha1Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha1); connect(d->m_ui.sha256Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha256); d->fileWatcher.addPath(properties->item().localPath()); connect(&d->fileWatcher, &QFileSystemWatcher::fileChanged, this, &KChecksumsPlugin::slotInvalidateCache); auto clipboard = QApplication::clipboard(); connect(d->m_ui.md5CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_md5); }); connect(d->m_ui.sha1CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_sha1); }); connect(d->m_ui.sha256CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_sha256); }); connect(d->m_ui.pasteButton, &QPushButton::clicked, this, [=]() { d->m_ui.lineEdit->setText(clipboard->text()); }); setDefaultState(); } KChecksumsPlugin::~KChecksumsPlugin() { delete d; } bool KChecksumsPlugin::supports(const KFileItemList &items) { if (items.count() != 1) { return false; } const KFileItem item = items.first(); return item.isFile() && !item.localPath().isEmpty() && item.isReadable() && !item.isDesktopFile() && !item.isLink(); } void KChecksumsPlugin::slotInvalidateCache() { d->m_md5 = QString(); d->m_sha1 = QString(); d->m_sha256 = QString(); } void KChecksumsPlugin::slotShowMd5() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.md5Button, label); d->m_ui.md5Button->hide(); showChecksum(QCryptographicHash::Md5, label, d->m_ui.md5CopyButton); } void KChecksumsPlugin::slotShowSha1() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha1Button, label); d->m_ui.sha1Button->hide(); showChecksum(QCryptographicHash::Sha1, label, d->m_ui.sha1CopyButton); } void KChecksumsPlugin::slotShowSha256() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha256Button, label); d->m_ui.sha256Button->hide(); showChecksum(QCryptographicHash::Sha256, label, d->m_ui.sha256CopyButton); } void KChecksumsPlugin::slotVerifyChecksum(const QString &input) { auto algorithm = detectAlgorithm(input); // Input is not a supported hash algorithm. if (algorithm == QCryptographicHash::Md4) { if (input.isEmpty()) { setDefaultState(); } else { setInvalidChecksumState(); } return; } const QString checksum = cachedChecksum(algorithm); // Checksum already in cache. if (!checksum.isEmpty()) { const bool isMatch = (checksum == input); if (isMatch) { setMatchState(); } else { setMismatchState(); } return; } // Calculate checksum in another thread. auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [=]() { const QString checksum = futureWatcher->result(); futureWatcher->deleteLater(); cacheChecksum(checksum, algorithm); switch (algorithm) { case QCryptographicHash::Md5: slotShowMd5(); break; case QCryptographicHash::Sha1: slotShowSha1(); break; case QCryptographicHash::Sha256: slotShowSha256(); break; default: break; } const bool isMatch = (checksum == input); if (isMatch) { setMatchState(); } else { setMismatchState(); } }); // Notify the user about the background computation. setVerifyState(); auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath()); futureWatcher->setFuture(future); } bool KChecksumsPlugin::isMd5(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{32}$")); return regex.match(input).hasMatch(); } bool KChecksumsPlugin::isSha1(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{40}$")); return regex.match(input).hasMatch(); } bool KChecksumsPlugin::isSha256(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{64}$")); return regex.match(input).hasMatch(); } QString KChecksumsPlugin::computeChecksum(QCryptographicHash::Algorithm algorithm, const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return QString(); } QCryptographicHash hash(algorithm); hash.addData(&file); return QString::fromLatin1(hash.result().toHex()); } QCryptographicHash::Algorithm KChecksumsPlugin::detectAlgorithm(const QString &input) { if (isMd5(input)) { return QCryptographicHash::Md5; } if (isSha1(input)) { return QCryptographicHash::Sha1; } if (isSha256(input)) { return QCryptographicHash::Sha256; } // Md4 used as negative error code. return QCryptographicHash::Md4; } void KChecksumsPlugin::setDefaultState() { QColor defaultColor = d->m_widget.palette().color(QPalette::Base); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, defaultColor); d->m_ui.feedbackLabel->hide(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(QString()); } void KChecksumsPlugin::setInvalidChecksumState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, warningColor); d->m_ui.feedbackLabel->setText(i18n("Invalid checksum.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The given input is not a valid MD5, SHA1 or SHA256 checksum.")); } void KChecksumsPlugin::setMatchState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor positiveColor = colorScheme.background(KColorScheme::PositiveBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, positiveColor); d->m_ui.feedbackLabel->setText(i18n("Checksums match.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum match.")); } void KChecksumsPlugin::setMismatchState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, warningColor); d->m_ui.feedbackLabel->setText(i18n("

Checksums do not match.

" "This may be due to a faulty download. Try re-downloading the file.
" "If the verification still fails, contact the source of the file.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum differ.")); } void KChecksumsPlugin::setVerifyState() { // Users can paste a checksum at any time, so reset to default. setDefaultState(); d->m_ui.feedbackLabel->setText(i18nc("notify the user about a computation in the background", "Verifying checksum...")); d->m_ui.feedbackLabel->show(); } void KChecksumsPlugin::showChecksum(QCryptographicHash::Algorithm algorithm, QLabel *label, QPushButton *copyButton) { const QString checksum = cachedChecksum(algorithm); // Checksum in cache, nothing else to do. if (!checksum.isEmpty()) { label->setText(checksum); return; } // Calculate checksum in another thread. auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [=]() { const QString checksum = futureWatcher->result(); futureWatcher->deleteLater(); label->setText(checksum); cacheChecksum(checksum, algorithm); copyButton->show(); }); auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath()); futureWatcher->setFuture(future); } QString KChecksumsPlugin::cachedChecksum(QCryptographicHash::Algorithm algorithm) const { switch (algorithm) { case QCryptographicHash::Md5: return d->m_md5; case QCryptographicHash::Sha1: return d->m_sha1; case QCryptographicHash::Sha256: return d->m_sha256; default: break; } return QString(); } void KChecksumsPlugin::cacheChecksum(const QString &checksum, QCryptographicHash::Algorithm algorithm) { switch (algorithm) { case QCryptographicHash::Md5: d->m_md5 = checksum; break; case QCryptographicHash::Sha1: d->m_sha1 = checksum; break; case QCryptographicHash::Sha256: d->m_sha256 = checksum; break; default: return; } } class KUrlPropsPlugin::KUrlPropsPluginPrivate { public: KUrlPropsPluginPrivate() { } ~KUrlPropsPluginPrivate() { } QFrame *m_frame; KUrlRequester *URLEdit; QString URLStr; bool fileNameReadOnly = false; }; KUrlPropsPlugin::KUrlPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KUrlPropsPluginPrivate) { d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("U&RL")); QVBoxLayout *layout = new QVBoxLayout(d->m_frame); layout->setContentsMargins(0, 0, 0, 0); QLabel *l; l = new QLabel(d->m_frame); l->setObjectName(QStringLiteral("Label_1")); l->setText(i18n("URL:")); layout->addWidget(l, Qt::AlignRight); d->URLEdit = new KUrlRequester(d->m_frame); layout->addWidget(d->URLEdit); KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); QUrl url = job->mostLocalUrl(); if (url.isLocalFile()) { QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); KDesktopFile config(path); const KConfigGroup dg = config.desktopGroup(); d->URLStr = dg.readPathEntry("URL", QString()); if (!d->URLStr.isEmpty()) { d->URLEdit->setUrl(QUrl(d->URLStr)); } } connect(d->URLEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed); layout->addStretch(1); } KUrlPropsPlugin::~KUrlPropsPlugin() { delete d; } void KUrlPropsPlugin::setFileNameReadOnly(bool ro) { d->fileNameReadOnly = ro; } // QString KUrlPropsPlugin::tabName () const // { // return i18n ("U&RL"); // } bool KUrlPropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasLinkType(); } void KUrlPropsPlugin::applyChanges() { KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { KMessageBox::sorry(nullptr, i18n("Could not save properties. Only entries on local file systems are supported.")); return; } QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have " "sufficient access to write to %1.", path)); return; } f.close(); KDesktopFile config(path); KConfigGroup dg = config.desktopGroup(); dg.writeEntry("Type", QStringLiteral("Link")); dg.writePathEntry("URL", d->URLEdit->url().toString()); // Users can't create a Link .desktop file with a Name field, // but distributions can. Update the Name field in that case, // if the file name could have been changed. if (!d->fileNameReadOnly && dg.hasKey("Name")) { const QString nameStr = nameFromFileName(properties->url().fileName()); dg.writeEntry("Name", nameStr); dg.writeEntry("Name", nameStr, KConfigBase::Persistent | KConfigBase::Localized); } } /* ---------------------------------------------------- * * KDevicePropsPlugin * * -------------------------------------------------- */ class KDevicePropsPlugin::KDevicePropsPluginPrivate { public: KDevicePropsPluginPrivate() { } ~KDevicePropsPluginPrivate() { } bool isMounted() const { const QString dev = device->currentText(); return !dev.isEmpty() && KMountPoint::currentMountPoints().findByDevice(dev); } QFrame *m_frame; QStringList mountpointlist; QLabel *m_freeSpaceText; QLabel *m_freeSpaceLabel; QProgressBar *m_freeSpaceBar; KComboBox *device; QLabel *mountpoint; QCheckBox *readonly; QStringList m_devicelist; }; KDevicePropsPlugin::KDevicePropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KDevicePropsPluginPrivate) { d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("De&vice")); QStringList devices; const KMountPoint::List mountPoints = KMountPoint::possibleMountPoints(); - for (KMountPoint::List::ConstIterator it = mountPoints.begin(); - it != mountPoints.end(); ++it) { - const KMountPoint::Ptr mp = (*it); + for (const KMountPoint::Ptr &mp : mountPoints) { QString mountPoint = mp->mountPoint(); QString device = mp->mountedFrom(); // qDebug()<<"mountPoint :"<mountType() :"<mountType(); if ((mountPoint != QLatin1String("-")) && (mountPoint != QLatin1String("none")) && !mountPoint.isEmpty() && device != QLatin1String("none")) { devices.append(device + QLatin1String(" (") + mountPoint + QLatin1Char(')')); d->m_devicelist.append(device); d->mountpointlist.append(mountPoint); } } QGridLayout *layout = new QGridLayout(d->m_frame); layout->setContentsMargins(0, 0, 0, 0); layout->setColumnStretch(1, 1); QLabel *label; label = new QLabel(d->m_frame); label->setText(devices.isEmpty() ? i18n("Device (/dev/fd0):") : // old style i18n("Device:")); // new style (combobox) layout->addWidget(label, 0, 0, Qt::AlignRight); d->device = new KComboBox(d->m_frame); d->device->setObjectName(QStringLiteral("ComboBox_device")); d->device->setEditable(true); d->device->addItems(devices); layout->addWidget(d->device, 0, 1); connect(d->device, QOverload::of(&QComboBox::activated), this, &KDevicePropsPlugin::slotActivated); d->readonly = new QCheckBox(d->m_frame); d->readonly->setObjectName(QStringLiteral("CheckBox_readonly")); d->readonly->setText(i18n("Read only")); layout->addWidget(d->readonly, 1, 1); label = new QLabel(d->m_frame); label->setText(i18n("File system:")); layout->addWidget(label, 2, 0, Qt::AlignRight); QLabel *fileSystem = new QLabel(d->m_frame); layout->addWidget(fileSystem, 2, 1); label = new QLabel(d->m_frame); label->setText(devices.isEmpty() ? i18n("Mount point (/mnt/floppy):") : // old style i18n("Mount point:")); // new style (combobox) layout->addWidget(label, 3, 0, Qt::AlignRight); d->mountpoint = new QLabel(d->m_frame); d->mountpoint->setObjectName(QStringLiteral("LineEdit_mountpoint")); layout->addWidget(d->mountpoint, 3, 1); // show disk free d->m_freeSpaceText = new QLabel(i18nc("Amount of used and available space on this device or partition", "Free space:"), d->m_frame); layout->addWidget(d->m_freeSpaceText, 4, 0, Qt::AlignRight); d->m_freeSpaceLabel = new QLabel(d->m_frame); layout->addWidget(d->m_freeSpaceLabel, 4, 1); d->m_freeSpaceBar = new QProgressBar(d->m_frame); d->m_freeSpaceBar->setObjectName(QStringLiteral("freeSpaceBar")); layout->addWidget(d->m_freeSpaceBar, 5, 0, 1, 2); // we show it in the slot when we know the values d->m_freeSpaceText->hide(); d->m_freeSpaceLabel->hide(); d->m_freeSpaceBar->hide(); KSeparator *sep = new KSeparator(Qt::Horizontal, d->m_frame); layout->addWidget(sep, 6, 0, 1, 2); layout->setRowStretch(7, 1); KIO::StatJob *job = KIO::mostLocalUrl(_props->url()); KJobWidgets::setWindow(job, _props); job->exec(); QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); const KDesktopFile _config(path); const KConfigGroup config = _config.desktopGroup(); QString deviceStr = config.readEntry("Dev"); QString mountPointStr = config.readEntry("MountPoint"); bool ro = config.readEntry("ReadOnly", false); fileSystem->setText(config.readEntry("FSType")); d->device->setEditText(deviceStr); if (!deviceStr.isEmpty()) { // Set default options for this device (first matching entry) int index = d->m_devicelist.indexOf(deviceStr); if (index != -1) { //qDebug() << "found it" << index; slotActivated(index); } } if (!mountPointStr.isEmpty()) { d->mountpoint->setText(mountPointStr); updateInfo(); } d->readonly->setChecked(ro); connect(d->device, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); connect(d->device, &QComboBox::currentTextChanged, this, &KPropertiesDialogPlugin::changed); connect(d->readonly, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(d->device, &QComboBox::currentTextChanged, this, &KDevicePropsPlugin::slotDeviceChanged); } KDevicePropsPlugin::~KDevicePropsPlugin() { delete d; } // QString KDevicePropsPlugin::tabName () const // { // return i18n ("De&vice"); // } void KDevicePropsPlugin::updateInfo() { // we show it in the slot when we know the values d->m_freeSpaceText->hide(); d->m_freeSpaceLabel->hide(); d->m_freeSpaceBar->hide(); if (!d->mountpoint->text().isEmpty() && d->isMounted()) { KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(d->mountpoint->text()); slotFoundMountPoint(info.mountPoint(), info.size() / 1024, info.used() / 1024, info.available() / 1024); } } void KDevicePropsPlugin::slotActivated(int index) { // index can be more than the number of known devices, when the user types // a "custom" device. if (index < d->m_devicelist.count()) { // Update mountpoint so that it matches the device that was selected in the combo d->device->setEditText(d->m_devicelist[index]); d->mountpoint->setText(d->mountpointlist[index]); } updateInfo(); } void KDevicePropsPlugin::slotDeviceChanged() { // Update mountpoint so that it matches the typed device int index = d->m_devicelist.indexOf(d->device->currentText()); if (index != -1) { d->mountpoint->setText(d->mountpointlist[index]); } else { d->mountpoint->setText(QString()); } updateInfo(); } void KDevicePropsPlugin::slotFoundMountPoint(const QString &, quint64 kibSize, quint64 /*kibUsed*/, quint64 kibAvail) { d->m_freeSpaceText->show(); d->m_freeSpaceLabel->show(); const int percUsed = kibSize != 0 ? (100 - (int)(100.0 * kibAvail / kibSize)) : 100; d->m_freeSpaceLabel->setText( i18nc("Available space out of total partition size (percent used)", "%1 free of %2 (%3% used)", KIO::convertSizeFromKiB(kibAvail), KIO::convertSizeFromKiB(kibSize), percUsed)); d->m_freeSpaceBar->setRange(0, 100); d->m_freeSpaceBar->setValue(percUsed); d->m_freeSpaceBar->show(); } bool KDevicePropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasDeviceType(); } void KDevicePropsPlugin::applyChanges() { KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } const QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have sufficient " "access to write to %1.", path)); return; } f.close(); KDesktopFile _config(path); KConfigGroup config = _config.desktopGroup(); config.writeEntry("Type", QStringLiteral("FSDevice")); config.writeEntry("Dev", d->device->currentText()); config.writeEntry("MountPoint", d->mountpoint->text()); config.writeEntry("ReadOnly", d->readonly->isChecked()); config.sync(); } /* ---------------------------------------------------- * * KDesktopPropsPlugin * * -------------------------------------------------- */ class KDesktopPropsPlugin::KDesktopPropsPluginPrivate { public: KDesktopPropsPluginPrivate() : w(new Ui_KPropertiesDesktopBase) , m_frame(new QFrame()) { } ~KDesktopPropsPluginPrivate() { delete w; } Ui_KPropertiesDesktopBase *w; QWidget *m_frame; QString m_origCommandStr; QString m_terminalOptionStr; QString m_suidUserStr; QString m_dbusStartupType; QString m_dbusServiceName; QString m_origDesktopFile; bool m_terminalBool; bool m_suidBool; bool m_hasDiscreteGpuBool; bool m_runOnDiscreteGpuBool; bool m_startupBool; }; KDesktopPropsPlugin::KDesktopPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KDesktopPropsPluginPrivate) { QMimeDatabase db; d->w->setupUi(d->m_frame); properties->addPage(d->m_frame, i18n("&Application")); bool bKDesktopMode = properties->url().scheme() == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop"); if (bKDesktopMode) { // Hide Name entry d->w->nameEdit->hide(); d->w->nameLabel->hide(); } d->w->pathEdit->setMode(KFile::Directory | KFile::LocalOnly); d->w->pathEdit->lineEdit()->setAcceptDrops(false); connect(d->w->nameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->genNameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->commentEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->commandEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->pathEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->browseButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotBrowseExec); connect(d->w->addFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAddFiletype); connect(d->w->delFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotDelFiletype); connect(d->w->advancedButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAdvanced); enum DiscreteGpuCheck { NotChecked, Present, Absent }; static DiscreteGpuCheck s_gpuCheck = NotChecked; if (s_gpuCheck == NotChecked) { // Check whether we have a discrete gpu bool hasDiscreteGpu = false; QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/kde/Solid/PowerManagement"), QStringLiteral("org.kde.Solid.PowerManagement"), QDBusConnection::sessionBus()); if (iface.isValid()) { QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); if (reply.isValid()) { hasDiscreteGpu = reply.value(); } } s_gpuCheck = hasDiscreteGpu ? Present : Absent; } d->m_hasDiscreteGpuBool = s_gpuCheck == Present; // now populate the page KIO::StatJob *job = KIO::mostLocalUrl(_props->url()); KJobWidgets::setWindow(job, _props); job->exec(); QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } d->m_origDesktopFile = url.toLocalFile(); QFile f(d->m_origDesktopFile); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); KDesktopFile _config(d->m_origDesktopFile); KConfigGroup config = _config.desktopGroup(); QString nameStr = _config.readName(); QString genNameStr = _config.readGenericName(); QString commentStr = _config.readComment(); QString commandStr = config.readEntry("Exec", QString()); d->m_origCommandStr = commandStr; QString pathStr = config.readEntry("Path", QString()); // not readPathEntry, see kservice.cpp d->m_terminalBool = config.readEntry("Terminal", false); d->m_terminalOptionStr = config.readEntry("TerminalOptions"); d->m_suidBool = config.readEntry("X-KDE-SubstituteUID", false); d->m_suidUserStr = config.readEntry("X-KDE-Username"); if (d->m_hasDiscreteGpuBool) { d->m_runOnDiscreteGpuBool = config.readEntry("X-KDE-RunOnDiscreteGpu", false); } if (config.hasKey("StartupNotify")) { d->m_startupBool = config.readEntry("StartupNotify", true); } else { d->m_startupBool = config.readEntry("X-KDE-StartupNotify", true); } d->m_dbusStartupType = config.readEntry("X-DBUS-StartupType").toLower(); // ### should there be a GUI for this setting? // At least we're copying it over to the local file, to avoid side effects (#157853) d->m_dbusServiceName = config.readEntry("X-DBUS-ServiceName"); const QStringList mimeTypes = config.readXdgListEntry("MimeType"); if (nameStr.isEmpty() || bKDesktopMode) { // We'll use the file name if no name is specified // because we _need_ a Name for a valid file. // But let's do it in apply, not here, so that we pick up the right name. setDirty(); } if (!bKDesktopMode) { d->w->nameEdit->setText(nameStr); } d->w->genNameEdit->setText(genNameStr); d->w->commentEdit->setText(commentStr); d->w->commandEdit->setText(commandStr); d->w->pathEdit->lineEdit()->setText(pathStr); // was: d->w->filetypeList->setFullWidth(true); // d->w->filetypeList->header()->setStretchEnabled(true, d->w->filetypeList->columns()-1); for (QStringList::ConstIterator it = mimeTypes.begin(); it != mimeTypes.end();) { QMimeType p = db.mimeTypeForName(*it); ++it; QString preference; if (it != mimeTypes.end()) { bool numeric; (*it).toInt(&numeric); if (numeric) { preference = *it; ++it; } } if (p.isValid()) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, p.name()); item->setText(1, p.comment()); item->setText(2, preference); d->w->filetypeList->addTopLevelItem(item); } } d->w->filetypeList->resizeColumnToContents(0); } KDesktopPropsPlugin::~KDesktopPropsPlugin() { delete d; } void KDesktopPropsPlugin::slotAddFiletype() { QMimeDatabase db; KMimeTypeChooserDialog dlg(i18n("Add File Type for %1", properties->url().fileName()), i18n("Select one or more file types to add:"), QStringList(), // no preselected mimetypes QString(), QStringList(), KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns, d->m_frame); if (dlg.exec() == QDialog::Accepted) { foreach (const QString &mimetype, dlg.chooser()->mimeTypes()) { QMimeType p = db.mimeTypeForName(mimetype); if (!p.isValid()) { continue; } bool found = false; int count = d->w->filetypeList->topLevelItemCount(); for (int i = 0; !found && i < count; ++i) { if (d->w->filetypeList->topLevelItem(i)->text(0) == mimetype) { found = true; } } if (!found) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, p.name()); item->setText(1, p.comment()); d->w->filetypeList->addTopLevelItem(item); } d->w->filetypeList->resizeColumnToContents(0); } } emit changed(); } void KDesktopPropsPlugin::slotDelFiletype() { QTreeWidgetItem *cur = d->w->filetypeList->currentItem(); if (cur) { delete cur; emit changed(); } } void KDesktopPropsPlugin::checkCommandChanged() { if (KIO::DesktopExecParser::executableName(d->w->commandEdit->text()) != KIO::DesktopExecParser::executableName(d->m_origCommandStr)) { d->m_origCommandStr = d->w->commandEdit->text(); d->m_dbusStartupType.clear(); // Reset d->m_dbusServiceName.clear(); } } void KDesktopPropsPlugin::applyChanges() { // qDebug(); KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { KMessageBox::sorry(nullptr, i18n("Could not save properties. Only entries on local file systems are supported.")); return; } const QString path(url.toLocalFile()); // make sure the directory exists QDir().mkpath(QFileInfo(path).absolutePath()); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have " "sufficient access to write to %1.", path)); return; } f.close(); // If the command is changed we reset certain settings that are strongly // coupled to the command. checkCommandChanged(); KDesktopFile origConfig(d->m_origDesktopFile); QScopedPointer _config(origConfig.copyTo(path)); KConfigGroup config = _config->desktopGroup(); config.writeEntry("Type", QStringLiteral("Application")); config.writeEntry("Comment", d->w->commentEdit->text()); config.writeEntry("Comment", d->w->commentEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat config.writeEntry("GenericName", d->w->genNameEdit->text()); config.writeEntry("GenericName", d->w->genNameEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat config.writeEntry("Exec", d->w->commandEdit->text()); config.writeEntry("Path", d->w->pathEdit->lineEdit()->text()); // not writePathEntry, see kservice.cpp // Write mimeTypes QStringList mimeTypes; int count = d->w->filetypeList->topLevelItemCount(); for (int i = 0; i < count; ++i) { QTreeWidgetItem *item = d->w->filetypeList->topLevelItem(i); QString preference = item->text(2); mimeTypes.append(item->text(0)); if (!preference.isEmpty()) { mimeTypes.append(preference); } } // qDebug() << mimeTypes; config.writeXdgListEntry("MimeType", mimeTypes); if (!d->w->nameEdit->isHidden()) { QString nameStr = d->w->nameEdit->text(); config.writeEntry("Name", nameStr); config.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized); } config.writeEntry("Terminal", d->m_terminalBool); config.writeEntry("TerminalOptions", d->m_terminalOptionStr); config.writeEntry("X-KDE-SubstituteUID", d->m_suidBool); config.writeEntry("X-KDE-Username", d->m_suidUserStr); if (d->m_hasDiscreteGpuBool) { config.writeEntry("X-KDE-RunOnDiscreteGpu", d->m_runOnDiscreteGpuBool); } config.writeEntry("StartupNotify", d->m_startupBool); config.writeEntry("X-DBUS-StartupType", d->m_dbusStartupType); config.writeEntry("X-DBUS-ServiceName", d->m_dbusServiceName); config.sync(); // KSycoca update needed? bool updateNeeded = !relativeAppsLocation(path).isEmpty(); if (updateNeeded) { KBuildSycocaProgressDialog::rebuildKSycoca(d->m_frame); } } void KDesktopPropsPlugin::slotBrowseExec() { QUrl f = QFileDialog::getOpenFileUrl(d->m_frame); if (f.isEmpty()) { return; } if (!f.isLocalFile()) { KMessageBox::sorry(d->m_frame, i18n("Only executables on local file systems are supported.")); return; } QString path = f.toLocalFile(); path = KShell::quoteArg(path); d->w->commandEdit->setText(path); } void KDesktopPropsPlugin::slotAdvanced() { QDialog dlg(d->m_frame); dlg.setObjectName(QStringLiteral("KPropertiesDesktopAdv")); dlg.setModal(true); dlg.setWindowTitle(i18n("Advanced Options for %1", properties->url().fileName())); Ui_KPropertiesDesktopAdvBase w; QWidget *mainWidget = new QWidget(&dlg); w.setupUi(mainWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(&dlg); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(mainWidget); layout->addWidget(buttonBox); dlg.setLayout(layout); // If the command is changed we reset certain settings that are strongly // coupled to the command. checkCommandChanged(); // check to see if we use konsole if not do not add the nocloseonexit // because we don't know how to do this on other terminal applications KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); bool terminalCloseBool = false; if (preferredTerminal == QLatin1String("konsole")) { terminalCloseBool = d->m_terminalOptionStr.contains(QLatin1String("--noclose")); w.terminalCloseCheck->setChecked(terminalCloseBool); d->m_terminalOptionStr.remove(QStringLiteral("--noclose")); } else { w.terminalCloseCheck->hide(); } w.terminalCheck->setChecked(d->m_terminalBool); w.terminalEdit->setText(d->m_terminalOptionStr); w.terminalCloseCheck->setEnabled(d->m_terminalBool); w.terminalEdit->setEnabled(d->m_terminalBool); w.terminalEditLabel->setEnabled(d->m_terminalBool); w.suidCheck->setChecked(d->m_suidBool); w.suidEdit->setText(d->m_suidUserStr); w.suidEdit->setEnabled(d->m_suidBool); w.suidEditLabel->setEnabled(d->m_suidBool); if (d->m_hasDiscreteGpuBool) { w.discreteGpuCheck->setChecked(d->m_runOnDiscreteGpuBool); } else { w.discreteGpuGroupBox->hide(); } w.startupInfoCheck->setChecked(d->m_startupBool); if (d->m_dbusStartupType == QLatin1String("unique")) { w.dbusCombo->setCurrentIndex(2); } else if (d->m_dbusStartupType == QLatin1String("multi")) { w.dbusCombo->setCurrentIndex(1); } else if (d->m_dbusStartupType == QLatin1String("wait")) { w.dbusCombo->setCurrentIndex(3); } else { w.dbusCombo->setCurrentIndex(0); } // Provide username completion up to 1000 users. const int maxEntries = 1000; QStringList userNames = KUser::allUserNames(maxEntries); if (userNames.size() < maxEntries) { KCompletion *kcom = new KCompletion; kcom->setOrder(KCompletion::Sorted); w.suidEdit->setCompletionObject(kcom, true); w.suidEdit->setAutoDeleteCompletionObject(true); w.suidEdit->setCompletionMode(KCompletion::CompletionAuto); kcom->setItems(userNames); } connect(w.terminalEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(w.terminalCloseCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.terminalCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.suidCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.suidEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(w.discreteGpuCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.startupInfoCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.dbusCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); if (dlg.exec() == QDialog::Accepted) { d->m_terminalOptionStr = w.terminalEdit->text().trimmed(); d->m_terminalBool = w.terminalCheck->isChecked(); d->m_suidBool = w.suidCheck->isChecked(); d->m_suidUserStr = w.suidEdit->text().trimmed(); if (d->m_hasDiscreteGpuBool) { d->m_runOnDiscreteGpuBool = w.discreteGpuCheck->isChecked(); } d->m_startupBool = w.startupInfoCheck->isChecked(); if (w.terminalCloseCheck->isChecked()) { d->m_terminalOptionStr.append(QLatin1String(" --noclose")); } switch (w.dbusCombo->currentIndex()) { case 1: d->m_dbusStartupType = QStringLiteral("multi"); break; case 2: d->m_dbusStartupType = QStringLiteral("unique"); break; case 3: d->m_dbusStartupType = QStringLiteral("wait"); break; default: d->m_dbusStartupType = QStringLiteral("none"); break; } } } bool KDesktopPropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasApplicationType() && KAuthorized::authorize(QStringLiteral("run_desktop_files")) && KAuthorized::authorize(QStringLiteral("shell_access")); } #include "moc_kpropertiesdialog.cpp" #include "moc_kpropertiesdialog_p.cpp" diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp index 3c445f24..3ccb6979 100644 --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -1,1797 +1,1796 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006 David Faure Copyright (C) 2009 Michael Pyne 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 "krun.h" #include "krun_p.h" #include // HAVE_X11 #include "kio_widgets_debug.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 "kio/job.h" #include "kio/global.h" #include "kio/scheduler.h" #include "kopenwithdialog.h" #include "krecentdocument.h" #include "kdesktopfileactions.h" #include "executablefileopendialog_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif #include KRun::KRunPrivate::KRunPrivate(KRun *parent) : q(parent), m_showingDialog(false) { } void KRun::KRunPrivate::startTimer() { m_timer->start(0); } // --------------------------------------------------------------------------- static QString schemeHandler(const QString &protocol) { // We have up to two sources of data, for protocols not handled by kioslaves (so called "helper") : // 1) the exec line of the .protocol file, if there's one // 2) the application associated with x-scheme-handler/ if there's one // If both exist, then: // A) if the .protocol file says "launch an application", then the new-style handler-app has priority // B) but if the .protocol file is for a kioslave (e.g. kio_http) then this has priority over // firefox or chromium saying x-scheme-handler/http. Gnome people want to send all HTTP urls // to a webbrowser, but we want mimetype-determination-in-calling-application by default // (the user can configure a BrowserApplication though) const KService::Ptr service = KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + protocol); if (service) { return service->exec(); // for helper protocols, the handler app has priority over the hardcoded one (see A above) } Q_ASSERT(KProtocolInfo::isHelperProtocol(protocol)); return KProtocolInfo::exec(protocol); } static bool checkNeedPortalSupport() { return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP"); } static qint64 runProcessRunner(KProcessRunner *processRunner, QWidget *widget) { QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); QObject::connect(processRunner, &KProcessRunner::error, receiver, [widget](const QString &errorString) { QEventLoopLocker locker; KMessageBox::sorry(widget, errorString); }); return processRunner->pid(); } // --------------------------------------------------------------------------- // Helper function that returns whether a file has the execute bit set or not. static bool hasExecuteBit(const QString &fileName) { QFileInfo file(fileName); return file.isExecutable(); } bool KRun::isExecutableFile(const QUrl &url, const QString &mimetype) { if (!url.isLocalFile()) { return false; } // While isExecutable performs similar check to this one, some users depend on // this method not returning true for application/x-desktop QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimetype); if (!mimeType.inherits(QStringLiteral("application/x-executable")) #ifdef Q_OS_WIN && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")) #endif && !mimeType.inherits(QStringLiteral("application/x-executable-script")) && !mimeType.inherits(QStringLiteral("application/x-sharedlib"))) { return false; } if (!hasExecuteBit(url.toLocalFile())) { return false; } return true; } void KRun::handleInitError(int kioErrorCode, const QString &errorMsg) { Q_UNUSED(kioErrorCode); d->m_showingDialog = true; KMessageBox::error(d->m_window, errorMsg); d->m_showingDialog = false; } void KRun::handleError(KJob *job) { Q_ASSERT(job); if (job) { d->m_showingDialog = true; job->uiDelegate()->showErrorMessage(); d->m_showingDialog = false; } } // Simple QDialog that resizes the given text edit after being shown to more // or less fit the enclosed text. class SecureMessageDialog : public QDialog { Q_OBJECT public: SecureMessageDialog(QWidget *parent) : QDialog(parent), m_textEdit(nullptr) { } void setTextEdit(QPlainTextEdit *textEdit) { m_textEdit = textEdit; } protected: void showEvent(QShowEvent *e) override { if (e->spontaneous()) { return; } // Now that we're shown, use our width to calculate a good // bounding box for the text, and resize m_textEdit appropriately. QDialog::showEvent(e); if (!m_textEdit) { return; } QSize fudge(20, 24); // About what it sounds like :-/ // Form rect with a lot of height for bounding. Use no more than // 5 lines. QRect curRect(m_textEdit->rect()); QFontMetrics metrics(fontMetrics()); curRect.setHeight(5 * metrics.lineSpacing()); curRect.setWidth(qMax(curRect.width(), 300)); // At least 300 pixels ok? QString text(m_textEdit->toPlainText()); curRect = metrics.boundingRect(curRect, Qt::TextWordWrap | Qt::TextSingleLine, text); // Scroll bars interfere. If we don't think there's enough room, enable // the vertical scrollbar however. m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); if (curRect.height() < m_textEdit->height()) { // then we've got room m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_textEdit->setMaximumHeight(curRect.height() + fudge.height()); } m_textEdit->setMinimumSize(curRect.size() + fudge); m_textEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); } private: QPlainTextEdit *m_textEdit; }; // Shows confirmation dialog whether an untrusted program should be run // or not, since it may be potentially dangerous. static int showUntrustedProgramWarning(const QString &programName, QWidget *window) { SecureMessageDialog *baseDialog = new SecureMessageDialog(window); baseDialog->setWindowTitle(i18nc("Warning about executing unknown program", "Warning")); QVBoxLayout *topLayout = new QVBoxLayout; baseDialog->setLayout(topLayout); // Dialog will have explanatory text with a disabled lineedit with the // Exec= to make it visually distinct. QWidget *baseWidget = new QWidget(baseDialog); QHBoxLayout *mainLayout = new QHBoxLayout(baseWidget); QLabel *iconLabel = new QLabel(baseWidget); QPixmap warningIcon(KIconLoader::global()->loadIcon(QStringLiteral("dialog-warning"), KIconLoader::NoGroup, KIconLoader::SizeHuge)); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(warningIcon); QVBoxLayout *contentLayout = new QVBoxLayout; QString warningMessage = i18nc("program name follows in a line edit below", "This will start the program:"); QLabel *message = new QLabel(warningMessage, baseWidget); contentLayout->addWidget(message); QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); textEdit->setPlainText(programName); textEdit->setReadOnly(true); contentLayout->addWidget(textEdit); QLabel *footerLabel = new QLabel(i18n("If you do not trust this program, click Cancel")); contentLayout->addWidget(footerLabel); contentLayout->addStretch(0); // Don't allow the text edit to expand mainLayout->addLayout(contentLayout); topLayout->addWidget(baseWidget); baseDialog->setTextEdit(textEdit); QDialogButtonBox *buttonBox = new QDialogButtonBox(baseDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::cont()); buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); QObject::connect(buttonBox, &QDialogButtonBox::accepted, baseDialog, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, baseDialog, &QDialog::reject); topLayout->addWidget(buttonBox); // Constrain maximum size. Minimum size set in // the dialog's show event. #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const QSize screenSize = QApplication::screens().at(0)->size(); #else const QSize screenSize = baseDialog->screen()->size(); #endif baseDialog->resize(screenSize.width() / 4, 50); baseDialog->setMaximumHeight(screenSize.height() / 3); baseDialog->setMaximumWidth(screenSize.width() / 10 * 8); return baseDialog->exec(); } // Helper function that attempts to set execute bit for given file. static bool setExecuteBit(const QString &fileName, QString &errorString) { QFile file(fileName); // corresponds to owner on unix, which will have to do since if the user // isn't the owner we can't change perms anyways. if (!file.setPermissions(QFile::ExeUser | file.permissions())) { errorString = file.errorString(); qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << errorString; return false; } return true; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { RunFlags flags = tempFile ? KRun::DeleteTemporaryFiles : RunFlags(); if (runExecutables) { flags |= KRun::RunExecutables; } return runUrl(url, mimetype, window, flags, suggestedFileName, asn); } #endif // This is called by foundMimeType, since it knows the mimetype of the URL bool KRun::runUrl(const QUrl &u, const QString &_mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { const QMimeDatabase db; const bool runExecutables = flags.testFlag(KRun::RunExecutables); const bool tempFile = flags.testFlag(KRun::DeleteTemporaryFiles); bool noRun = false; bool noAuth = false; if (_mimetype == QLatin1String("inode/directory-locked")) { KMessageBox::error(window, i18n("Unable to enter %1.\nYou do not have access rights to this location.", u.toDisplayString().toHtmlEscaped())); return false; } else if (_mimetype == QLatin1String("application/x-desktop")) { if (u.isLocalFile() && runExecutables) { return KDesktopFileActions::runWithStartup(u, true, asn); } } else if (isExecutable(_mimetype)) { // Check whether file is executable script const QMimeType mime = db.mimeTypeForName(_mimetype); bool isTextFile = mime.inherits(QStringLiteral("text/plain")); // Only run local files if (u.isLocalFile() && runExecutables) { if (KAuthorized::authorize(QStringLiteral("shell_access"))) { bool canRun = true; bool isFileExecutable = hasExecuteBit(u.toLocalFile()); #ifdef Q_OS_WIN // On Windows, run all executables normally const bool supportsRunningExecutable = true; #else // On non-Windows systems, this will prevent Windows executables // from being run, so that programs like Wine can handle them instead. const bool supportsRunningExecutable = !mime.inherits(QStringLiteral("application/x-ms-dos-executable")); #endif if (!supportsRunningExecutable) { // Don't run Windows executables on non-Windows platform canRun = false; } else if (!isFileExecutable && !isTextFile) { // For executables that aren't scripts and without execute bit, // show prompt asking user if he wants to run the program. canRun = false; int result = showUntrustedProgramWarning(u.fileName(), window); if (result == QDialog::Accepted) { QString errorString; if (!setExecuteBit(u.toLocalFile(), errorString)) { KMessageBox::sorry( window, i18n("Unable to make file %1 executable.\n%2.", u.toLocalFile(), errorString) ); } else { canRun = true; } } } else if (!isFileExecutable && isTextFile) { // Don't try to run scripts without execute bit, instead // open them with default application canRun = false; } if (canRun) { return (KRun::runCommand(KShell::quoteArg(u.toLocalFile()), QString(), QString(), window, asn, u.adjusted(QUrl::RemoveFilename).toLocalFile())); // just execute the url as a command // ## TODO implement deleting the file if tempFile==true } } else { // Show no permission warning noAuth = true; } } else if (!isTextFile) { // Show warning for executables that aren't scripts noRun = true; } } if (noRun) { KMessageBox::sorry(window, i18n("The file %1 is an executable program. " "For safety it will not be started.", u.toDisplayString().toHtmlEscaped())); return false; } if (noAuth) { KMessageBox::error(window, i18n("You do not have permission to run %1.", u.toDisplayString().toHtmlEscaped())); return false; } QList lst; lst.append(u); KService::Ptr offer = KMimeTypeTrader::self()->preferredService(_mimetype); if (!offer) { #ifdef Q_OS_WIN // As KDE on windows doesn't know about the windows default applications offers will be empty in nearly all cases. // So we use QDesktopServices::openUrl to let windows decide how to open the file return QDesktopServices::openUrl(u); #else // Open-with dialog // TODO : pass the mimetype as a parameter, to show it (comment field) in the dialog ! // Hmm, in fact KOpenWithDialog::setServiceType already guesses the mimetype from the first URL of the list... return displayOpenWithDialog(lst, window, tempFile, suggestedFileName, asn); #endif } return KRun::runApplication(*offer, lst, window, flags, suggestedFileName, asn); } bool KRun::displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { KMessageBox::sorry(window, i18n("You are not authorized to select an application to open this file.")); return false; } #ifdef Q_OS_WIN KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); if (cfgGroup.readEntry("Native", true)) { return KRun::KRunPrivate::displayNativeOpenWithDialog(lst, window, tempFiles, suggestedFileName, asn); } #endif KOpenWithDialog dialog(lst, QString(), QString(), window); dialog.setWindowModality(Qt::WindowModal); if (dialog.exec()) { KService::Ptr service = dialog.service(); if (!service) { //qDebug() << "No service set, running " << dialog.text(); service = KService::Ptr(new KService(QString() /*name*/, dialog.text(), QString() /*icon*/)); } const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return KRun::runApplication(*service, lst, window, flags, suggestedFileName, asn); } return false; } #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::shellQuote(QString &_str) { // Credits to Walter, says Bernd G. :) if (_str.isEmpty()) { // Don't create an explicit empty parameter return; } const QChar q = QLatin1Char('\''); _str.replace(q, QLatin1String("'\\''")).prepend(q).append(q); } #endif QStringList KRun::processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles, const QString &suggestedFileName) { KIO::DesktopExecParser parser(_service, _urls); parser.setUrlsAreTempFiles(tempFiles); parser.setSuggestedFileName(suggestedFileName); return parser.resultingArguments(); } #ifndef KIOWIDGETS_NO_DEPRECATED QString KRun::binaryName(const QString &execLine, bool removePath) { return removePath ? KIO::DesktopExecParser::executableName(execLine) : KIO::DesktopExecParser::executablePath(execLine); } #endif // This code is also used in klauncher. // TODO: move this to KProcessRunner bool KRun::checkStartupNotify(const QString & /*binName*/, const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { bool silent = false; QByteArray wmclass; if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); } else { // non-compliant app if (service) { if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant wmclass = "0"; // krazy:exclude=doublequote_chars } else { return false; // no startup notification at all } } else { #if 0 // Create startup notification even for apps for which there shouldn't be any, // just without any visual feedback. This will ensure they'll be positioned on the proper // virtual desktop, and will get user timestamp from the ASN ID. wmclass = '0'; silent = true; #else // That unfortunately doesn't work, when the launched non-compliant application // launches another one that is compliant and there is any delay inbetween (bnc:#343359) return false; #endif } } if (silent_arg) { *silent_arg = silent; } if (wmclass_arg) { *wmclass_arg = wmclass; } return true; } static qint64 runApplicationImpl(const KService::Ptr &service, const QList &_urls, QWidget *window, KRun::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { QList urlsToRun = _urls; if ((_urls.count() > 1) && !service->allowMultipleFiles()) { // We need to launch the application N times. That sucks. // We ignore the result for application 2 to N. // For the first file we launch the application in the // usual way. The reported result is based on this // application. QList::ConstIterator it = _urls.begin(); while (++it != _urls.end()) { QList singleUrl; singleUrl.append(*it); runApplicationImpl(service, singleUrl, window, flags, suggestedFileName, QByteArray()); } urlsToRun.clear(); urlsToRun.append(_urls.first()); } // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead auto windowId = WId{}; if (window) { window = window->window(); windowId = window ? window->winId() : WId{}; } auto *processRunner = new KProcessRunner(service, urlsToRun, windowId, flags, suggestedFileName, asn); return runProcessRunner(processRunner, window); } // WARNING: don't call this from DesktopExecParser, since klauncher uses that too... // TODO: make this async, see the job->exec() in there... static QList resolveURLs(const QList &_urls, const KService &_service) { // Check which protocols the application supports. // This can be a list of actual protocol names, or just KIO for KDE apps. - QStringList appSupportedProtocols = KIO::DesktopExecParser::supportedProtocols(_service); + const QStringList appSupportedProtocols = KIO::DesktopExecParser::supportedProtocols(_service); QList urls(_urls); if (!appSupportedProtocols.contains(QLatin1String("KIO"))) { - for (QList::Iterator it = urls.begin(); it != urls.end(); ++it) { - const QUrl url = *it; + for (QUrl &url : urls) { bool supported = KIO::DesktopExecParser::isProtocolInSupportedList(url, appSupportedProtocols); //qDebug() << "Looking at url=" << url << " supported=" << supported; if (!supported && KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local")) { // Maybe we can resolve to a local URL? KIO::StatJob *job = KIO::mostLocalUrl(url); if (job->exec()) { // ## nasty nested event loop! const QUrl localURL = job->mostLocalUrl(); if (localURL != url) { - *it = localURL; + url = localURL; //qDebug() << "Changed to" << localURL; } } } } } return urls; } // Helper function to make the given .desktop file executable by ensuring // that a #!/usr/bin/env xdg-open line is added if necessary and the file has // the +x bit set for the user. Returns false if either fails. static bool makeServiceFileExecutable(const QString &fileName, QString &errorString) { // Open the file and read the first two characters, check if it's // #!. If not, create a new file, prepend appropriate lines, and copy // over. QFile desktopFile(fileName); if (!desktopFile.open(QFile::ReadOnly)) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error opening service" << fileName << errorString; return false; } QByteArray header = desktopFile.peek(2); // First two chars of file if (header.size() == 0) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error inspecting service" << fileName << errorString; return false; // Some kind of error } if (header != "#!") { // Add header QSaveFile saveFile; saveFile.setFileName(fileName); if (!saveFile.open(QIODevice::WriteOnly)) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Unable to open replacement file for" << fileName << errorString; return false; } QByteArray shebang("#!/usr/bin/env xdg-open\n"); if (saveFile.write(shebang) != shebang.size()) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error occurred adding header for" << fileName << errorString; saveFile.cancelWriting(); return false; } // Now copy the one into the other and then close and reopen desktopFile QByteArray desktopData(desktopFile.readAll()); if (desktopData.isEmpty()) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Unable to read service" << fileName << errorString; saveFile.cancelWriting(); return false; } if (saveFile.write(desktopData) != desktopData.size()) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error copying service" << fileName << errorString; saveFile.cancelWriting(); return false; } desktopFile.close(); if (!saveFile.commit()) { // Figures.... errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error committing changes to service" << fileName << errorString; return false; } if (!desktopFile.open(QFile::ReadOnly)) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error re-opening service" << fileName << errorString; return false; } } // Add header return setExecuteBit(fileName, errorString); } // Helper function to make a .desktop file executable if prompted by the user. // returns true if KRun::run() should continue with execution, false if user declined // to make the file executable or we failed to make it executable. static bool makeServiceExecutable(const KService &service, QWidget *window) { if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { qCWarning(KIO_WIDGETS) << "No authorization to execute " << service.entryPath(); KMessageBox::sorry(window, i18n("You are not authorized to execute this service.")); return false; // Don't circumvent the Kiosk } // We can use KStandardDirs::findExe to resolve relative pathnames // but that gets rid of the command line arguments. QString program = QFileInfo(service.exec()).canonicalFilePath(); if (program.isEmpty()) { // e.g. due to command line arguments program = service.exec(); } int result = showUntrustedProgramWarning(program, window); if (result != QDialog::Accepted) { return false; } // Assume that service is an absolute path since we're being called (relative paths // would have been allowed unless Kiosk said no, therefore we already know where the // .desktop file is. Now add a header to it if it doesn't already have one // and add the +x bit. QString errorString; if (!::makeServiceFileExecutable(service.entryPath(), errorString)) { QString serviceName = service.name(); if (serviceName.isEmpty()) { serviceName = service.genericName(); } KMessageBox::sorry( window, i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString) ); return false; } return true; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::run(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return runApplication(_service, _urls, window, flags, suggestedFileName, asn) != 0; } #endif qint64 KRun::runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { if (!service.entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(service.entryPath()) && !::makeServiceExecutable(service, window)) { return 0; } KService::Ptr servicePtr(new KService(service)); // clone return runApplicationImpl(servicePtr, urls, window, flags, suggestedFileName, asn); } qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!_service.entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(_service.entryPath()) && !::makeServiceExecutable(_service, window)) { return 0; } if (!tempFiles) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : _urls) { KRecentDocument::add(url, _service.desktopEntryName()); } } bool useKToolInvocation = !(tempFiles || _service.entryPath().isEmpty() || !suggestedFileName.isEmpty()); if (useKToolInvocation) { // Is klauncher installed? Let's try to start it, if it fails, then we won't use it. static int klauncherAvailable = -1; if (klauncherAvailable == -1) { KToolInvocation::ensureKdeinitRunning(); QDBusConnectionInterface *dbusDaemon = QDBusConnection::sessionBus().interface(); klauncherAvailable = dbusDaemon->isServiceRegistered(QStringLiteral("org.kde.klauncher5")); } if (klauncherAvailable == 0) { useKToolInvocation = false; } } if (!useKToolInvocation) { KService::Ptr servicePtr(new KService(_service)); // clone return runApplicationImpl(servicePtr, _urls, window, tempFiles ? RunFlags(DeleteTemporaryFiles) : RunFlags(), suggestedFileName, asn); } // Resolve urls if needed, depending on what the app supports const QList urls = resolveURLs(_urls, _service); //qDebug() << "Running" << _service.entryPath() << _urls << "using klauncher"; QString error; int pid = 0; //TODO KF6: change KToolInvokation to take a qint64* QByteArray myasn = asn; // startServiceByDesktopPath() doesn't take QWidget*, add it to the startup info now if (window) { if (myasn.isEmpty()) { myasn = KStartupInfo::createNewStartupId(); } if (myasn != "0") { KStartupInfoId id; id.initId(myasn); KStartupInfoData data; // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead if (window->window()) { data.setLaunchedBy(window->window()->winId()); } KStartupInfo::sendChange(id, data); } } int i = KToolInvocation::startServiceByDesktopPath( _service.entryPath(), QUrl::toStringList(urls), &error, nullptr, &pid, myasn ); if (i != 0) { //qDebug() << error; KMessageBox::sorry(window, error); return 0; } //qDebug() << "startServiceByDesktopPath worked fine"; return pid; } bool KRun::run(const QString &_exec, const QList &_urls, QWidget *window, const QString &_name, const QString &_icon, const QByteArray &asn) { KService::Ptr service(new KService(_name, _exec, _icon)); return runApplication(*service, _urls, window, RunFlags{}, QString(), asn); } bool KRun::runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory) { if (cmd.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command was empty, nothing to run"; return false; } const QStringList args = KShell::splitArgs(cmd); if (args.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command could not be parsed."; return false; } const QString bin = args.first(); return KRun::runCommand(cmd, bin, bin /*iconName*/, window, QByteArray(), workingDirectory); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn) { return runCommand(cmd, execName, iconName, window, asn, QString()); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn, const QString &workingDirectory) { //qDebug() << "runCommand " << cmd << "," << execName; // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead auto windowId = WId{}; if (window) { window = window->window(); windowId = window ? window->winId() : WId{}; } auto *processRunner = new KProcessRunner(cmd, execName, iconName, windowId, asn, workingDirectory); return runProcessRunner(processRunner, window); } KRun::KRun(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) : d(new KRunPrivate(this)) { d->m_timer = new QTimer(this); d->m_timer->setObjectName(QStringLiteral("KRun::timer")); d->m_timer->setSingleShot(true); d->init(url, window, showProgressInfo, asn); } void KRun::KRunPrivate::init(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) { m_bFault = false; m_bAutoDelete = true; m_bProgressInfo = showProgressInfo; m_bFinished = false; m_job = nullptr; m_strURL = url; m_bScanFile = false; m_bIsDirectory = false; m_runExecutables = true; m_followRedirections = true; m_window = window; m_asn = asn; q->setEnableExternalBrowser(true); // Start the timer. This means we will return to the event // loop and do initialization afterwards. // Reason: We must complete the constructor before we do anything else. m_bCheckPrompt = false; m_bInit = true; q->connect(m_timer, &QTimer::timeout, q, &KRun::slotTimeout); startTimer(); //qDebug() << "new KRun" << q << url << "timer=" << m_timer; } void KRun::init() { //qDebug() << "INIT called"; if (!d->m_strURL.isValid() || d->m_strURL.scheme().isEmpty()) { const QString error = !d->m_strURL.isValid() ? d->m_strURL.errorString() : d->m_strURL.toString(); handleInitError(KIO::ERR_MALFORMED_URL, i18n("Malformed URL\n%1", error)); qCWarning(KIO_WIDGETS) << "Malformed URL:" << error; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_strURL)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_strURL.toDisplayString()); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { // use the function from QDesktopServices as it handles portals correctly d->m_bFault = !QDesktopServices::openUrl(d->m_strURL); d->m_bFinished = true; d->startTimer(); return; } if (!d->m_externalBrowser.isEmpty() && d->m_strURL.scheme().startsWith(QLatin1String("http"))) { if (d->runExecutable(d->m_externalBrowser)) { return; } } else if (d->m_strURL.isLocalFile() && (d->m_strURL.host().isEmpty() || (d->m_strURL.host() == QLatin1String("localhost")) || (d->m_strURL.host().compare(QHostInfo::localHostName(), Qt::CaseInsensitive) == 0))) { const QString localPath = d->m_strURL.toLocalFile(); if (!QFile::exists(localPath)) { handleInitError(KIO::ERR_DOES_NOT_EXIST, i18n("Unable to run the command specified. " "The file or folder %1 does not exist.", localPath.toHtmlEscaped())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); //qDebug() << "MIME TYPE is " << mime.name(); if (!d->m_externalBrowser.isEmpty() && ( mime.inherits(QStringLiteral("text/html")) || mime.inherits(QStringLiteral("application/xhtml+xml")))) { if (d->runExecutable(d->m_externalBrowser)) { return; } } else if (mime.isDefault() && !QFileInfo(localPath).isReadable()) { // Unknown mimetype because the file is unreadable, no point in showing an open-with dialog (#261002) const QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, localPath); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } else { mimeTypeDetermined(mime.name()); return; } } else if (KIO::DesktopExecParser::hasSchemeHandler(d->m_strURL)) { //qDebug() << "Using scheme handler"; const QString exec = schemeHandler(d->m_strURL.scheme()); if (exec.isEmpty()) { mimeTypeDetermined(KProtocolManager::defaultMimetype(d->m_strURL)); return; } else { if (run(exec, QList() << d->m_strURL, d->m_window, QString(), QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } } #if 0 // removed for KF5 (for portability). Reintroduce a bool or flag if useful. // Did we already get the information that it is a directory ? if ((d->m_mode & QT_STAT_MASK) == QT_STAT_DIR) { mimeTypeDetermined("inode/directory"); return; } #endif // Let's see whether it is a directory if (!KProtocolManager::supportsListing(d->m_strURL)) { // No support for listing => it can't be a directory (example: http) if (!KProtocolManager::supportsReading(d->m_strURL)) { // No support for reading files either => we can't do anything (example: mailto URL, with no associated app) handleInitError(KIO::ERR_UNSUPPORTED_ACTION, i18n("Could not find any application or handler for %1", d->m_strURL.toDisplayString())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } scanFile(); return; } //qDebug() << "Testing directory (stating)"; // It may be a directory or a file, let's stat KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::StatJob *job = KIO::stat(d->m_strURL, KIO::StatJob::SourceSide, 0 /* no details */, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotStatResult); d->m_job = job; //qDebug() << "Job" << job << "is about stating" << d->m_strURL; } KRun::~KRun() { //qDebug() << this; d->m_timer->stop(); killJob(); //qDebug() << this << "done"; delete d; } bool KRun::KRunPrivate::runExecutable(const QString &_exec) { QList urls; urls.append(m_strURL); if (_exec.startsWith(QLatin1Char('!'))) { // Literal command const QString exec = _exec.midRef(1) + QLatin1String(" %u"); if (q->run(exec, urls, m_window, QString(), QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } else { KService::Ptr service = KService::serviceByStorageId(_exec); if (service && q->runApplication(*service, urls, m_window, RunFlags{}, QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } return false; } void KRun::KRunPrivate::showPrompt() { ExecutableFileOpenDialog *dialog = new ExecutableFileOpenDialog(q->window()); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &ExecutableFileOpenDialog::finished, q, [this, dialog](int result){ onDialogFinished(result, dialog->isDontAskAgainChecked()); }); dialog->show(); } bool KRun::KRunPrivate::isPromptNeeded() { if (m_strURL == QUrl(QStringLiteral("remote:/x-wizard_service.desktop"))) { return false; } const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); const bool isFileExecutable = (isExecutableFile(m_strURL, mime.name()) || mime.inherits(QStringLiteral("application/x-desktop"))); const bool isTextFile = mime.inherits(QStringLiteral("text/plain")); if (isFileExecutable && isTextFile) { KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); const QString value = cfgGroup.readEntry("behaviourOnLaunch", "alwaysAsk"); if (value == QLatin1String("alwaysAsk")) { return true; } else { q->setRunExecutables(value == QLatin1String("execute")); } } return false; } void KRun::KRunPrivate::onDialogFinished(int result, bool isDontAskAgainSet) { if (result == ExecutableFileOpenDialog::Rejected) { m_bFinished = true; m_bInit = false; startTimer(); return; } q->setRunExecutables(result == ExecutableFileOpenDialog::ExecuteFile); if (isDontAskAgainSet) { QString output = result == ExecutableFileOpenDialog::OpenFile ? QStringLiteral("open") : QStringLiteral("execute"); KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); cfgGroup.writeEntry("behaviourOnLaunch", output); } startTimer(); } void KRun::scanFile() { //qDebug() << d->m_strURL; // First, let's check for well-known extensions // Not when there is a query in the URL, in any case. if (!d->m_strURL.hasQuery()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); if (!mime.isDefault() || d->m_strURL.isLocalFile()) { //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); mimeTypeDetermined(mime.name()); return; } } // No mimetype found, and the URL is not local (or fast mode not allowed). // We need to apply the 'KIO' method, i.e. either asking the server or // getting some data out of the file, to know what mimetype it is. if (!KProtocolManager::supportsReading(d->m_strURL)) { qCWarning(KIO_WIDGETS) << "#### NO SUPPORT FOR READING!"; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } //qDebug() << this << "Scanning file" << d->m_strURL; KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::TransferJob *job = KIO::get(d->m_strURL, KIO::NoReload /*reload*/, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotScanFinished); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &KRun::slotScanMimeType); d->m_job = job; //qDebug() << "Job" << job << "is about getting from" << d->m_strURL; } // When arriving in that method there are 6 possible states: // must_show_prompt, must_init, must_scan_file, found_dir, done+error or done+success. void KRun::slotTimeout() { if (d->m_bCheckPrompt) { d->m_bCheckPrompt = false; if (d->isPromptNeeded()) { d->showPrompt(); return; } } if (d->m_bInit) { d->m_bInit = false; init(); return; } if (d->m_bFault) { emit error(); } if (d->m_bFinished) { emit finished(); } else { if (d->m_bScanFile) { d->m_bScanFile = false; scanFile(); return; } else if (d->m_bIsDirectory) { d->m_bIsDirectory = false; mimeTypeDetermined(QStringLiteral("inode/directory")); return; } } if (d->m_bAutoDelete) { deleteLater(); return; } } void KRun::slotStatResult(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR" << job->error() << job->errorString(); handleError(job); //qDebug() << this << " KRun returning from showErrorDialog, starting timer to delete us"; d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } else { //qDebug() << "Finished"; KIO::StatJob *statJob = qobject_cast(job); if (!statJob) { qFatal("Fatal Error: job is a %s, should be a StatJob", typeid(*job).name()); } // Update our URL in case of a redirection setUrl(statJob->url()); const KIO::UDSEntry entry = statJob->statResult(); const mode_t mode = entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE); if ((mode & QT_STAT_MASK) == QT_STAT_DIR) { d->m_bIsDirectory = true; // it's a dir } else { d->m_bScanFile = true; // it's a file } d->m_localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); // mimetype already known? (e.g. print:/manager) const QString knownMimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (!knownMimeType.isEmpty()) { mimeTypeDetermined(knownMimeType); d->m_bFinished = true; } // We should have found something assert(d->m_bScanFile || d->m_bIsDirectory); // Start the timer. Once we get the timer event this // protocol server is back in the pool and we can reuse it. // This gives better performance than starting a new slave d->startTimer(); } } void KRun::slotScanMimeType(KIO::Job *, const QString &mimetype) { if (mimetype.isEmpty()) { qCWarning(KIO_WIDGETS) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << url().scheme(); } mimeTypeDetermined(mimetype); d->m_job = nullptr; } void KRun::slotScanFinished(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR (stat):" << job->error() << ' ' << job->errorString(); handleError(job); d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } } void KRun::mimeTypeDetermined(const QString &mimeType) { // foundMimeType reimplementations might show a dialog box; // make sure some timer doesn't kill us meanwhile (#137678, #156447) Q_ASSERT(!d->m_showingDialog); d->m_showingDialog = true; foundMimeType(mimeType); d->m_showingDialog = false; // We cannot assume that we're finished here. Some reimplementations // start a KIO job and call setFinished only later. } void KRun::foundMimeType(const QString &type) { //qDebug() << "Resulting mime type is " << type; QMimeDatabase db; KIO::TransferJob *job = qobject_cast(d->m_job); if (job) { // Update our URL in case of a redirection if (d->m_followRedirections) { setUrl(job->url()); } job->putOnHold(); KIO::Scheduler::publishSlaveOnHold(); d->m_job = nullptr; } Q_ASSERT(!d->m_bFinished); // Support for preferred service setting, see setPreferredService if (!d->m_preferredService.isEmpty()) { //qDebug() << "Attempting to open with preferred service: " << d->m_preferredService; KService::Ptr serv = KService::serviceByDesktopName(d->m_preferredService); if (serv && serv->hasMimeType(type)) { QList lst; lst.append(d->m_strURL); if (KRun::runApplication(*serv, lst, d->m_window, RunFlags{}, QString(), d->m_asn)) { setFinished(true); return; } /// Note: if that service failed, we'll go to runUrl below to /// maybe find another service, even though an error dialog box was /// already displayed. That's good if runUrl tries another service, /// but it's not good if it tries the same one :} } } // Resolve .desktop files from media:/, remote:/, applications:/ etc. QMimeType mime = db.mimeTypeForName(type); if (!mime.isValid()) { qCWarning(KIO_WIDGETS) << "Unknown mimetype " << type; } else if (mime.inherits(QStringLiteral("application/x-desktop")) && !d->m_localPath.isEmpty()) { d->m_strURL = QUrl::fromLocalFile(d->m_localPath); } KRun::RunFlags runFlags; if (d->m_runExecutables) { runFlags |= KRun::RunExecutables; } if (!KRun::runUrl(d->m_strURL, type, d->m_window, runFlags, d->m_suggestedFileName, d->m_asn)) { d->m_bFault = true; } setFinished(true); } void KRun::killJob() { if (d->m_job) { //qDebug() << this << "m_job=" << d->m_job; d->m_job->kill(); d->m_job = nullptr; } } void KRun::abort() { if (d->m_bFinished) { return; } //qDebug() << this << "m_showingDialog=" << d->m_showingDialog; killJob(); // If we're showing an error message box, the rest will be done // after closing the msgbox -> don't autodelete nor emit signals now. if (d->m_showingDialog) { return; } d->m_bFault = true; d->m_bFinished = true; d->m_bInit = false; d->m_bScanFile = false; // will emit the error and autodelete this d->startTimer(); } QWidget *KRun::window() const { return d->m_window; } bool KRun::hasError() const { return d->m_bFault; } bool KRun::hasFinished() const { return d->m_bFinished; } bool KRun::autoDelete() const { return d->m_bAutoDelete; } void KRun::setAutoDelete(bool b) { d->m_bAutoDelete = b; } void KRun::setEnableExternalBrowser(bool b) { d->m_externalBrowserEnabled = b; if (d->m_externalBrowserEnabled) { d->m_externalBrowser = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); // If a default browser isn't set in kdeglobals, fall back to mimeapps.list if (!d->m_externalBrowser.isEmpty()) { return; } KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); KConfigGroup defaultApps(profile, "Default Applications"); d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); if (d->m_externalBrowser.isEmpty()) { d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); } } else { d->m_externalBrowser.clear(); } } void KRun::setPreferredService(const QString &desktopEntryName) { d->m_preferredService = desktopEntryName; } void KRun::setRunExecutables(bool b) { d->m_runExecutables = b; } void KRun::setSuggestedFileName(const QString &fileName) { d->m_suggestedFileName = fileName; } void KRun::setShowScriptExecutionPrompt(bool showPrompt) { d->m_bCheckPrompt = showPrompt; } void KRun::setFollowRedirections(bool followRedirections) { d->m_followRedirections = followRedirections; } QString KRun::suggestedFileName() const { return d->m_suggestedFileName; } bool KRun::isExecutable(const QString &mimeTypeName) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimeTypeName); return (mimeType.inherits(QLatin1String("application/x-desktop")) || mimeType.inherits(QLatin1String("application/x-executable")) || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ mimeType.inherits(QLatin1String("application/x-sharedlib")) || mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || mimeType.inherits(QLatin1String("application/x-shellscript"))); } void KRun::setUrl(const QUrl &url) { d->m_strURL = url; } QUrl KRun::url() const { return d->m_strURL; } void KRun::setError(bool error) { d->m_bFault = error; } void KRun::setProgressInfo(bool progressInfo) { d->m_bProgressInfo = progressInfo; } bool KRun::progressInfo() const { return d->m_bProgressInfo; } void KRun::setFinished(bool finished) { d->m_bFinished = finished; if (finished) { d->startTimer(); } } void KRun::setJob(KIO::Job *job) { d->m_job = job; } KIO::Job *KRun::job() { return d->m_job; } #ifndef KIOWIDGETS_NO_DEPRECATED QTimer &KRun::timer() { return *d->m_timer; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setDoScanFile(bool scanFile) { d->m_bScanFile = scanFile; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::doScanFile() const { return d->m_bScanFile; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setIsDirecory(bool isDirectory) { d->m_bIsDirectory = isDirectory; } #endif bool KRun::isDirectory() const { return d->m_bIsDirectory; } #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setInitializeNextAction(bool initialize) { d->m_bInit = initialize; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::initializeNextAction() const { return d->m_bInit; } #endif bool KRun::isLocalFile() const { return d->m_strURL.isLocalFile(); } /****************/ KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList &urls, WId windowId, KRun::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) : m_process{new KProcess}, m_executable(KIO::DesktopExecParser::executablePath(service->exec())) { KIO::DesktopExecParser execParser(*service, urls); execParser.setUrlsAreTempFiles(flags & KRun::DeleteTemporaryFiles); execParser.setSuggestedFileName(suggestedFileName); const QStringList args = execParser.resultingArguments(); if (args.isEmpty()) { emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); return; } //qDebug() << "runTempService: KProcess args=" << args; *m_process << args; enum DiscreteGpuCheck { NotChecked, Present, Absent }; static DiscreteGpuCheck s_gpuCheck = NotChecked; if (service->runOnDiscreteGpu() && s_gpuCheck == NotChecked) { // Check whether we have a discrete gpu bool hasDiscreteGpu = false; QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/kde/Solid/PowerManagement"), QStringLiteral("org.kde.Solid.PowerManagement"), QDBusConnection::sessionBus()); if (iface.isValid()) { QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); if (reply.isValid()) { hasDiscreteGpu = reply.value(); } } s_gpuCheck = hasDiscreteGpu ? Present : Absent; } if (service->runOnDiscreteGpu() && s_gpuCheck == Present) { m_process->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); } QString workingDir(service->workingDirectory()); if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); } m_process->setWorkingDirectory(workingDir); if ((flags & KRun::DeleteTemporaryFiles) == 0) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : urls) { KRecentDocument::add(url, service->desktopEntryName()); } } const QString bin = KIO::DesktopExecParser::executableName(m_executable); init(service, bin, service->name(), service->icon(), windowId, asn); } KProcessRunner::KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, WId windowId, const QByteArray &asn, const QString &workingDirectory) : m_process{new KProcess}, m_executable(execName) { m_process->setShellCommand(cmd); if (!workingDirectory.isEmpty()) { m_process->setWorkingDirectory(workingDirectory); } QString bin = KIO::DesktopExecParser::executableName(m_executable); KService::Ptr service = KService::serviceByDesktopName(bin); init(service, bin, execName /*user-visible name*/, iconName, windowId, asn); } void KProcessRunner::init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, const QString &iconName, WId windowId, const QByteArray &asn) { if (service && !service->entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { qCWarning(KIO_WIDGETS) << "No authorization to execute " << service->entryPath(); emitDelayedError(i18n("You are not authorized to execute this file.")); return; } #if HAVE_X11 static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); if (isX11) { bool silent; QByteArray wmclass; const bool startup_notify = (asn != "0" && KRun::checkStartupNotify(QString() /*unused*/, service.data(), &silent, &wmclass)); if (startup_notify) { m_startupId.initId(asn); m_startupId.setupStartupEnv(); KStartupInfoData data; data.setHostname(); data.setBin(bin); if (!userVisibleName.isEmpty()) { data.setName(userVisibleName); } else if (service && !service->name().isEmpty()) { data.setName(service->name()); } data.setDescription(i18n("Launching %1", data.name())); if (!iconName.isEmpty()) { data.setIcon(iconName); } else if (service && !service->icon().isEmpty()) { data.setIcon(service->icon()); } if (!wmclass.isEmpty()) { data.setWMClass(wmclass); } if (silent) { data.setSilent(KStartupInfoData::Yes); } data.setDesktop(KWindowSystem::currentDesktop()); if (windowId) { data.setLaunchedBy(windowId); } if (service && !service->entryPath().isEmpty()) { data.setApplicationId(service->entryPath()); } KStartupInfo::sendStartup(m_startupId, data); } } #else Q_UNUSED(bin); Q_UNUSED(userVisibleName); Q_UNUSED(iconName); #endif startProcess(); } void KProcessRunner::startProcess() { connect(m_process.get(), QOverload::of(&QProcess::finished), this, &KProcessRunner::slotProcessExited); m_process->start(); if (!m_process->waitForStarted()) { //qDebug() << "wait for started failed, exitCode=" << process->exitCode() // << "exitStatus=" << process->exitStatus(); // Note that exitCode is 255 here (the first time), and 0 later on (bug?). // Use delayed invocation so the caller has time to connect to the signal QMetaObject::invokeMethod(this, [this]() { slotProcessExited(255, m_process->exitStatus()); }, Qt::QueuedConnection); } else { m_pid = m_process->processId(); #if HAVE_X11 if (!m_startupId.isNull() && m_pid) { KStartupInfoData data; data.addPid(m_pid); KStartupInfo::sendChange(m_startupId, data); KStartupInfo::resetStartupEnv(); } #endif } } KProcessRunner::~KProcessRunner() { // This destructor deletes m_process, since it's a unique_ptr. } qint64 KProcessRunner::pid() const { return m_pid; } void KProcessRunner::terminateStartupNotification() { #if HAVE_X11 if (!m_startupId.isNull()) { KStartupInfoData data; data.addPid(m_pid); // announce this pid for the startup notification has finished data.setHostname(); KStartupInfo::sendFinish(m_startupId, data); } #endif } void KProcessRunner::emitDelayedError(const QString &errorMsg) { // Use delayed invocation so the caller has time to connect to the signal QMetaObject::invokeMethod(this, [this, errorMsg]() { emit error(errorMsg); deleteLater(); }, Qt::QueuedConnection); } void KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { //qDebug() << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; Q_UNUSED(exitStatus) terminateStartupNotification(); // do this before the messagebox if (exitCode != 0 && !m_executable.isEmpty()) { // Let's see if the error is because the exe doesn't exist. // When this happens, waitForStarted returns false, but not if kioexec // was involved, then we come here, that's why the code is here. // // We'll try to find the executable relatively to current directory, // (or with a full path, if m_executable is absolute), and then in the PATH. if (!QFile(m_executable).exists() && QStandardPaths::findExecutable(m_executable).isEmpty()) { const QString &errorString = i18n("Could not find the program '%1'", m_executable); qWarning() << errorString; emit error(errorString); } else { //qDebug() << process->readAllStandardError(); } } deleteLater(); } #include "moc_krun.cpp" #include "moc_krun_p.cpp" #include "krun.moc" diff --git a/src/widgets/kshellcompletion.cpp b/src/widgets/kshellcompletion.cpp index def505d4..c38183a2 100644 --- a/src/widgets/kshellcompletion.cpp +++ b/src/widgets/kshellcompletion.cpp @@ -1,318 +1,317 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Smith 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 "kshellcompletion.h" #include #include #include #include class KShellCompletionPrivate { public: KShellCompletionPrivate() : m_word_break_char(QLatin1Char(' ')) , m_quote_char1(QLatin1Char('\"')) , m_quote_char2(QLatin1Char('\'')) , m_escape_char(QLatin1Char('\\')) { } void splitText(const QString &text, QString &text_start, QString &text_compl) const; bool quoteText(QString *text, bool force, bool skip_last) const; QString unquote(const QString &text) const; QString m_text_start; // part of the text that was not completed QString m_text_compl; // part of the text that was completed (unchanged) QChar m_word_break_char; QChar m_quote_char1; QChar m_quote_char2; QChar m_escape_char; }; KShellCompletion::KShellCompletion() : KUrlCompletion(), d(new KShellCompletionPrivate) { } KShellCompletion::~KShellCompletion() { delete d; } /* * makeCompletion() * * Entry point for file name completion */ QString KShellCompletion::makeCompletion(const QString &text) { // Split text at the last unquoted space // d->splitText(text, d->m_text_start, d->m_text_compl); // Remove quotes from the text to be completed // QString tmp = d->unquote(d->m_text_compl); d->m_text_compl = tmp; // Do exe-completion if there was no unquoted space // bool is_exe_completion = true; - for (int i = 0; i < d->m_text_start.length(); i++) { - if (d->m_text_start[i] != d->m_word_break_char) { + for (const QChar ch : qAsConst(d->m_text_start)) { + if (ch != d->m_word_break_char) { is_exe_completion = false; break; } } Mode mode = (is_exe_completion ? ExeCompletion : FileCompletion); setMode(mode); // Make completion on the last part of text // return KUrlCompletion::makeCompletion(d->m_text_compl); } /* * postProcessMatch, postProcessMatches * * Called by KCompletion before emitting match() and matches() * * Add add the part of the text that was not completed * Add quotes when needed */ void KShellCompletion::postProcessMatch(QString *match) const { KUrlCompletion::postProcessMatch(match); if (match->isNull()) { return; } if (match->endsWith(QLatin1Char('/'))) { d->quoteText(match, false, true); // don't quote the trailing '/' } else { d->quoteText(match, false, false); // quote the whole text } match->prepend(d->m_text_start); } void KShellCompletion::postProcessMatches(QStringList *matches) const { KUrlCompletion::postProcessMatches(matches); - for (QStringList::Iterator it = matches->begin(); - it != matches->end(); ++it) { - if (!(*it).isNull()) { - if ((*it).endsWith(QLatin1Char('/'))) { - d->quoteText(&(*it), false, true); // don't quote trailing '/' + for (QString &match : *matches) { + if (!match.isNull()) { + if (match.endsWith(QLatin1Char('/'))) { + d->quoteText(&match, false, true); // don't quote trailing '/' } else { - d->quoteText(&(*it), false, false); // quote the whole text + d->quoteText(&match, false, false); // quote the whole text } - (*it).prepend(d->m_text_start); + match.prepend(d->m_text_start); } } } void KShellCompletion::postProcessMatches(KCompletionMatches *matches) const { KUrlCompletion::postProcessMatches(matches); - for (KCompletionMatches::Iterator it = matches->begin(); - it != matches->end(); ++it) { - if (!(*it).value().isNull()) { - if ((*it).value().endsWith(QLatin1Char('/'))) { - d->quoteText(&(*it).value(), false, true); // don't quote trailing '/' + for (auto &match : *matches) { + QString& matchString = match.value(); + if (!matchString.isNull()) { + if (matchString.endsWith(QLatin1Char('/'))) { + d->quoteText(&matchString, false, true); // don't quote trailing '/' } else { - d->quoteText(&(*it).value(), false, false); // quote the whole text + d->quoteText(&matchString, false, false); // quote the whole text } - (*it).value().prepend(d->m_text_start); + matchString.prepend(d->m_text_start); } } } /* * splitText * * Split text at the last unquoted space * * text_start = [out] text at the left, including the space * text_compl = [out] text at the right */ void KShellCompletionPrivate::splitText(const QString &text, QString &text_start, QString &text_compl) const { bool in_quote = false; bool escaped = false; QChar p_last_quote_char; int last_unquoted_space = -1; int end_space_len = 0; for (int pos = 0; pos < text.length(); pos++) { end_space_len = 0; if (escaped) { escaped = false; } else if (in_quote && text[pos] == p_last_quote_char) { in_quote = false; } else if (!in_quote && text[pos] == m_quote_char1) { p_last_quote_char = m_quote_char1; in_quote = true; } else if (!in_quote && text[pos] == m_quote_char2) { p_last_quote_char = m_quote_char2; in_quote = true; } else if (text[pos] == m_escape_char) { escaped = true; } else if (!in_quote && text[pos] == m_word_break_char) { end_space_len = 1; while (pos + 1 < text.length() && text[pos + 1] == m_word_break_char) { end_space_len++; pos++; } if (pos + 1 == text.length()) { break; } last_unquoted_space = pos; } } text_start = text.left(last_unquoted_space + 1); // the last part without trailing blanks text_compl = text.mid(last_unquoted_space + 1); } /* * quoteText() * * Add quotations to 'text' if needed or if 'force' = true * Returns true if quotes were added * * skip_last => ignore the last character (we add a space or '/' to all filenames) */ bool KShellCompletionPrivate::quoteText(QString *text, bool force, bool skip_last) const { int pos = 0; if (!force) { pos = text->indexOf(m_word_break_char); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_quote_char1); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_quote_char2); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_escape_char); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (force || (pos >= 0)) { // Escape \ in the string text->replace(m_escape_char, QString(m_escape_char) + m_escape_char); // Escape " in the string text->replace(m_quote_char1, QString(m_escape_char) + m_quote_char1); // " at the beginning text->insert(0, m_quote_char1); // " at the end if (skip_last) { text->insert(text->length() - 1, m_quote_char1); } else { text->insert(text->length(), m_quote_char1); } return true; } return false; } /* * unquote * * Remove quotes and return the result in a new string * */ QString KShellCompletionPrivate::unquote(const QString &text) const { bool in_quote = false; bool escaped = false; QChar p_last_quote_char; QString result; - for (int pos = 0; pos < text.length(); pos++) { + for (const QChar ch : text) { if (escaped) { escaped = false; - result.insert(result.length(), text[pos]); - } else if (in_quote && text[pos] == p_last_quote_char) { + result.insert(result.length(), ch); + } else if (in_quote && ch == p_last_quote_char) { in_quote = false; - } else if (!in_quote && text[pos] == m_quote_char1) { + } else if (!in_quote && ch == m_quote_char1) { p_last_quote_char = m_quote_char1; in_quote = true; - } else if (!in_quote && text[pos] == m_quote_char2) { + } else if (!in_quote && ch == m_quote_char2) { p_last_quote_char = m_quote_char2; in_quote = true; - } else if (text[pos] == m_escape_char) { + } else if (ch == m_escape_char) { escaped = true; - result.insert(result.length(), text[pos]); + result.insert(result.length(), ch); } else { - result.insert(result.length(), text[pos]); + result.insert(result.length(), ch); } } return result; } diff --git a/src/widgets/ksslinfodialog.cpp b/src/widgets/ksslinfodialog.cpp index 0f64c64e..732c1194 100644 --- a/src/widgets/ksslinfodialog.cpp +++ b/src/widgets/ksslinfodialog.cpp @@ -1,243 +1,242 @@ /* This file is part of the KDE project * * Copyright (C) 2000,2001 George Staikos * Copyright (C) 2000 Malte Starostik * * 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 "ksslinfodialog.h" #include "ui_sslinfo.h" #include "ksslcertificatebox.h" #include #include #include #include // BarIcon #include "ktcpsocket.h" class Q_DECL_HIDDEN KSslInfoDialog::KSslInfoDialogPrivate { public: QList certificateChain; QList > certificateErrors; bool isMainPartEncrypted; bool auxPartsEncrypted; Ui::SslInfo ui; KSslCertificateBox *subject; KSslCertificateBox *issuer; }; KSslInfoDialog::KSslInfoDialog(QWidget *parent) : QDialog(parent), d(new KSslInfoDialogPrivate) { setWindowTitle(i18n("KDE SSL Information")); setAttribute(Qt::WA_DeleteOnClose); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QWidget *mainWidget = new QWidget(this); d->ui.setupUi(mainWidget); layout->addWidget(mainWidget); d->subject = new KSslCertificateBox(d->ui.certParties); d->issuer = new KSslCertificateBox(d->ui.certParties); d->ui.certParties->addTab(d->subject, i18nc("The receiver of the SSL certificate", "Subject")); d->ui.certParties->addTab(d->issuer, i18nc("The authority that issued the SSL certificate", "Issuer")); d->isMainPartEncrypted = true; d->auxPartsEncrypted = true; updateWhichPartsEncrypted(); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(buttonBox); #if 0 if (QSslSocket::supportsSsl()) { if (d->m_secCon) { d->pixmap->setPixmap(BarIcon("security-high")); d->info->setText(i18n("Current connection is secured with SSL.")); } else { d->pixmap->setPixmap(BarIcon("security-low")); d->info->setText(i18n("Current connection is not secured with SSL.")); } } else { d->pixmap->setPixmap(BarIcon("security-low")); d->info->setText(i18n("SSL support is not available in this build of KDE.")); } #endif } KSslInfoDialog::~KSslInfoDialog() { delete d; } void KSslInfoDialog::setMainPartEncrypted(bool mainEncrypted) { d->isMainPartEncrypted = mainEncrypted; updateWhichPartsEncrypted(); } void KSslInfoDialog::setAuxiliaryPartsEncrypted(bool auxEncrypted) { d->auxPartsEncrypted = auxEncrypted; updateWhichPartsEncrypted(); } void KSslInfoDialog::updateWhichPartsEncrypted() { if (d->isMainPartEncrypted) { if (d->auxPartsEncrypted) { d->ui.encryptionIndicator->setPixmap(QIcon::fromTheme(QStringLiteral("security-high")) .pixmap(KIconLoader::SizeSmallMedium)); d->ui.explanation->setText(i18n("Current connection is secured with SSL.")); } else { d->ui.encryptionIndicator->setPixmap(QIcon::fromTheme(QStringLiteral("security-medium")) .pixmap(KIconLoader::SizeSmallMedium)); d->ui.explanation->setText(i18n("The main part of this document is secured " "with SSL, but some parts are not.")); } } else { if (d->auxPartsEncrypted) { d->ui.encryptionIndicator->setPixmap(QIcon::fromTheme(QStringLiteral("security-medium")) .pixmap(KIconLoader::SizeSmallMedium)); d->ui.explanation->setText(i18n("Some of this document is secured with SSL, " "but the main part is not.")); } else { d->ui.encryptionIndicator->setPixmap(QIcon::fromTheme(QStringLiteral("security-low")) .pixmap(KIconLoader::SizeSmallMedium)); d->ui.explanation->setText(i18n("Current connection is not secured with SSL.")); } } } void KSslInfoDialog::setSslInfo(const QList &certificateChain, const QString &ip, const QString &host, const QString &sslProtocol, const QString &cipher, int usedBits, int bits, const QList > &validationErrors) { d->certificateChain = certificateChain; d->certificateErrors = validationErrors; d->ui.certSelector->clear(); - for (int i = 0; i < certificateChain.size(); i++) { - const QSslCertificate &cert = certificateChain[i]; + for (const QSslCertificate &cert : certificateChain) { QString name; static const QSslCertificate::SubjectInfo si[] = { QSslCertificate::CommonName, QSslCertificate::Organization, QSslCertificate::OrganizationalUnitName }; for (int j = 0; j < 3 && name.isEmpty(); j++) { name = cert.subjectInfo(si[j]).join(QLatin1String(", ")); } d->ui.certSelector->addItem(name); } if (certificateChain.size() < 2) { d->ui.certSelector->setEnabled(false); } connect(d->ui.certSelector, QOverload::of(&QComboBox::currentIndexChanged), this, &KSslInfoDialog::displayFromChain); if (d->certificateChain.isEmpty()) { d->certificateChain.append(QSslCertificate()); } displayFromChain(0); d->ui.ip->setText(ip); d->ui.address->setText(host); d->ui.sslVersion->setText(sslProtocol); const QStringList cipherInfo = cipher.split(QLatin1Char('\n'), QString::SkipEmptyParts); if (cipherInfo.size() >= 4) { d->ui.encryption->setText(i18nc("%1, using %2 bits of a %3 bit key", "%1, %2 %3", cipherInfo[0], i18ncp("Part of: %1, using %2 bits of a %3 bit key", "using %1 bit", "using %1 bits", usedBits), i18ncp("Part of: %1, using %2 bits of a %3 bit key", "of a %1 bit key", "of a %1 bit key", bits))); d->ui.details->setText(QStringLiteral("Auth = %1, Kx = %2, MAC = %3") .arg(cipherInfo[1], cipherInfo[2], cipherInfo[3])); } else { d->ui.encryption->setText(QString()); d->ui.details->setText(QString()); } } void KSslInfoDialog::displayFromChain(int i) { const QSslCertificate &cert = d->certificateChain[i]; QString trusted; if (!d->certificateErrors[i].isEmpty()) { trusted = i18nc("The certificate is not trusted", "NO, there were errors:"); foreach (KSslError::Error e, d->certificateErrors[i]) { KSslError classError(e); trusted += QLatin1Char('\n') + classError.errorString(); } } else { trusted = i18nc("The certificate is trusted", "Yes"); } d->ui.trusted->setText(trusted); QString vp = i18nc("%1 is the effective date of the certificate, %2 is the expiry date", "%1 to %2", cert.effectiveDate().toString(), cert.expiryDate().toString()); d->ui.validityPeriod->setText(vp); d->ui.serial->setText(QString::fromUtf8(cert.serialNumber())); d->ui.digest->setText(QString::fromUtf8(cert.digest().toHex())); d->ui.sha1Digest->setText(QString::fromUtf8(cert.digest(QCryptographicHash::Sha1).toHex())); d->subject->setCertificate(cert, KSslCertificateBox::Subject); d->issuer->setCertificate(cert, KSslCertificateBox::Issuer); } //static QList > KSslInfoDialog::errorsFromString(const QString &es) { const QStringList sl = es.split(QLatin1Char('\n'), QString::KeepEmptyParts); QList > ret; ret.reserve(sl.size()); for (const QString &s : sl) { QList certErrors; const QStringList sl2 = s.split(QLatin1Char('\t'), QString::SkipEmptyParts); for (const QString &s2 : sl2) { bool didConvert; KSslError::Error error = static_cast(s2.toInt(&didConvert)); if (didConvert) { certErrors.append(error); } } ret.append(certErrors); } return ret; } diff --git a/src/widgets/kurlcompletion.cpp b/src/widgets/kurlcompletion.cpp index 4c92eee6..97eaa267 100644 --- a/src/widgets/kurlcompletion.cpp +++ b/src/widgets/kurlcompletion.cpp @@ -1,1558 +1,1560 @@ /* 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 // 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 const QStringList allUsers = KUser::allUserNames(); for (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(); const QStringList keys = env.keys(); QStringList l; l.reserve(keys.size()); for (const QString &key : keys) { l.append(prepend + QLatin1Char('$') + 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(); url_list.reserve(dirList.size()); 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.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; + result.reserve(text.size()); - for (int pos = 0; pos < text.length(); pos++) - if (text.at(pos) != QLatin1Char('\\')) { - result.insert(result.length(), text.at(pos)); + for (const QChar ch : text) { + if (ch != QLatin1Char('\\')) { + result.append(ch); } + } return result; } #include "moc_kurlcompletion.cpp" #include "kurlcompletion.moc" diff --git a/src/widgets/kurlrequester.cpp b/src/widgets/kurlrequester.cpp index 0b728f66..bea162e1 100644 --- a/src/widgets/kurlrequester.cpp +++ b/src/widgets/kurlrequester.cpp @@ -1,716 +1,716 @@ /* 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() override {} 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_fileDialogModeWasDirAndFile(false), 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; bool dirsOnly = false; if (m & KFile::Directory) { fileMode = QFileDialog::Directory; if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) { dirsOnly = 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); dlg->setOption(QFileDialog::ShowDirsOnly, dirsOnly); } // 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('|')); - const QStringRef globs = it->leftRef(sep); - const QStringRef desc = it->midRef(sep + 1); - *it = desc + QLatin1String(" (") + globs + QLatin1Char(')'); + for (QString &qFilter : qFilters) { + int sep = qFilter.indexOf(QLatin1Char('|')); + const QStringRef globs = qFilter.leftRef(sep); + const QStringRef desc = qFilter.midRef(sep + 1); + qFilter = desc + QLatin1String(" (") + globs + QLatin1Char(')'); } return qFilters; } QUrl getDirFromFileDialog(const QUrl &openUrl) const { return QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly); } void createFileDialog() { //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(); } } // slots void _k_slotUpdateUrl(); void _k_slotOpenDialog(); void _k_slotFileDialogAccepted(); QUrl m_startDir; bool m_startDirCustomized; bool m_fileDialogModeWasDirAndFile; 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->setContentsMargins(0, 0, 0, 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 (!m_fileDialogModeWasDirAndFile && (((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); if (((fileDialogMode & KFile::Directory) && (fileDialogMode & KFile::File)) || m_fileDialogModeWasDirAndFile) { QMenu *dirOrFileMenu = new QMenu(); QAction *fileAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("File")); QAction *dirAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Directory")); dirOrFileMenu->addAction(fileAction); dirOrFileMenu->addAction(dirAction); connect(fileAction, &QAction::triggered, [this]() { fileDialogMode = KFile::File; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); connect(dirAction, &QAction::triggered, [this]() { fileDialogMode = KFile::Directory; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); dirOrFileMenu->exec(m_parent->mapToGlobal(QPoint(m_parent->width(), m_parent->height()))); return; } createFileDialog(); } } 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->fileMode() == QFileDialog::Directory && !(d->fileDialogMode & KFile::Directory)) || (d->myFileDialog->fileMode() != QFileDialog::Directory && (d->fileDialogMode & KFile::Directory)))) { delete d->myFileDialog; d->myFileDialog = nullptr; } 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" diff --git a/src/widgets/previewjob.cpp b/src/widgets/previewjob.cpp index efbcedb2..38a925ed 100644 --- a/src/widgets/previewjob.cpp +++ b/src/widgets/previewjob.cpp @@ -1,826 +1,828 @@ // -*- c++ -*- /* This file is part of the KDE libraries Copyright (C) 2000 David Faure 2000 Carsten Pfeiffer 2001 Malte Starostik 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 "previewjob.h" #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) #define WITH_SHM 1 #else #define WITH_SHM 0 #endif #if WITH_SHM #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "job_p.h" namespace KIO { struct PreviewItem; } using namespace KIO; struct KIO::PreviewItem { KFileItem item; KService::Ptr plugin; }; class KIO::PreviewJobPrivate: public KIO::JobPrivate { public: PreviewJobPrivate(const KFileItemList &items, const QSize &size) : initialItems(items), width(size.width()), height(size.height()), cacheWidth(width), cacheHeight(height), bScale(true), bSave(true), ignoreMaximumSize(false), sequenceIndex(0), succeeded(false), maximumLocalSize(0), maximumRemoteSize(0), iconSize(0), iconAlpha(70), shmid(-1), shmaddr(nullptr) { // http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY thumbRoot = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/"); } enum { STATE_STATORIG, // if the thumbnail exists STATE_GETORIG, // if we create it STATE_CREATETHUMB // thumbnail:/ slave } state; KFileItemList initialItems; QStringList enabledPlugins; // Some plugins support remote URLs, QHash m_remoteProtocolPlugins; // Our todo list :) // We remove the first item at every step, so use QLinkedList QLinkedList items; // The current item PreviewItem currentItem; // The modification time of that URL QDateTime tOrig; // Path to thumbnail cache for the current size QString thumbPath; // Original URL of current item in RFC2396 format // (file:///path/to/a%20file instead of file:/path/to/a file) QByteArray origName; // Thumbnail file name for current item QString thumbName; // Size of thumbnail int width; int height; // Unscaled size of thumbnail (128 or 256 if cache is enabled) int cacheWidth; int cacheHeight; // Whether the thumbnail should be scaled bool bScale; // Whether we should save the thumbnail bool bSave; bool ignoreMaximumSize; int sequenceIndex; bool succeeded; // If the file to create a thumb for was a temp file, this is its name QString tempName; KIO::filesize_t maximumLocalSize; KIO::filesize_t maximumRemoteSize; // the size for the icon overlay int iconSize; // the transparency of the blended mimetype icon int iconAlpha; // Shared memory segment Id. The segment is allocated to a size // of extent x extent x 4 (32 bit image) on first need. int shmid; // And the data area uchar *shmaddr; // Root of thumbnail cache QString thumbRoot; void getOrCreateThumbnail(); bool statResultThumbnail(); void createThumbnail(const QString &); void cleanupTempFile(); void determineNextFile(); void emitPreview(const QImage &thumb); void startPreview(); void slotThumbData(KIO::Job *, const QByteArray &); Q_DECLARE_PUBLIC(PreviewJob) }; #ifndef KIOWIDGETS_NO_DEPRECATED PreviewJob::PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width))) { Q_D(PreviewJob); d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins(); d->iconSize = iconSize; d->iconAlpha = iconAlpha; d->bScale = scale; d->bSave = save && scale; // Return to event loop first, determineNextFile() might delete this; QTimer::singleShot(0, this, SLOT(startPreview())); } #endif PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) : KIO::Job(*new PreviewJobPrivate(items, size)) { Q_D(PreviewJob); if (enabledPlugins) { d->enabledPlugins = *enabledPlugins; } else { const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); d->enabledPlugins = globalConfig.readEntry("Plugins", QStringList { QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")}); } // Return to event loop first, determineNextFile() might delete this; QTimer::singleShot(0, this, SLOT(startPreview())); } PreviewJob::~PreviewJob() { #if WITH_SHM Q_D(PreviewJob); if (d->shmaddr) { shmdt((char *)d->shmaddr); shmctl(d->shmid, IPC_RMID, nullptr); } #endif } void PreviewJob::setOverlayIconSize(int size) { Q_D(PreviewJob); d->iconSize = size; } int PreviewJob::overlayIconSize() const { Q_D(const PreviewJob); return d->iconSize; } void PreviewJob::setOverlayIconAlpha(int alpha) { Q_D(PreviewJob); d->iconAlpha = qBound(0, alpha, 255); } int PreviewJob::overlayIconAlpha() const { Q_D(const PreviewJob); return d->iconAlpha; } void PreviewJob::setScaleType(ScaleType type) { Q_D(PreviewJob); switch (type) { case Unscaled: d->bScale = false; d->bSave = false; break; case Scaled: d->bScale = true; d->bSave = false; break; case ScaledAndCached: d->bScale = true; d->bSave = true; break; default: break; } } PreviewJob::ScaleType PreviewJob::scaleType() const { Q_D(const PreviewJob); if (d->bScale) { return d->bSave ? ScaledAndCached : Scaled; } return Unscaled; } void PreviewJobPrivate::startPreview() { Q_Q(PreviewJob); // Load the list of plugins to determine which mimetypes are supported const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator")); QMap mimeMap; QHash > protocolMap; for (KService::List::ConstIterator it = plugins.constBegin(); it != plugins.constEnd(); ++it) { QStringList protocols = (*it)->property(QStringLiteral("X-KDE-Protocols")).toStringList(); const QString p = (*it)->property(QStringLiteral("X-KDE-Protocol")).toString(); if (!p.isEmpty()) { protocols.append(p); } foreach (const QString &protocol, protocols) { // We cannot use mimeTypes() here, it doesn't support groups such as: text/* const QStringList mtypes = (*it)->serviceTypes(); // Add supported mimetype for this protocol QStringList &_ms = m_remoteProtocolPlugins[protocol]; foreach (const QString &_m, mtypes) { if (_m != QLatin1String("ThumbCreator")) { protocolMap[protocol].insert(_m, *it); if (!_ms.contains(_m)) { _ms.append(_m); } } } } if (enabledPlugins.contains((*it)->desktopEntryName())) { const QStringList mimeTypes = (*it)->serviceTypes(); for (QStringList::ConstIterator mt = mimeTypes.constBegin(); mt != mimeTypes.constEnd(); ++mt) { if (*mt != QLatin1String("ThumbCreator")) { mimeMap.insert(*mt, *it); } } } } const auto mountsList = KMountPoint::currentMountPoints(); KMountPoint::List encryptedMountsList; const auto thumbRootMount = mountsList.findByPath(thumbRoot); std::copy_if(mountsList.begin(), mountsList.end(), std::back_inserter(encryptedMountsList), [&thumbRootMount] (KMountPoint::Ptr mount) { return (thumbRootMount != mount) && (mount->mountType() == QLatin1String("fuse.cryfs") || mount->mountType() == QLatin1String("fuse.encfs")); }); // Look for images and store the items in our todo list :) bool bNeedCache = false; KFileItemList::const_iterator kit = initialItems.constBegin(); const KFileItemList::const_iterator kend = initialItems.constEnd(); for (; kit != kend; ++kit) { PreviewItem item; item.item = *kit; if (encryptedMountsList.findByPath(item.item.localPath())) { continue; } const QString mimeType = item.item.mimetype(); KService::Ptr plugin(nullptr); // look for protocol-specific thumbnail plugins first QHash >::const_iterator it = protocolMap.constFind(item.item.url().scheme()); if (it != protocolMap.constEnd()) { plugin = it.value().value(mimeType); } if (!plugin) { QMap::ConstIterator pluginIt = mimeMap.constFind(mimeType); if (pluginIt == mimeMap.constEnd()) { QString groupMimeType = mimeType; groupMimeType.replace(QRegExp(QStringLiteral("/.*")), QStringLiteral("/*")); pluginIt = mimeMap.constFind(groupMimeType); if (pluginIt == mimeMap.constEnd()) { QMimeDatabase db; // check mime type inheritance, resolve aliases const QMimeType mimeInfo = db.mimeTypeForName(mimeType); if (mimeInfo.isValid()) { const QStringList parentMimeTypes = mimeInfo.allAncestors(); for (const QString &parentMimeType : parentMimeTypes) { pluginIt = mimeMap.constFind(parentMimeType); if (pluginIt != mimeMap.constEnd()) { break; } } } } } if (pluginIt != mimeMap.constEnd()) { plugin = *pluginIt; } } if (plugin) { item.plugin = plugin; items.append(item); if (!bNeedCache && bSave && plugin->property(QStringLiteral("CacheThumbnail")).toBool()) { const QUrl url = (*kit).url(); if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) { bNeedCache = true; } } } else { emit q->failed(*kit); } } KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings"); maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits::max()); maximumRemoteSize = cg.readEntry("MaximumRemoteSize", 0); if (bNeedCache) { if (width <= 128 && height <= 128) { cacheWidth = cacheHeight = 128; } else { cacheWidth = cacheHeight = 256; } thumbPath = thumbRoot + QLatin1String(cacheWidth == 128 ? "normal/" : "large/"); if (!QDir(thumbPath).exists()) { if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions) QFile f(thumbPath); f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700 } } } else { bSave = false; } initialItems.clear(); determineNextFile(); } void PreviewJob::removeItem(const QUrl &url) { Q_D(PreviewJob); for (QLinkedList::Iterator it = d->items.begin(); it != d->items.end(); ++it) if ((*it).item.url() == url) { d->items.erase(it); break; } if (d->currentItem.item.url() == url) { KJob *job = subjobs().first(); job->kill(); removeSubjob(job); d->determineNextFile(); } } void KIO::PreviewJob::setSequenceIndex(int index) { d_func()->sequenceIndex = index; } int KIO::PreviewJob::sequenceIndex() const { return d_func()->sequenceIndex; } void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) { d_func()->ignoreMaximumSize = ignoreSize; } void PreviewJobPrivate::cleanupTempFile() { if (!tempName.isEmpty()) { Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink()); QFile::remove(tempName); tempName.clear(); } } void PreviewJobPrivate::determineNextFile() { Q_Q(PreviewJob); if (!currentItem.item.isNull()) { if (!succeeded) { emit q->failed(currentItem.item); } } // No more items ? if (items.isEmpty()) { q->emitResult(); return; } else { // First, stat the orig file state = PreviewJobPrivate::STATE_STATORIG; currentItem = items.first(); succeeded = false; items.removeFirst(); KIO::Job *job = KIO::stat(currentItem.item.url(), KIO::HideProgressInfo); job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1")); job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); q->addSubjob(job); } } void PreviewJob::slotResult(KJob *job) { Q_D(PreviewJob); removeSubjob(job); Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ... switch (d->state) { case PreviewJobPrivate::STATE_STATORIG: { if (job->error()) { // that's no good news... // Drop this one and move on to the next one d->determineNextFile(); return; } const KIO::UDSEntry entry = static_cast(job)->statResult(); d->tOrig = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0)); bool skipCurrentItem = false; const KIO::filesize_t size = (KIO::filesize_t)entry.numberValue(KIO::UDSEntry::UDS_SIZE, 0); const QUrl itemUrl = d->currentItem.item.mostLocalUrl(); if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) { skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin->property(QStringLiteral("IgnoreMaximumSize")).toBool(); } else { // For remote items the "IgnoreMaximumSize" plugin property is not respected skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize; // Remote directories are not supported, don't try to do a file_copy on them if (!skipCurrentItem) { // TODO update item.mimeType from the UDS entry, in case it wasn't set initially // But we don't use the mimetype anymore, we just use isDir(). if (d->currentItem.item.isDir()) { skipCurrentItem = true; } } } if (skipCurrentItem) { d->determineNextFile(); return; } bool pluginHandlesSequences = d->currentItem.plugin->property(QStringLiteral("HandleSequences"), QVariant::Bool).toBool(); if (!d->currentItem.plugin->property(QStringLiteral("CacheThumbnail")).toBool() || (d->sequenceIndex && pluginHandlesSequences)) { // This preview will not be cached, no need to look for a saved thumbnail // Just create it, and be done d->getOrCreateThumbnail(); return; } if (d->statResultThumbnail()) { return; } d->getOrCreateThumbnail(); return; } case PreviewJobPrivate::STATE_GETORIG: { if (job->error()) { d->cleanupTempFile(); d->determineNextFile(); return; } d->createThumbnail(static_cast(job)->destUrl().toLocalFile()); return; } case PreviewJobPrivate::STATE_CREATETHUMB: { d->cleanupTempFile(); d->determineNextFile(); return; } } } bool PreviewJobPrivate::statResultThumbnail() { if (thumbPath.isEmpty()) { return false; } QUrl url = currentItem.item.mostLocalUrl(); // Don't include the password if any url.setPassword(QString()); origName = url.toEncoded(); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(QFile::encodeName(QString::fromUtf8(origName))); thumbName = QString::fromUtf8(QFile::encodeName(QString::fromLatin1(md5.result().toHex()))) + QLatin1String(".png"); QImage thumb; if (!thumb.load(thumbPath + thumbName)) { return false; } if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName) || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) { return false; } QString thumbnailerVersion = currentItem.plugin->property(QStringLiteral("ThumbnailerVersion"), QVariant::String).toString(); if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) { //Check if the version matches //The software string should read "KDE Thumbnail Generator pluginName (vX)" QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed(); if (softwareString.isEmpty()) { // The thumbnail has been created with an older version, recreating return false; } int versionIndex = softwareString.lastIndexOf(QLatin1String("(v")); if (versionIndex < 0) { return false; } QString cachedVersion = softwareString.remove(0, versionIndex + 2); cachedVersion.chop(1); uint thumbnailerMajor = thumbnailerVersion.toInt(); uint cachedMajor = cachedVersion.toInt(); if (thumbnailerMajor > cachedMajor) { return false; } } // Found it, use it emitPreview(thumb); succeeded = true; determineNextFile(); return true; } void PreviewJobPrivate::getOrCreateThumbnail() { Q_Q(PreviewJob); // We still need to load the orig file ! (This is getting tedious) :) const KFileItem &item = currentItem.item; const QString localPath = item.localPath(); if (!localPath.isEmpty()) { createThumbnail(localPath); } else { const QUrl fileUrl = item.url(); // heuristics for remote URL support bool supportsProtocol = false; if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) { // There's a plugin supporting this protocol and mimetype supportsProtocol = true; } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) { // Assume KIO understands any URL, ThumbCreator slaves who have // X-KDE-Protocols=KIO will get fed the remote URL directly. supportsProtocol = true; } if (supportsProtocol) { createThumbnail(fileUrl.toString()); return; } if (item.isDir()) { // Skip remote dirs (bug 208625) cleanupTempFile(); determineNextFile(); return; } // No plugin support access to this remote content, copy the file // to the local machine, then create the thumbnail state = PreviewJobPrivate::STATE_GETORIG; QTemporaryFile localFile; localFile.setAutoRemove(false); localFile.open(); tempName = localFile.fileName(); const QUrl currentURL = item.mostLocalUrl(); KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */); job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1")); q->addSubjob(job); } } void PreviewJobPrivate::createThumbnail(const QString &pixPath) { Q_Q(PreviewJob); state = PreviewJobPrivate::STATE_CREATETHUMB; QUrl thumbURL; thumbURL.setScheme(QStringLiteral("thumbnail")); thumbURL.setPath(pixPath); KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo); q->addSubjob(job); q->connect(job, SIGNAL(data(KIO::Job*,QByteArray)), SLOT(slotThumbData(KIO::Job*,QByteArray))); bool save = bSave && currentItem.plugin->property(QStringLiteral("CacheThumbnail")).toBool() && !sequenceIndex; job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype()); job->addMetaData(QStringLiteral("width"), QString().setNum(save ? cacheWidth : width)); job->addMetaData(QStringLiteral("height"), QString().setNum(save ? cacheHeight : height)); job->addMetaData(QStringLiteral("iconSize"), QString().setNum(save ? 64 : iconSize)); job->addMetaData(QStringLiteral("iconAlpha"), QString().setNum(iconAlpha)); job->addMetaData(QStringLiteral("plugin"), currentItem.plugin->library()); job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(','))); if (sequenceIndex) { job->addMetaData(QStringLiteral("sequence-index"), QString().setNum(sequenceIndex)); } #if WITH_SHM if (shmid == -1) { if (shmaddr) { shmdt((char *)shmaddr); shmctl(shmid, IPC_RMID, nullptr); } shmid = shmget(IPC_PRIVATE, cacheWidth * cacheHeight * 4, IPC_CREAT | 0600); if (shmid != -1) { shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY)); if (shmaddr == (uchar *) - 1) { shmctl(shmid, IPC_RMID, nullptr); shmaddr = nullptr; shmid = -1; } } else { shmaddr = nullptr; } } if (shmid != -1) { job->addMetaData(QStringLiteral("shmid"), QString().setNum(shmid)); } #endif } void PreviewJobPrivate::slotThumbData(KIO::Job *, const QByteArray &data) { bool save = bSave && currentItem.plugin->property(QStringLiteral("CacheThumbnail")).toBool() && (!currentItem.item.url().isLocalFile() || !currentItem.item.url().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) && !sequenceIndex; QImage thumb; #if WITH_SHM if (shmaddr) { // Keep this in sync with kdebase/kioslave/thumbnail.cpp QDataStream str(data); int width, height; quint8 iFormat; str >> width >> height >> iFormat; QImage::Format format = static_cast(iFormat); thumb = QImage(shmaddr, width, height, format).copy(); } else #endif thumb.loadFromData(data); if (thumb.isNull()) { QDataStream s(data); s >> thumb; } if (save) { thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName)); thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch())); thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size())); thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype()); QString thumbnailerVersion = currentItem.plugin->property(QStringLiteral("ThumbnailerVersion"), QVariant::String).toString(); QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin->name(); if (!thumbnailerVersion.isEmpty()) { signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')')); } thumb.setText(QStringLiteral("Software"), signature); QSaveFile saveFile(thumbPath + thumbName); if (saveFile.open(QIODevice::WriteOnly)) { if (thumb.save(&saveFile, "PNG")) { saveFile.commit(); } } } emitPreview(thumb); succeeded = true; } void PreviewJobPrivate::emitPreview(const QImage &thumb) { Q_Q(PreviewJob); QPixmap pix; if (thumb.width() > width || thumb.height() > height) { pix = QPixmap::fromImage(thumb.scaled(QSize(width, height), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else { pix = QPixmap::fromImage(thumb); } emit q->gotPreview(currentItem.item, pix); } QStringList PreviewJob::availablePlugins() { QStringList result; const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator")); - for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) - if (!result.contains((*it)->desktopEntryName())) { - result.append((*it)->desktopEntryName()); + for (const KService::Ptr &plugin : plugins) { + const QString desktopEntryName = plugin->desktopEntryName(); + if (!result.contains(desktopEntryName)) { + result.append(desktopEntryName); } + } return result; } QStringList PreviewJob::defaultPlugins() { const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail"); QStringList defaultPlugins = availablePlugins(); for (const QString &plugin : blacklist) { defaultPlugins.removeAll(plugin); } return defaultPlugins; } QStringList PreviewJob::supportedMimeTypes() { QStringList result; const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator")); - for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) { - result += (*it)->mimeTypes(); + for (const KService::Ptr &plugin : plugins) { + result += plugin->mimeTypes(); } return result; } #ifndef KIOWIDGETS_NO_DEPRECATED PreviewJob *KIO::filePreview(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) { return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); } PreviewJob *KIO::filePreview(const QList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) { KFileItemList fileItems; fileItems.reserve(items.size()); - for (QList::const_iterator it = items.begin(); it != items.end(); ++it) { - Q_ASSERT((*it).isValid()); // please call us with valid urls only - fileItems.append(KFileItem(*it)); + for (const QUrl &url : items) { + Q_ASSERT(url.isValid()); // please call us with valid urls only + fileItems.append(KFileItem(url)); } return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); } #endif PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) { return new PreviewJob(items, size, enabledPlugins); } #ifndef KIOWIDGETS_NO_DEPRECATED KIO::filesize_t PreviewJob::maximumFileSize() { KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings"); return cg.readEntry("MaximumSize", 5 * 1024 * 1024LL /* 5MB */); } #endif #include "moc_previewjob.cpp"