diff --git a/src/ioslaves/ftp/ftp.cpp b/src/ioslaves/ftp/ftp.cpp index 30bdcd32..35f5f51b 100644 --- a/src/ioslaves/ftp/ftp.cpp +++ b/src/ioslaves/ftp/ftp.cpp @@ -1,2712 +1,2712 @@ /* This file is part of the KDE libraries Copyright (C) 2000-2006 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. */ /* Recommended reading explaining FTP details and quirks: http://cr.yp.to/ftp.html (by D.J. Bernstein) RFC: RFC 959 "File Transfer Protocol (FTP)" RFC 1635 "How to Use Anonymous FTP" RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV) */ #include #define KIO_FTP_PRIVATE_INCLUDE #include "ftp.h" #ifdef Q_OS_WIN #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kioglobal_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_FTP) Q_LOGGING_CATEGORY(KIO_FTP, "kf5.kio.kio_ftp", QtWarningMsg) #if HAVE_STRTOLL #define charToLongLong(a) strtoll(a, nullptr, 10) #else #define charToLongLong(a) strtol(a, nullptr, 10) #endif #define FTP_LOGIN "anonymous" #define FTP_PASSWD "anonymous@" #define ENABLE_CAN_RESUME // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.ftp" FILE "ftp.json") }; static QString ftpCleanPath(const QString &path) { if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) { return path.left((path.length() - qstrlen(";type=X"))); } return path; } static char ftpModeFromPath(const QString &path, char defaultMode = '\0') { const int index = path.lastIndexOf(QLatin1String(";type=")); if (index > -1 && (index + 6) < path.size()) { const QChar mode = path.at(index + 6); // kio_ftp supports only A (ASCII) and I(BINARY) modes. if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) { return mode.toUpper().toLatin1(); } } return defaultMode; } static bool supportedProxyScheme(const QString &scheme) { return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks")); } static bool isSocksProxy() { return (QNetworkProxy::applicationProxy().type() == QNetworkProxy::Socks5Proxy); } // JPF: somebody should find a better solution for this or move this to KIO namespace KIO { enum buffersizes { /** * largest buffer size that should be used to transfer data between * KIO slaves using the data() function */ maximumIpcSize = 32 * 1024, /** * this is a reasonable value for an initial read() that a KIO slave * can do to obtain data via a slow network connection. */ initialIpcSize = 2 * 1024, /** * recommended size of a data block passed to findBufferFileType() */ minimumMimeSize = 1024 }; // JPF: this helper was derived from write_all in file.cc (FileProtocol). static // JPF: in ftp.cc we make it static /** * This helper handles some special issues (blocking and interrupted * system call) when writing to a file handle. * * @return 0 on success or an error code on failure (ERR_CANNOT_WRITE, * ERR_DISK_FULL, ERR_CONNECTION_BROKEN). */ int WriteToFile(int fd, const char *buf, size_t len) { while (len > 0) { // JPF: shouldn't there be a KDE_write? ssize_t written = write(fd, buf, len); if (written >= 0) { buf += written; len -= written; continue; } switch (errno) { case EINTR: continue; case EPIPE: return ERR_CONNECTION_BROKEN; case ENOSPC: return ERR_DISK_FULL; default: return ERR_CANNOT_WRITE; } } return 0; } } const KIO::filesize_t Ftp::UnknownSize = (KIO::filesize_t) - 1; using namespace KIO; extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationName(QStringLiteral("kio_ftp")); qCDebug(KIO_FTP) << "Starting"; if (argc != 4) { fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n"); exit(-1); } Ftp slave(argv[2], argv[3]); slave.dispatchLoop(); qCDebug(KIO_FTP) << "Done"; return 0; } //=============================================================================== // Ftp //=============================================================================== Ftp::Ftp(const QByteArray &pool, const QByteArray &app) : SlaveBase(QByteArrayLiteral("ftp"), pool, app) { // init the socket data m_data = m_control = nullptr; m_server = nullptr; ftpCloseControlConnection(); // init other members m_port = 0; m_socketProxyAuth = nullptr; } Ftp::~Ftp() { qCDebug(KIO_FTP); closeConnection(); } /** * This closes a data connection opened by ftpOpenDataConnection(). */ void Ftp::ftpCloseDataConnection() { delete m_data; m_data = nullptr; delete m_server; m_server = nullptr; } /** * This closes a control connection opened by ftpOpenControlConnection() and reinits the * related states. This method gets called from the constructor with m_control = nullptr. */ void Ftp::ftpCloseControlConnection() { m_extControl = 0; delete m_control; m_control = nullptr; m_cDataMode = 0; m_bLoggedOn = false; // logon needs control connection m_bTextMode = false; m_bBusy = false; } /** * Returns the last response from the server (iOffset >= 0) -or- reads a new response * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0). */ const char *Ftp::ftpResponse(int iOffset) { Q_ASSERT(m_control); // must have control connection socket const char *pTxt = m_lastControlLine.data(); // read the next line ... if (iOffset < 0) { int iMore = 0; m_iRespCode = 0; if (!pTxt) { return nullptr; // avoid using a nullptr when calling atoi. } // If the server sends a multiline response starting with // "nnn-text" we loop here until a final "nnn text" line is // reached. Only data from the final line will be stored. do { while (!m_control->canReadLine() && m_control->waitForReadyRead((readTimeout() * 1000))) {} m_lastControlLine = m_control->readLine(); pTxt = m_lastControlLine.data(); int iCode = atoi(pTxt); if (iMore == 0) { // first line qCDebug(KIO_FTP) << " > " << pTxt; if (iCode >= 100) { m_iRespCode = iCode; if (pTxt[3] == '-') { // marker for a multiple line response iMore = iCode; } } else { qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt; } } else { // multi-line qCDebug(KIO_FTP) << " > " << pTxt; if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') { iMore = 0; } } } while (iMore != 0); qCDebug(KIO_FTP) << "resp> " << pTxt; m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0; } // return text with offset ... while (iOffset-- > 0 && pTxt[0]) { pTxt++; } return pTxt; } void Ftp::closeConnection() { if (m_control || m_data) qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy; if (m_bBusy) { // ftpCloseCommand not called qCWarning(KIO_FTP) << "Abandoned data stream"; ftpCloseDataConnection(); } if (m_bLoggedOn) { // send quit if (!ftpSendCmd(QByteArrayLiteral("quit"), 0) || (m_iRespType != 2)) { qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode; } } // close the data and control connections ... ftpCloseDataConnection(); ftpCloseControlConnection(); } void Ftp::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass) { qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user; m_proxyURL.clear(); m_proxyUrls = config()->readEntry("ProxyUrls", QStringList()); qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls; if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) { closeConnection(); } m_host = _host; m_port = _port; m_user = _user; m_pass = _pass; } void Ftp::openConnection() { ftpOpenConnection(loginExplicit); } bool Ftp::ftpOpenConnection(LoginMode loginMode) { // check for implicit login if we are already logged on ... if (loginMode == loginImplicit && m_bLoggedOn) { Q_ASSERT(m_control); // must have control connection socket return true; } qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]"; infoMessage(i18n("Opening connection to host %1", m_host)); if (m_host.isEmpty()) { error(ERR_UNKNOWN_HOST, QString()); return false; } Q_ASSERT(!m_bLoggedOn); m_initialPath.clear(); m_currentPath.clear(); if (!ftpOpenControlConnection()) { return false; // error emitted by ftpOpenControlConnection } infoMessage(i18n("Connected to host %1", m_host)); bool userNameChanged = false; if (loginMode != loginDefered) { m_bLoggedOn = ftpLogin(&userNameChanged); if (!m_bLoggedOn) { return false; // error emitted by ftpLogin } } m_bTextMode = config()->readEntry("textmode", false); connected(); // Redirected due to credential change... if (userNameChanged && m_bLoggedOn) { QUrl realURL; realURL.setScheme(QStringLiteral("ftp")); if (m_user != QLatin1String(FTP_LOGIN)) { realURL.setUserName(m_user); } if (m_pass != QLatin1String(FTP_PASSWD)) { realURL.setPassword(m_pass); } realURL.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { realURL.setPort(m_port); } if (m_initialPath.isEmpty()) { m_initialPath = '/'; } realURL.setPath(m_initialPath); qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL; redirection(realURL); finished(); return false; } return true; } /** * Called by @ref openConnection. It opens the control connection to the ftp server. * * @return true on success. */ bool Ftp::ftpOpenControlConnection() { if (m_proxyUrls.isEmpty()) { return ftpOpenControlConnection(m_host, m_port); } int errorCode = 0; QString errorMessage; Q_FOREACH (const QString &proxyUrl, m_proxyUrls) { const QUrl url(proxyUrl); const QString scheme(url.scheme()); if (!supportedProxyScheme(scheme)) { // TODO: Need a new error code to indicate unsupported URL scheme. errorCode = ERR_CANNOT_CONNECT; errorMessage = url.toString(); continue; } if (scheme == QLatin1String("socks")) { qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url; const int proxyPort = url.port(); QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, url.host(), (proxyPort == -1 ? 0 : proxyPort)); QNetworkProxy::setApplicationProxy(proxy); if (ftpOpenControlConnection(m_host, m_port)) { return true; } QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); } else { if (ftpOpenControlConnection(url.host(), url.port())) { m_proxyURL = url; return true; } } } if (errorCode) { error(errorCode, errorMessage); } return false; } bool Ftp::ftpOpenControlConnection(const QString &host, int port) { // implicitly close, then try to open a new connection ... closeConnection(); QString sErrorMsg; // now connect to the server and read the login message ... if (port == 0) { port = 21; // default FTP port } m_control = synchronousConnectToHost(host, port); - connect(m_control, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), - this, SLOT(proxyAuthentication(QNetworkProxy,QAuthenticator*))); + connect(m_control, &QAbstractSocket::proxyAuthenticationRequired, + this, &Ftp::proxyAuthentication); int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT; // on connect success try to read the server message... if (iErrorCode == 0) { const char *psz = ftpResponse(-1); if (m_iRespType != 2) { // login not successful, do we have an message text? if (psz[0]) { sErrorMsg = i18n("%1 (Error %2)", host, remoteEncoding()->decode(psz).trimmed()); } iErrorCode = ERR_CANNOT_CONNECT; } } else { if (m_control->error() == QAbstractSocket::HostNotFoundError) { iErrorCode = ERR_UNKNOWN_HOST; } sErrorMsg = QStringLiteral("%1: %2").arg(host, m_control->errorString()); } // if there was a problem - report it ... if (iErrorCode == 0) { // OK, return success return true; } closeConnection(); // clean-up on error error(iErrorCode, sErrorMsg); return false; } /** * Called by @ref openConnection. It logs us in. * @ref m_initialPath is set to the current working directory * if logging on was successful. * * @return true on success. */ bool Ftp::ftpLogin(bool *userChanged) { infoMessage(i18n("Sending login information")); Q_ASSERT(!m_bLoggedOn); QString user(m_user); QString pass(m_pass); if (config()->readEntry("EnableAutoLogin", false)) { QString au = config()->readEntry("autoLoginUser"); if (!au.isEmpty()) { user = au; pass = config()->readEntry("autoLoginPass"); } } AuthInfo info; info.url.setScheme(QStringLiteral("ftp")); info.url.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { info.url.setPort(m_port); } if (!user.isEmpty()) { info.url.setUserName(user); } // Check for cached authentication first and fallback to // anonymous login when no stored credentials are found. if (!config()->readEntry("TryAnonymousLoginFirst", false) && pass.isEmpty() && checkCachedAuthentication(info)) { user = info.username; pass = info.password; } // Try anonymous login if both username/password // information is blank. if (user.isEmpty() && pass.isEmpty()) { user = QStringLiteral(FTP_LOGIN); pass = QStringLiteral(FTP_PASSWD); } QByteArray tempbuf; QString lastServerResponse; int failedAuth = 0; bool promptForRetry = false; // Give the user the option to login anonymously... info.setExtraField(QStringLiteral("anonymous"), false); do { // Check the cache and/or prompt user for password if 1st // login attempt failed OR the user supplied a login name, // but no password. if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) { QString errorMsg; qCDebug(KIO_FTP) << "Prompting user for login info..."; // Ask user if we should retry after when login fails! if (failedAuth > 0 && promptForRetry) { errorMsg = i18n("Message sent:\nLogin using username=%1 and " "password=[hidden]\n\nServer replied:\n%2\n\n" , user, lastServerResponse); } if (user != QStringLiteral(FTP_LOGIN)) { info.username = user; } info.prompt = i18n("You need to supply a username and a password " "to access this site."); info.commentLabel = i18n("Site:"); info.comment = i18n("%1", m_host); info.keepPassword = true; // Prompt the user for persistence as well. info.setModified(false); // Default the modified flag since we reuse authinfo. const bool disablePassDlg = config()->readEntry("DisablePassDlg", false); if (disablePassDlg) { error(ERR_USER_CANCELED, m_host); return false; } const int errorCode = openPasswordDialogV2(info, errorMsg); if (errorCode) { error(errorCode, QString()); return false; } else { // User can decide go anonymous using checkbox if (info.getExtraField(QStringLiteral("anonymous")).toBool()) { user = QStringLiteral(FTP_LOGIN); pass = QStringLiteral(FTP_PASSWD); } else { user = info.username; pass = info.password; } promptForRetry = true; } } tempbuf = "USER "; tempbuf += user.toLatin1(); if (m_proxyURL.isValid()) { tempbuf += '@'; tempbuf += m_host.toLatin1(); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { tempbuf += ':'; tempbuf += QString::number(m_port).toLatin1(); } } qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf; bool loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); bool needPass = (m_iRespCode == 331); // Prompt user for login info if we do not // get back a "230" or "331". if (!loggedIn && !needPass) { lastServerResponse = ftpResponse(0); qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse; ++failedAuth; continue; // Well we failed, prompt the user please!! } if (needPass) { tempbuf = "PASS "; tempbuf += pass.toLatin1(); qCDebug(KIO_FTP) << "Sending Login password: " << "[protected]"; loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); } if (loggedIn) { // Make sure the user name changed flag is properly set. if (userChanged) { *userChanged = (!m_user.isEmpty() && (m_user != user)); } // Do not cache the default login!! if (user != QLatin1String(FTP_LOGIN) && pass != QLatin1String(FTP_PASSWD)) { // Update the username in case it was changed during login. if (!m_user.isEmpty()) { info.url.setUserName(user); m_user = user; } // Cache the password if the user requested it. if (info.keepPassword) { cacheAuthentication(info); } } failedAuth = -1; } else { // some servers don't let you login anymore // if you fail login once, so restart the connection here lastServerResponse = ftpResponse(0); if (!ftpOpenControlConnection()) { return false; } } } while (++failedAuth); qCDebug(KIO_FTP) << "Login OK"; infoMessage(i18n("Login OK")); // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix: // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) { if (!qstrncmp(ftpResponse(0), "215 Windows_NT", 14)) { // should do for any version ftpSendCmd(QByteArrayLiteral("site dirstyle")); // Check if it was already in Unix style // Patch from Keith Refson if (!qstrncmp(ftpResponse(0), "200 MSDOS-like directory output is on", 37)) //It was in Unix style already! { ftpSendCmd(QByteArrayLiteral("site dirstyle")); } // windows won't support chmod before KDE konquers their desktop... m_extControl |= chmodUnknown; } } else { qCWarning(KIO_FTP) << "SYST failed"; } if (config()->readEntry("EnableAutoLoginMacro", false)) { ftpAutoLoginMacro(); } // Get the current working directory qCDebug(KIO_FTP) << "Searching for pwd"; if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "Couldn't issue pwd command"; error(ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ? return false; } QString sTmp = remoteEncoding()->decode(ftpResponse(3)); int iBeg = sTmp.indexOf('"'); int iEnd = sTmp.lastIndexOf('"'); if (iBeg > 0 && iBeg < iEnd) { m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1); if (m_initialPath[0] != '/') { m_initialPath.prepend('/'); } qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath; m_currentPath = m_initialPath; } return true; } void Ftp::ftpAutoLoginMacro() { QString macro = metaData(QStringLiteral("autoLoginMacro")); if (macro.isEmpty()) { return; } const QStringList list = macro.split('\n', QString::SkipEmptyParts); for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it) { if ((*it).startsWith(QLatin1String("init"))) { const QStringList list2 = macro.split('\\', QString::SkipEmptyParts); it = list2.begin(); ++it; // ignore the macro name for (; it != list2.end(); ++it) { // TODO: Add support for arbitrary commands // besides simply changing directory!! if ((*it).startsWith(QLatin1String("cwd"))) { (void)ftpFolder((*it).mid(4), false); } } break; } } } /** * ftpSendCmd - send a command (@p cmd) and read response * * @param maxretries number of time it should retry. Since it recursively * calls itself if it can't read the answer (this happens especially after * timeouts), we need to limit the recursiveness ;-) * * return true if any response received, false on error */ bool Ftp::ftpSendCmd(const QByteArray &cmd, int maxretries) { Q_ASSERT(m_control); // must have control connection socket if (cmd.indexOf('\r') != -1 || cmd.indexOf('\n') != -1) { qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data(); error(ERR_UNSUPPORTED_ACTION, m_host); return false; } // Don't print out the password... bool isPassCmd = (cmd.left(4).toLower() == "pass"); #if 0 if (!isPassCmd) { qDebug() << "send> " << cmd.data(); } else { qDebug() << "send> pass [protected]"; } #endif // Send the message... QByteArray buf = cmd; buf += "\r\n"; // Yes, must use CR/LF - see http://cr.yp.to/ftp/request.html int num = m_control->write(buf); while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) {} // If we were able to successfully send the command, then we will // attempt to read the response. Otherwise, take action to re-attempt // the login based on the maximum number of retries specified... if (num > 0) { ftpResponse(-1); } else { m_iRespType = m_iRespCode = 0; } // If respCh is NULL or the response is 421 (Timed-out), we try to re-send // the command based on the value of maxretries. if ((m_iRespType <= 0) || (m_iRespCode == 421)) { // We have not yet logged on... if (!m_bLoggedOn) { // The command was sent from the ftpLogin function, i.e. we are actually // attempting to login in. NOTE: If we already sent the username, we // return false and let the user decide whether (s)he wants to start from // the beginning... if (maxretries > 0 && !isPassCmd) { closeConnection(); if (ftpOpenConnection(loginDefered)) { ftpSendCmd(cmd, maxretries - 1); } } return false; } else { if (maxretries < 1) { return false; } else { qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection."; closeConnection(); // Close the old connection... openConnection(); // Attempt to re-establish a new connection... if (!m_bLoggedOn) { if (m_control) { // if openConnection succeeded ... qCDebug(KIO_FTP) << "Login failure, aborting"; error(ERR_CANNOT_LOGIN, m_host); closeConnection(); } return false; } qCDebug(KIO_FTP) << "Logged back in, re-issuing command"; // If we were able to login, resend the command... if (maxretries) { maxretries--; } return ftpSendCmd(cmd, maxretries); } } } return true; } /* * ftpOpenPASVDataConnection - set up data connection, using PASV mode * * return 0 if successful, ERR_INTERNAL otherwise * doesn't set error message, since non-pasv mode will always be tried if * this one fails */ int Ftp::ftpOpenPASVDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection // Check that we can do PASV QHostAddress address = m_control->peerAddress(); if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) { return ERR_INTERNAL; // no PASV for non-PF_INET connections } if (m_extControl & pasvUnknown) { return ERR_INTERNAL; // already tried and got "unknown command" } m_bPasv = true; /* Let's PASsiVe*/ if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "PASV attempt failed"; // unknown command? if (m_iRespType == 5) { qCDebug(KIO_FTP) << "disabling use of PASV"; m_extControl |= pasvUnknown; } return ERR_INTERNAL; } // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)' // but anonftpd gives '227 =160,39,200,55,6,245' int i[6]; const char *start = strchr(ftpResponse(3), '('); if (!start) { start = strchr(ftpResponse(3), '='); } if (!start || (sscanf(start, "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 && sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) { qCritical() << "parsing IP and port numbers failed. String parsed: " << start; return ERR_INTERNAL; } // we ignore the host part on purpose for two reasons // a) it might be wrong anyway // b) it would make us being susceptible to a port scanning attack // now connect the data socket ... quint16 port = i[4] << 8 | i[5]; const QString host = (isSocksProxy() ? m_host : address.toString()); m_data = synchronousConnectToHost(host, port); return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL; } /* * ftpOpenEPSVDataConnection - opens a data connection via EPSV */ int Ftp::ftpOpenEPSVDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection QHostAddress address = m_control->peerAddress(); int portnum; if (m_extControl & epsvUnknown) { return ERR_INTERNAL; } m_bPasv = true; if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) { // unknown command? if (m_iRespType == 5) { qCDebug(KIO_FTP) << "disabling use of EPSV"; m_extControl |= epsvUnknown; } return ERR_INTERNAL; } const char *start = strchr(ftpResponse(3), '|'); if (!start || sscanf(start, "|||%d|", &portnum) != 1) { return ERR_INTERNAL; } const QString host = (isSocksProxy() ? m_host : address.toString()); m_data = synchronousConnectToHost(host, portnum); return m_data->isOpen() ? 0 : ERR_INTERNAL; } /* * ftpOpenDataConnection - set up data connection * * The routine calls several ftpOpenXxxxConnection() helpers to find * the best connection mode. If a helper cannot connect if returns * ERR_INTERNAL - so this is not really an error! All other error * codes are treated as fatal, e.g. they are passed back to the caller * who is responsible for calling error(). ftpOpenPortDataConnection * can be called as last try and it does never return ERR_INTERNAL. * * @return 0 if successful, err code otherwise */ int Ftp::ftpOpenDataConnection() { // make sure that we are logged on and have no data connection... Q_ASSERT(m_bLoggedOn); ftpCloseDataConnection(); int iErrCode = 0; int iErrCodePASV = 0; // Remember error code from PASV // First try passive (EPSV & PASV) modes if (!config()->readEntry("DisablePassiveMode", false)) { iErrCode = ftpOpenPASVDataConnection(); if (iErrCode == 0) { return 0; // success } iErrCodePASV = iErrCode; ftpCloseDataConnection(); if (!config()->readEntry("DisableEPSV", false)) { iErrCode = ftpOpenEPSVDataConnection(); if (iErrCode == 0) { return 0; // success } ftpCloseDataConnection(); } // if we sent EPSV ALL already and it was accepted, then we can't // use active connections any more if (m_extControl & epsvAllSent) { return iErrCodePASV; } } // fall back to port mode iErrCode = ftpOpenPortDataConnection(); if (iErrCode == 0) { return 0; // success } ftpCloseDataConnection(); // prefer to return the error code from PASV if any, since that's what should have worked in the first place return iErrCodePASV ? iErrCodePASV : iErrCode; } /* * ftpOpenPortDataConnection - set up data connection * * @return 0 if successful, err code otherwise (but never ERR_INTERNAL * because this is the last connection mode that is tried) */ int Ftp::ftpOpenPortDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection m_bPasv = false; if (m_extControl & eprtUnknown) { return ERR_INTERNAL; } if (!m_server) { m_server = new QTcpServer; m_server->listen(QHostAddress::Any, 0); } if (!m_server->isListening()) { delete m_server; m_server = nullptr; return ERR_CANNOT_LISTEN; } m_server->setMaxPendingConnections(1); QString command; QHostAddress localAddress = m_control->localAddress(); if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) { struct { quint32 ip4; quint16 port; } data; data.ip4 = localAddress.toIPv4Address(); data.port = m_server->serverPort(); unsigned char *pData = reinterpret_cast(&data); command.sprintf("PORT %d,%d,%d,%d,%d,%d", pData[3], pData[2], pData[1], pData[0], pData[5], pData[4]); } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) { command = QStringLiteral("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort()); } if (ftpSendCmd(command.toLatin1()) && (m_iRespType == 2)) { return 0; } delete m_server; m_server = nullptr; return ERR_INTERNAL; } bool Ftp::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset) { int errCode = 0; if (!ftpDataMode(ftpModeFromPath(_path, _mode))) { errCode = ERR_CANNOT_CONNECT; } else { errCode = ftpOpenDataConnection(); } if (errCode != 0) { error(errCode, m_host); return false; } if (_offset > 0) { // send rest command if offset > 0, this applies to retr and stor commands char buf[100]; sprintf(buf, "rest %lld", _offset); if (!ftpSendCmd(buf)) { return false; } if (m_iRespType != 3) { error(ERR_CANNOT_RESUME, _path); // should never happen return false; } } QByteArray tmp = _command; QString errormessage; if (!_path.isEmpty()) { tmp += ' '; tmp += remoteEncoding()->encode(ftpCleanPath(_path)); } if (!ftpSendCmd(tmp) || (m_iRespType != 1)) { if (_offset > 0 && qstrcmp(_command, "retr") == 0 && (m_iRespType == 4)) { errorcode = ERR_CANNOT_RESUME; } // The error here depends on the command errormessage = _path; } else { // Only now we know for sure that we can resume if (_offset > 0 && qstrcmp(_command, "retr") == 0) { canResume(); } if (m_server && !m_data) { qCDebug(KIO_FTP) << "waiting for connection from remote."; m_server->waitForNewConnection(connectTimeout() * 1000); m_data = m_server->nextPendingConnection(); } if (m_data) { qCDebug(KIO_FTP) << "connected with remote."; m_bBusy = true; // cleared in ftpCloseCommand return true; } qCDebug(KIO_FTP) << "no connection received from remote."; errorcode = ERR_CANNOT_ACCEPT; errormessage = m_host; return false; } if (errorcode != KJob::NoError) { error(errorcode, errormessage); } return false; } bool Ftp::ftpCloseCommand() { // first close data sockets (if opened), then read response that // we got for whatever was used in ftpOpenCommand ( should be 226 ) ftpCloseDataConnection(); if (!m_bBusy) { return true; } qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result"; m_bBusy = false; if (!ftpResponse(-1) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message"; return false; } return true; } void Ftp::mkdir(const QUrl &url, int permissions) { if (!ftpOpenConnection(loginImplicit)) { return; } const QByteArray encodedPath(remoteEncoding()->encode(url)); const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size()); if (!ftpSendCmd((QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) { QString currentPath(m_currentPath); // Check whether or not mkdir failed because // the directory already exists... if (ftpFolder(path, false)) { error(ERR_DIR_ALREADY_EXIST, path); // Change the directory back to what it was... (void) ftpFolder(currentPath, false); return; } error(ERR_CANNOT_MKDIR, path); return; } if (permissions != -1) { // chmod the dir we just created, ignoring errors. (void) ftpChmod(path, permissions); } finished(); } void Ftp::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags) { if (!ftpOpenConnection(loginImplicit)) { return; } // The actual functionality is in ftpRename because put needs it if (ftpRename(src.path(), dst.path(), flags)) { finished(); } } bool Ftp::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags) { Q_ASSERT(m_bLoggedOn); // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793). if (!(jobFlags & KIO::Overwrite)) { if (ftpFileExists(dst)) { error(ERR_FILE_ALREADY_EXIST, dst); return false; } } if (ftpFolder(dst, false)) { error(ERR_DIR_ALREADY_EXIST, dst); return false; } // CD into parent folder const int pos = src.lastIndexOf('/'); if (pos >= 0) { if (!ftpFolder(src.left(pos + 1), false)) { return false; } } QByteArray from_cmd = "RNFR "; from_cmd += remoteEncoding()->encode(src.mid(pos + 1)); if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) { error(ERR_CANNOT_RENAME, src); return false; } QByteArray to_cmd = "RNTO "; to_cmd += remoteEncoding()->encode(dst); if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) { error(ERR_CANNOT_RENAME, src); return false; } return true; } void Ftp::del(const QUrl &url, bool isfile) { if (!ftpOpenConnection(loginImplicit)) { return; } // When deleting a directory, we must exit from it first // The last command probably went into it (to stat it) if (!isfile) { ftpFolder(remoteEncoding()->directory(url), false); // ignore errors } QByteArray cmd = isfile ? "DELE " : "RMD "; cmd += remoteEncoding()->encode(url); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { error(ERR_CANNOT_DELETE, url.path()); } else { finished(); } } bool Ftp::ftpChmod(const QString &path, int permissions) { Q_ASSERT(m_bLoggedOn); if (m_extControl & chmodUnknown) { // previous errors? return false; } // we need to do bit AND 777 to get permissions, in case // we were sent a full mode (unlikely) QString cmd = QLatin1String("SITE CHMOD ") + QString::number(permissions & 511, 8 /*octal*/) + ' '; cmd += path; ftpSendCmd(remoteEncoding()->encode(cmd)); if (m_iRespType == 2) { return true; } if (m_iRespCode == 500) { m_extControl |= chmodUnknown; qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling"; } return false; } void Ftp::chmod(const QUrl &url, int permissions) { if (!ftpOpenConnection(loginImplicit)) { return; } if (!ftpChmod(url.path(), permissions)) { error(ERR_CANNOT_CHMOD, url.path()); } else { finished(); } } void Ftp::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir) { Q_ASSERT(entry.count() == 0); // by contract :-) entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ftpEnt.size); entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date.toTime_t()); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, ftpEnt.access); entry.fastInsert(KIO::UDSEntry::UDS_USER, ftpEnt.owner); if (!ftpEnt.group.isEmpty()) { entry.fastInsert(KIO::UDSEntry::UDS_GROUP, ftpEnt.group); } if (!ftpEnt.link.isEmpty()) { entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(QUrl("ftp://host/" + filename)); // Links on ftp sites are often links to dirs, and we have no way to check // that. Let's do like Netscape : assume dirs generally. // But we do this only when the mimetype can't be known from the filename. // --> we do better than Netscape :-) if (mime.isDefault()) { qCDebug(KIO_FTP) << "Setting guessed mime type to inode/directory for " << filename; entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory")); isDir = true; } } entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type); // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime); // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime); } void Ftp::ftpShortStatAnswer(const QString &filename, bool isDir) { UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); if (isDir) { entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); } // No details about size, ownership, group, etc. statEntry(entry); finished(); } void Ftp::ftpStatAnswerNotFound(const QString &path, const QString &filename) { // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source") // When e.g. uploading a file, we still need stat() to return "not found" // when the file doesn't exist. QString statSide = metaData(QStringLiteral("statSide")); qCDebug(KIO_FTP) << "statSide=" << statSide; if (statSide == QLatin1String("source")) { qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing"; // MS Server is incapable of handling "list " in a case insensitive way // But "retr " works. So lie in stat(), to get going... // // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run // where listing permissions are denied, but downloading is still possible. ftpShortStatAnswer(filename, false /*file, not dir*/); return; } error(ERR_DOES_NOT_EXIST, path); } void Ftp::stat(const QUrl &url) { qCDebug(KIO_FTP) << "path=" << url.path(); if (!ftpOpenConnection(loginImplicit)) { return; } const QString path = ftpCleanPath(QDir::cleanPath(url.path())); qCDebug(KIO_FTP) << "cleaned path=" << path; // We can't stat root, but we know it's a dir. if (path.isEmpty() || path == QLatin1String("/")) { UDSEntry entry; //entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) ); entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); entry.fastInsert(KIO::UDSEntry::UDS_USER, QStringLiteral("root")); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QStringLiteral("root")); // no size statEntry(entry); finished(); return; } QUrl tempurl(url); tempurl.setPath(path); // take the clean one QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash); QString parentDir; QString filename = tempurl.fileName(); Q_ASSERT(!filename.isEmpty()); QString search = filename; // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info) // if it doesn't work, it's a file (and then we'll use dir filename) bool isDir = ftpFolder(path, false); // if we're only interested in "file or directory", we should stop here QString sDetails = metaData(QStringLiteral("details")); int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); qCDebug(KIO_FTP) << "details=" << details; if (details == 0) { if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ? // no -> it doesn't exist at all ftpStatAnswerNotFound(path, filename); return; } ftpShortStatAnswer(filename, isDir); // successfully found a dir or a file -> done return; } if (!isDir) { // It is a file or it doesn't exist, try going to parent directory parentDir = tempurl.adjusted(QUrl::RemoveFilename).path(); // With files we can do "LIST " to avoid listing the whole dir listarg = filename; } else { // --- New implementation: // Don't list the parent dir. Too slow, might not show it, etc. // Just return that it's a dir. UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); // No clue about size, ownership, group, etc. statEntry(entry); finished(); return; } // Now cwd the parent dir, to prepare for listing if (!ftpFolder(parentDir, true)) { return; } if (!ftpOpenCommand("list", listarg, 'I', ERR_DOES_NOT_EXIST)) { qCritical() << "COULD NOT LIST"; return; } qCDebug(KIO_FTP) << "Starting of list was ok"; Q_ASSERT(!search.isEmpty() && search != QLatin1String("/")); bool bFound = false; QUrl linkURL; FtpEntry ftpEnt; QList ftpValidateEntList; while (ftpReadDir(ftpEnt)) { if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(0).isSpace()) { ftpValidateEntList.append(ftpEnt); continue; } // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at) // return only the filename when doing "dir /full/path/to/file" if (!bFound) { bFound = maybeEmitStatEntry(ftpEnt, search, filename, isDir); } qCDebug(KIO_FTP) << ftpEnt.name; } for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { FtpEntry &ftpEnt = ftpValidateEntList[i]; fixupEntryName(&ftpEnt); if (maybeEmitStatEntry(ftpEnt, search, filename, isDir)) { break; } } ftpCloseCommand(); // closes the data connection only if (!bFound) { ftpStatAnswerNotFound(path, filename); return; } if (!linkURL.isEmpty()) { if (linkURL == url || linkURL == tempurl) { error(ERR_CYCLIC_LINK, linkURL.toString()); return; } Ftp::stat(linkURL); return; } qCDebug(KIO_FTP) << "stat : finished successfully"; finished(); } bool Ftp::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &search, const QString &filename, bool isDir) { if ((search == ftpEnt.name || filename == ftpEnt.name) && !filename.isEmpty()) { UDSEntry entry; ftpCreateUDSEntry(filename, ftpEnt, entry, isDir); statEntry(entry); return true; } return false; } void Ftp::listDir(const QUrl &url) { qCDebug(KIO_FTP) << url; if (!ftpOpenConnection(loginImplicit)) { return; } // No path specified ? QString path = url.path(); if (path.isEmpty()) { QUrl realURL; realURL.setScheme(QStringLiteral("ftp")); realURL.setUserName(m_user); realURL.setPassword(m_pass); realURL.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { realURL.setPort(m_port); } if (m_initialPath.isEmpty()) { m_initialPath = '/'; } realURL.setPath(m_initialPath); qCDebug(KIO_FTP) << "REDIRECTION to " << realURL; redirection(realURL); finished(); return; } qCDebug(KIO_FTP) << "hunting for path" << path; if (!ftpOpenDir(path)) { if (ftpFileExists(path)) { error(ERR_IS_FILE, path); } else { // not sure which to emit //error( ERR_DOES_NOT_EXIST, path ); error(ERR_CANNOT_ENTER_DIRECTORY, path); } return; } UDSEntry entry; FtpEntry ftpEnt; QList ftpValidateEntList; while (ftpReadDir(ftpEnt)) { qCDebug(KIO_FTP) << ftpEnt.name; //Q_ASSERT( !ftpEnt.name.isEmpty() ); if (!ftpEnt.name.isEmpty()) { if (ftpEnt.name.at(0).isSpace()) { ftpValidateEntList.append(ftpEnt); continue; } //if ( S_ISDIR( (mode_t)ftpEnt.type ) ) // qDebug() << "is a dir"; //if ( !ftpEnt.link.isEmpty() ) // qDebug() << "is a link to " << ftpEnt.link; ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); listEntry(entry); entry.clear(); } } for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { FtpEntry &ftpEnt = ftpValidateEntList[i]; fixupEntryName(&ftpEnt); ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); listEntry(entry); entry.clear(); } ftpCloseCommand(); // closes the data connection only finished(); } void Ftp::slave_status() { qCDebug(KIO_FTP) << "Got slave_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " [" << (m_bLoggedOn ? "Connected" : "Not connected") << "]"; slaveStatus(m_host, m_bLoggedOn); } bool Ftp::ftpOpenDir(const QString &path) { //QString path( _url.path(QUrl::RemoveTrailingSlash) ); // We try to change to this directory first to see whether it really is a directory. // (And also to follow symlinks) QString tmp = path.isEmpty() ? QStringLiteral("/") : path; // We get '550', whether it's a file or doesn't exist... if (!ftpFolder(tmp, false)) { return false; } // Don't use the path in the list command: // We changed into this directory anyway - so it's enough just to send "list". // We use '-a' because the application MAY be interested in dot files. // The only way to really know would be to have a metadata flag for this... // Since some windows ftp server seems not to support the -a argument, we use a fallback here. // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com) // Pass KJob::NoError first because we don't want to emit error before we // have tried all commands. if (!ftpOpenCommand("list -la", QString(), 'I', KJob::NoError)) { if (!ftpOpenCommand("list", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY)) { qCWarning(KIO_FTP) << "Can't open for listing"; return false; } } qCDebug(KIO_FTP) << "Starting of list was ok"; return true; } bool Ftp::ftpReadDir(FtpEntry &de) { Q_ASSERT(m_data); // get a line from the data connection ... while (true) { while (!m_data->canReadLine() && m_data->waitForReadyRead((readTimeout() * 1000))) {} QByteArray data = m_data->readLine(); if (data.size() == 0) { break; } const char *buffer = data.data(); qCDebug(KIO_FTP) << "dir > " << buffer; //Normally the listing looks like // -rw-r--r-- 1 dfaure dfaure 102 Nov 9 12:30 log // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442) // d [RWCEAFMS] Admin 512 Oct 13 2004 PSI // we should always get the following 5 fields ... const char *p_access, *p_junk, *p_owner, *p_group, *p_size; if ((p_access = strtok((char *)buffer, " ")) == nullptr) { continue; } if ((p_junk = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_owner = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_group = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_size = strtok(nullptr, " ")) == nullptr) { continue; } qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size; de.access = 0; if (qstrlen(p_access) == 1 && p_junk[0] == '[') { // Netware de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions } const char *p_date_1, *p_date_2, *p_date_3, *p_name; // A special hack for "/dev". A listing may look like this: // crw-rw-rw- 1 root root 1, 5 Jun 29 1997 zero // So we just ignore the number in front of the ",". Ok, it is a hack :-) if (strchr(p_size, ',') != nullptr) { qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)"; if ((p_size = strtok(nullptr, " ")) == nullptr) { continue; } } // This is needed for ftp servers with a directory listing like this (#375610): // drwxr-xr-x folder 0 Mar 15 15:50 directory_name if (strcmp(p_junk, "folder") == 0) { p_date_1 = p_group; p_date_2 = p_size; p_size = p_owner; p_group = nullptr; p_owner = nullptr; } // Check whether the size we just read was really the size // or a month (this happens when the server lists no group) // Used to be the case on sunsite.uio.no, but not anymore // This is needed for the Netware case, too. else if (!isdigit(*p_size)) { p_date_1 = p_size; p_date_2 = strtok(nullptr, " "); p_size = p_group; p_group = nullptr; qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1; } else { p_date_1 = strtok(nullptr, " "); p_date_2 = strtok(nullptr, " "); qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1; } if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(nullptr, " ")) != nullptr && (p_name = strtok(nullptr, "\r\n")) != nullptr) { { QByteArray tmp(p_name); if (p_access[0] == 'l') { int i = tmp.lastIndexOf(" -> "); if (i != -1) { de.link = remoteEncoding()->decode(p_name + i + 4); tmp.truncate(i); } else { de.link.clear(); } } else { de.link.clear(); } if (tmp[0] == '/') { // listing on ftp://ftp.gnupg.org/ starts with '/' tmp.remove(0, 1); } if (tmp.indexOf('/') != -1) { continue; // Don't trick us! } de.name = remoteEncoding()->decode(tmp); } de.type = S_IFREG; switch (p_access[0]) { case 'd': de.type = S_IFDIR; break; case 's': de.type = S_IFSOCK; break; case 'b': de.type = S_IFBLK; break; case 'c': de.type = S_IFCHR; break; case 'l': de.type = S_IFREG; // we don't set S_IFLNK here. de.link says it. break; default: break; } if (p_access[1] == 'r') { de.access |= S_IRUSR; } if (p_access[2] == 'w') { de.access |= S_IWUSR; } if (p_access[3] == 'x' || p_access[3] == 's') { de.access |= S_IXUSR; } if (p_access[4] == 'r') { de.access |= S_IRGRP; } if (p_access[5] == 'w') { de.access |= S_IWGRP; } if (p_access[6] == 'x' || p_access[6] == 's') { de.access |= S_IXGRP; } if (p_access[7] == 'r') { de.access |= S_IROTH; } if (p_access[8] == 'w') { de.access |= S_IWOTH; } if (p_access[9] == 'x' || p_access[9] == 't') { de.access |= S_IXOTH; } if (p_access[3] == 's' || p_access[3] == 'S') { de.access |= S_ISUID; } if (p_access[6] == 's' || p_access[6] == 'S') { de.access |= S_ISGID; } if (p_access[9] == 't' || p_access[9] == 'T') { de.access |= S_ISVTX; } de.owner = remoteEncoding()->decode(p_owner); de.group = remoteEncoding()->decode(p_group); de.size = charToLongLong(p_size); // Parsing the date is somewhat tricky // Examples : "Oct 6 22:49", "May 13 1999" // First get current date - we need the current month and year QDate currentDate(QDate::currentDate()); int currentMonth = currentDate.month(); int day = currentDate.day(); int month = currentDate.month(); int year = currentDate.year(); int minute = 0; int hour = 0; // Get day number (always second field) if (p_date_2) { day = atoi(p_date_2); } // Get month from first field // NOTE : no, we don't want to use KLocale here // It seems all FTP servers use the English way qCDebug(KIO_FTP) << "Looking for month " << p_date_1; static const char s_months[][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; for (int c = 0; c < 12; c ++) if (!qstrcmp(p_date_1, s_months[c])) { qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1; month = c + 1; break; } // Parse third field if (qstrlen(p_date_3) == 4) { // 4 digits, looks like a year year = atoi(p_date_3); } else { // otherwise, the year is implicit // according to man ls, this happens when it is between than 6 months // old and 1 hour in the future. // So the year is : current year if tm_mon <= currentMonth+1 // otherwise current year minus one // (The +1 is a security for the "+1 hour" at the end of the month issue) if (month > currentMonth + 1) { year--; } // and p_date_3 contains probably a time char *semicolon; if (p_date_3 && (semicolon = (char *)strchr(p_date_3, ':'))) { *semicolon = '\0'; minute = atoi(semicolon + 1); hour = atoi(p_date_3); } else { qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3; } } de.date = QDateTime(QDate(year, month, day), QTime(hour, minute)); qCDebug(KIO_FTP) << de.date; return true; } } // line invalid, loop to get another line return false; } //=============================================================================== // public: get download file from server // helper: ftpGet called from get() and copy() //=============================================================================== void Ftp::get(const QUrl &url) { qCDebug(KIO_FTP) << url; int iError = 0; const StatusCode cs = ftpGet(iError, -1, url, 0); ftpCloseCommand(); // must close command! if (cs == statusSuccess) { finished(); return; } if (iError) { // can have only server side errs error(iError, url.path()); } } Ftp::StatusCode Ftp::ftpGet(int &iError, int iCopyFile, const QUrl &url, KIO::fileoffset_t llOffset) { // Calls error() by itself! if (!ftpOpenConnection(loginImplicit)) { return statusServerError; } // Try to find the size of the file (and check that it exists at // the same time). If we get back a 550, "File does not exist" // or "not a plain file", check if it is a directory. If it is a // directory, return an error; otherwise simply try to retrieve // the request... if (!ftpSize(url.path(), '?') && (m_iRespCode == 550) && ftpFolder(url.path(), false)) { // Ok it's a dir in fact qCDebug(KIO_FTP) << "it is a directory in fact"; iError = ERR_IS_DIRECTORY; return statusServerError; } QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { llOffset = resumeOffset.toLongLong(); qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset; } if (!ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset)) { qCWarning(KIO_FTP) << "Can't open for reading"; return statusServerError; } // Read the size from the response string if (m_size == UnknownSize) { const char *psz = strrchr(ftpResponse(4), '('); if (psz) { m_size = charToLongLong(psz + 1); } if (!m_size) { m_size = UnknownSize; } } // Send the mime-type... if (iCopyFile == -1) { StatusCode status = ftpSendMimeType(iError, url); if (status != statusSuccess) { return status; } } KIO::filesize_t bytesLeft = 0; if (m_size != UnknownSize) { bytesLeft = m_size - llOffset; totalSize(m_size); // emit the total size... } qCDebug(KIO_FTP) << "starting with offset=" << llOffset; KIO::fileoffset_t processed_size = llOffset; QByteArray array; char buffer[maximumIpcSize]; // start with small data chunks in case of a slow data source (modem) // - unfortunately this has a negative impact on performance for large // - files - so we will increase the block size after a while ... int iBlockSize = initialIpcSize; int iBufferCur = 0; while (m_size == UnknownSize || bytesLeft > 0) { // let the buffer size grow if the file is larger 64kByte ... if (processed_size - llOffset > 1024 * 64) { iBlockSize = maximumIpcSize; } // read the data and detect EOF or error ... if (iBlockSize + iBufferCur > (int)sizeof(buffer)) { iBlockSize = sizeof(buffer) - iBufferCur; } if (m_data->bytesAvailable() == 0) { m_data->waitForReadyRead((readTimeout() * 1000)); } int n = m_data->read(buffer + iBufferCur, iBlockSize); if (n <= 0) { // this is how we detect EOF in case of unknown size if (m_size == UnknownSize && n == 0) { break; } // unexpected eof. Happens when the daemon gets killed. iError = ERR_CANNOT_READ; return statusServerError; } processed_size += n; // collect very small data chunks in buffer before processing ... if (m_size != UnknownSize) { bytesLeft -= n; iBufferCur += n; if (iBufferCur < minimumMimeSize && bytesLeft > 0) { processedSize(processed_size); continue; } n = iBufferCur; iBufferCur = 0; } // write output file or pass to data pump ... if (iCopyFile == -1) { array = QByteArray::fromRawData(buffer, n); data(array); array.clear(); } else if ((iError = WriteToFile(iCopyFile, buffer, n)) != 0) { return statusClientError; // client side error } processedSize(processed_size); } qCDebug(KIO_FTP) << "done"; if (iCopyFile == -1) { // must signal EOF to data pump ... data(array); // array is empty and must be empty! } processedSize(m_size == UnknownSize ? processed_size : m_size); return statusSuccess; } #if 0 void Ftp::mimetype(const QUrl &url) { if (!ftpOpenConnection(loginImplicit)) { return; } if (!ftpOpenCommand("retr", url.path(), 'I', ERR_CANNOT_OPEN_FOR_READING, 0)) { qCWarning(KIO_FTP) << "Can't open for reading"; return; } char buffer[ 2048 ]; QByteArray array; // Get one chunk of data only and send it, KIO::Job will determine the // mimetype from it using KMimeMagic int n = m_data->read(buffer, 2048); array.setRawData(buffer, n); data(array); array.resetRawData(buffer, n); qCDebug(KIO_FTP) << "aborting"; ftpAbortTransfer(); qCDebug(KIO_FTP) << "finished"; finished(); qCDebug(KIO_FTP) << "after finished"; } void Ftp::ftpAbortTransfer() { // RFC 959, page 34-35 // IAC (interpret as command) = 255 ; IP (interrupt process) = 254 // DM = 242 (data mark) char msg[4]; // 1. User system inserts the Telnet "Interrupt Process" (IP) signal // in the Telnet stream. msg[0] = (char) 255; //IAC msg[1] = (char) 254; //IP (void) send(sControl, msg, 2, 0); // 2. User system sends the Telnet "Sync" signal. msg[0] = (char) 255; //IAC msg[1] = (char) 242; //DM if (send(sControl, msg, 2, MSG_OOB) != 2) ; // error... // Send ABOR qCDebug(KIO_FTP) << "send ABOR"; QCString buf = "ABOR\r\n"; if (KSocks::self()->write(sControl, buf.data(), buf.length()) <= 0) { error(ERR_CANNOT_WRITE, QString()); return; } // qCDebug(KIO_FTP) << "read resp"; if (readresp() != '2') { error(ERR_CANNOT_READ, QString()); return; } qCDebug(KIO_FTP) << "close sockets"; closeSockets(); } #endif //=============================================================================== // public: put upload file to server // helper: ftpPut called from put() and copy() //=============================================================================== void Ftp::put(const QUrl &url, int permissions, KIO::JobFlags flags) { qCDebug(KIO_FTP) << url; int iError = 0; // iError gets status const StatusCode cs = ftpPut(iError, -1, url, permissions, flags); ftpCloseCommand(); // must close command! if (cs == statusSuccess) { finished(); return; } if (iError) { // can have only server side errs error(iError, url.path()); } } Ftp::StatusCode Ftp::ftpPut(int &iError, int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags) { if (!ftpOpenConnection(loginImplicit)) { return statusServerError; } // Don't use mark partial over anonymous FTP. // My incoming dir allows put but not rename... bool bMarkPartial; if (m_user.isEmpty() || m_user == QLatin1String(FTP_LOGIN)) { bMarkPartial = false; } else { bMarkPartial = config()->readEntry("MarkPartial", true); } QString dest_orig = dest_url.path(); QString dest_part(dest_orig); dest_part += QLatin1String(".part"); if (ftpSize(dest_orig, 'I')) { if (m_size == 0) { // delete files with zero size QByteArray cmd = "DELE "; cmd += remoteEncoding()->encode(dest_orig); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { iError = ERR_CANNOT_DELETE_PARTIAL; return statusServerError; } } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { iError = ERR_FILE_ALREADY_EXIST; return statusServerError; } else if (bMarkPartial) { // when using mark partial, append .part extension if (!ftpRename(dest_orig, dest_part, KIO::Overwrite)) { iError = ERR_CANNOT_RENAME_PARTIAL; return statusServerError; } } // Don't chmod an existing file permissions = -1; } else if (bMarkPartial && ftpSize(dest_part, 'I')) { // file with extension .part exists if (m_size == 0) { // delete files with zero size QByteArray cmd = "DELE "; cmd += remoteEncoding()->encode(dest_part); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { iError = ERR_CANNOT_DELETE_PARTIAL; return statusServerError; } } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { flags |= canResume(m_size) ? KIO::Resume : KIO::DefaultFlags; if (!(flags & KIO::Resume)) { iError = ERR_FILE_ALREADY_EXIST; return statusServerError; } } } else { m_size = 0; } QString dest; // if we are using marking of partial downloads -> add .part extension if (bMarkPartial) { qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig; dest = dest_part; } else { dest = dest_orig; } KIO::fileoffset_t offset = 0; // set the mode according to offset if ((flags & KIO::Resume) && m_size > 0) { offset = m_size; if (iCopyFile != -1) { if (QT_LSEEK(iCopyFile, offset, SEEK_SET) < 0) { iError = ERR_CANNOT_RESUME; return statusClientError; } } } if (! ftpOpenCommand("stor", dest, '?', ERR_CANNOT_WRITE, offset)) { return statusServerError; } qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset; KIO::fileoffset_t processed_size = offset; QByteArray buffer; int result; int iBlockSize = initialIpcSize; // Loop until we got 'dataEnd' do { if (iCopyFile == -1) { dataReq(); // Request for data result = readData(buffer); } else { // let the buffer size grow if the file is larger 64kByte ... if (processed_size - offset > 1024 * 64) { iBlockSize = maximumIpcSize; } buffer.resize(iBlockSize); result = QT_READ(iCopyFile, buffer.data(), buffer.size()); if (result < 0) { iError = ERR_CANNOT_WRITE; } else { buffer.resize(result); } } if (result > 0) { m_data->write(buffer); while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) {} processed_size += result; processedSize(processed_size); } } while (result > 0); if (result != 0) { // error ftpCloseCommand(); // don't care about errors qCDebug(KIO_FTP) << "Error during 'put'. Aborting."; if (bMarkPartial) { // Remove if smaller than minimum size if (ftpSize(dest, 'I') && (processed_size < config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE))) { QByteArray cmd = "DELE "; cmd += remoteEncoding()->encode(dest); (void) ftpSendCmd(cmd); } } return statusServerError; } if (!ftpCloseCommand()) { iError = ERR_CANNOT_WRITE; return statusServerError; } // after full download rename the file back to original name if (bMarkPartial) { qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")"; if (!ftpRename(dest, dest_orig, KIO::Overwrite)) { iError = ERR_CANNOT_RENAME_PARTIAL; return statusServerError; } } // set final permissions if (permissions != -1) { if (m_user == QLatin1String(FTP_LOGIN)) qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???"; // chmod the file we just put if (! ftpChmod(dest_orig, permissions)) { // To be tested //if ( m_user != FTP_LOGIN ) // warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) ); } } return statusSuccess; } /** Use the SIZE command to get the file size. Warning : the size depends on the transfer mode, hence the second arg. */ bool Ftp::ftpSize(const QString &path, char mode) { m_size = UnknownSize; if (!ftpDataMode(mode)) { return false; } QByteArray buf; buf = "SIZE "; buf += remoteEncoding()->encode(path); if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } // skip leading "213 " (response code) QByteArray psz(ftpResponse(4)); if (psz.isEmpty()) { return false; } bool ok = false; m_size = psz.trimmed().toLongLong(&ok); if (!ok) { m_size = UnknownSize; } return true; } bool Ftp::ftpFileExists(const QString &path) { QByteArray buf; buf = "SIZE "; buf += remoteEncoding()->encode(path); if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } // skip leading "213 " (response code) const char *psz = ftpResponse(4); return psz != nullptr; } // Today the differences between ASCII and BINARY are limited to // CR or CR/LF line terminators. Many servers ignore ASCII (like // win2003 -or- vsftp with default config). In the early days of // computing, when even text-files had structure, this stuff was // more important. // Theoretically "list" could return different results in ASCII // and BINARY mode. But again, most servers ignore ASCII here. bool Ftp::ftpDataMode(char cMode) { if (cMode == '?') { cMode = m_bTextMode ? 'A' : 'I'; } else if (cMode == 'a') { cMode = 'A'; } else if (cMode != 'A') { cMode = 'I'; } qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode; if (m_cDataMode == cMode) { return true; } QByteArray buf = "TYPE "; buf += cMode; if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } m_cDataMode = cMode; return true; } bool Ftp::ftpFolder(const QString &path, bool bReportError) { QString newPath = path; int iLen = newPath.length(); if (iLen > 1 && newPath[iLen - 1] == '/') { newPath.truncate(iLen - 1); } qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath; if (m_currentPath == newPath) { return true; } QByteArray tmp = "cwd "; tmp += remoteEncoding()->encode(newPath); if (!ftpSendCmd(tmp)) { return false; // connection failure } if (m_iRespType != 2) { if (bReportError) { error(ERR_CANNOT_ENTER_DIRECTORY, path); } return false; // not a folder } m_currentPath = newPath; return true; } //=============================================================================== // public: copy don't use kio data pump if one side is a local file // helper: ftpCopyPut called from copy() on upload // helper: ftpCopyGet called from copy() on download //=============================================================================== void Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags) { int iError = 0; int iCopyFile = -1; StatusCode cs = statusSuccess; bool bSrcLocal = src.isLocalFile(); bool bDestLocal = dest.isLocalFile(); QString sCopyFile; if (bSrcLocal && !bDestLocal) { // File -> Ftp sCopyFile = src.toLocalFile(); qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path(); cs = ftpCopyPut(iError, iCopyFile, sCopyFile, dest, permissions, flags); if (cs == statusServerError) { sCopyFile = dest.toString(); } } else if (!bSrcLocal && bDestLocal) { // Ftp -> File sCopyFile = dest.toLocalFile(); qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile; cs = ftpCopyGet(iError, iCopyFile, sCopyFile, src, permissions, flags); if (cs == statusServerError) { sCopyFile = src.toString(); } } else { error(ERR_UNSUPPORTED_ACTION, QString()); return; } // perform clean-ups and report error (if any) if (iCopyFile != -1) { QT_CLOSE(iCopyFile); } ftpCloseCommand(); // must close command! if (iError) { error(iError, sCopyFile); } else { finished(); } } Ftp::StatusCode Ftp::ftpCopyPut(int &iError, int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) { // check if source is ok ... QFileInfo info(sCopyFile); bool bSrcExists = info.exists(); if (bSrcExists) { if (info.isDir()) { iError = ERR_IS_DIRECTORY; return statusClientError; } } else { iError = ERR_DOES_NOT_EXIST; return statusClientError; } iCopyFile = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY); if (iCopyFile == -1) { iError = ERR_CANNOT_OPEN_FOR_READING; return statusClientError; } // delegate the real work (iError gets status) ... totalSize(info.size()); #ifdef ENABLE_CAN_RESUME return ftpPut(iError, iCopyFile, url, permissions, flags & ~KIO::Resume); #else return ftpPut(iError, iCopyFile, url, permissions, flags | KIO::Resume); #endif } Ftp::StatusCode Ftp::ftpCopyGet(int &iError, int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) { // check if destination is ok ... QFileInfo info(sCopyFile); const bool bDestExists = info.exists(); if (bDestExists) { if (info.isDir()) { iError = ERR_IS_DIRECTORY; return statusClientError; } if (!(flags & KIO::Overwrite)) { iError = ERR_FILE_ALREADY_EXIST; return statusClientError; } } // do we have a ".part" file? const QString sPart = sCopyFile + QLatin1String(".part"); bool bResume = false; QFileInfo sPartInfo(sPart); const bool bPartExists = sPartInfo.exists(); const bool bMarkPartial = config()->readEntry("MarkPartial", true); const QString dest = bMarkPartial ? sPart : sCopyFile; if (bMarkPartial && bPartExists && sPartInfo.size() > 0) { // must not be a folder! please fix a similar bug in kio_file!! if (sPartInfo.isDir()) { iError = ERR_DIR_ALREADY_EXIST; return statusClientError; // client side error } //doesn't work for copy? -> design flaw? #ifdef ENABLE_CAN_RESUME bResume = canResume(sPartInfo.size()); #else bResume = true; #endif } if (bPartExists && !bResume) { // get rid of an unwanted ".part" file QFile::remove(sPart); } // WABA: Make sure that we keep writing permissions ourselves, // otherwise we can be in for a surprise on NFS. mode_t initialMode; if (permissions != -1) { initialMode = permissions | S_IWUSR; } else { initialMode = 0666; } // open the output file ... KIO::fileoffset_t hCopyOffset = 0; if (bResume) { iCopyFile = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming hCopyOffset = QT_LSEEK(iCopyFile, 0, SEEK_END); if (hCopyOffset < 0) { iError = ERR_CANNOT_RESUME; return statusClientError; // client side error } qCDebug(KIO_FTP) << "resuming at " << hCopyOffset; } else { iCopyFile = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); } if (iCopyFile == -1) { qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile; iError = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING; return statusClientError; } // delegate the real work (iError gets status) ... StatusCode iRes = ftpGet(iError, iCopyFile, url, hCopyOffset); if (QT_CLOSE(iCopyFile) && iRes == statusSuccess) { iError = ERR_CANNOT_WRITE; iRes = statusClientError; } iCopyFile = -1; // handle renaming or deletion of a partial file ... if (bMarkPartial) { if (iRes == statusSuccess) { // rename ".part" on success if (!QFile::rename(sPart, sCopyFile)) { // If rename fails, try removing the destination first if it exists. if (!bDestExists || !(QFile::remove(sCopyFile) && QFile::rename(sPart, sCopyFile))) { qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile; iError = ERR_CANNOT_RENAME_PARTIAL; iRes = statusClientError; } } } else { sPartInfo.refresh(); if (sPartInfo.exists()) { // should a very small ".part" be deleted? int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); if (sPartInfo.size() < size) { QFile::remove(sPart); } } } } if (iRes == statusSuccess) { const QString mtimeStr = metaData(QStringLiteral("modified")); if (!mtimeStr.isEmpty()) { QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); if (dt.isValid()) { qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr; struct utimbuf utbuf; utbuf.actime = sPartInfo.lastRead().toTime_t(); // access time, unchanged utbuf.modtime = dt.toTime_t(); // modification time ::utime(QFile::encodeName(sCopyFile).constData(), &utbuf); } } } return iRes; } Ftp::StatusCode Ftp::ftpSendMimeType(int &iError, const QUrl &url) { const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : m_size); QByteArray buffer(totalSize, '\0'); while (true) { // Wait for content to be available... if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((readTimeout() * 1000))) { iError = ERR_CANNOT_READ; return statusServerError; } const int bytesRead = m_data->peek(buffer.data(), totalSize); // If we got a -1, it must be an error so return an error. if (bytesRead == -1) { iError = ERR_CANNOT_READ; return statusServerError; } // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size // equal to the size we want, then break. if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) { break; } } if (!buffer.isEmpty()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(url.path(), buffer); qCDebug(KIO_FTP) << "Emitting mimetype" << mime.name(); mimeType(mime.name()); // emit the mime type... } return statusSuccess; } void Ftp::proxyAuthentication(const QNetworkProxy &proxy, QAuthenticator *authenticator) { Q_UNUSED(proxy); qCDebug(KIO_FTP) << "Authenticator received -- realm:" << authenticator->realm() << "user:" << authenticator->user(); AuthInfo info; info.url = m_proxyURL; info.realmValue = authenticator->realm(); info.verifyPath = true; //### whatever info.username = authenticator->user(); const bool haveCachedCredentials = checkCachedAuthentication(info); // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, // and it was not successful. see below and saveProxyAuthenticationForSocket(). if (!haveCachedCredentials || m_socketProxyAuth) { // 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(m_control, SIGNAL(connected()), this, SLOT(saveProxyAuthentication())); + connect(m_control, &QAbstractSocket::connected, this, &Ftp::saveProxyAuthentication); //### 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, m_proxyURL.host()); const int errorCode = openPasswordDialogV2(info, i18n("Proxy Authentication Failed.")); if (errorCode) { qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error."; error(errorCode, m_proxyURL.host()); 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); } m_proxyURL.setUserName(info.username); m_proxyURL.setPassword(info.password); } void Ftp::saveProxyAuthentication() { qCDebug(KIO_FTP); - disconnect(m_control, SIGNAL(connected()), this, SLOT(saveProxyAuthentication())); + disconnect(m_control, &QAbstractSocket::connected, this, &Ftp::saveProxyAuthentication); Q_ASSERT(m_socketProxyAuth); if (m_socketProxyAuth) { qCDebug(KIO_FTP) << "-- realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); KIO::AuthInfo a; a.verifyPath = true; a.url = m_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 Ftp::fixupEntryName(FtpEntry *e) { Q_ASSERT(e); if (e->type == S_IFDIR) { if (!ftpFolder(e->name, false)) { QString name(e->name.trimmed()); if (ftpFolder(name, false)) { e->name = name; qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; } else { int index = 0; while (e->name.at(index).isSpace()) { index++; name = e->name.mid(index); if (ftpFolder(name, false)) { qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; e->name = name; break; } } } } } else { if (!ftpFileExists(e->name)) { QString name(e->name.trimmed()); if (ftpFileExists(name)) { e->name = name; qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; } else { int index = 0; while (e->name.at(index).isSpace()) { index++; name = e->name.mid(index); if (ftpFileExists(name)) { qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; e->name = name; break; } } } } } } QTcpSocket *Ftp::synchronousConnectToHost(const QString &host, quint16 port) { QTcpSocket *socket = new QSslSocket; socket->connectToHost(host, port); socket->waitForConnected(connectTimeout() * 1000); return socket; } // needed for JSON file embedding #include "ftp.moc" diff --git a/src/ioslaves/http/http.cpp b/src/ioslaves/http/http.cpp index 75615699..a9f8ba27 100644 --- a/src/ioslaves/http/http.cpp +++ b/src/ioslaves/http/http.cpp @@ -1,5648 +1,5648 @@ /* Copyright (C) 2000-2003 Waldo Bastian Copyright (C) 2000-2002 George Staikos Copyright (C) 2000-2002 Dawit Alemayehu Copyright (C) 2001,2002 Hamish Rodda Copyright (C) 2007 Nick Shaforostoff Copyright (C) 2007-2018 Daniel Nicoletti Copyright (C) 2008,2009 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO delete / do not save very big files; "very big" to be defined #define QT_NO_CAST_FROM_ASCII #include "http.h" #include #include // must be explicitly included for MacOSX #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "httpauthentication.h" #include "kioglobal_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_HTTP) Q_LOGGING_CATEGORY(KIO_HTTP, "kf5.kio.kio_http", QtWarningMsg) // disable debug by default // HeaderTokenizer declarations #include "parsinghelpers.h" //string parsing helpers and HeaderTokenizer implementation #include "parsinghelpers.cpp" // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.http" FILE "http.json") }; static bool supportedProxyScheme(const QString &scheme) { // scheme is supposed to be lowercase return (scheme.startsWith(QLatin1String("http")) || scheme == QLatin1String("socks")); } // see filenameFromUrl(): a sha1 hash is 160 bits static const int s_hashedUrlBits = 160; // this number should always be divisible by eight static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; static const int s_MaxInMemPostBufSize = 256 * 1024; // Write anything over 256 KB to file... using namespace KIO; extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier app.setApplicationName(QStringLiteral("kio_http")); if (argc != 4) { fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n"); exit(-1); } HTTPProtocol slave(argv[1], argv[2], argv[3]); slave.dispatchLoop(); return 0; } /*********************************** Generic utility functions ********************/ static QString toQString(const QByteArray &value) { return QString::fromLatin1(value.constData(), value.size()); } static bool isCrossDomainRequest(const QString &fqdn, const QString &originURL) { //TODO read the RFC if (originURL == QLatin1String("true")) { // Backwards compatibility return true; } QUrl url(originURL); // Document Origin domain QString a = url.host(); // Current request domain QString b = fqdn; if (a == b) { return false; } QStringList la = a.split(QLatin1Char('.'), QString::SkipEmptyParts); QStringList lb = b.split(QLatin1Char('.'), QString::SkipEmptyParts); if (qMin(la.count(), lb.count()) < 2) { return true; // better safe than sorry... } while (la.count() > 2) { la.pop_front(); } while (lb.count() > 2) { lb.pop_front(); } return la != lb; } /* Eliminates any custom header that could potentially alter the request */ static QString sanitizeCustomHTTPHeader(const QString &_header) { QString sanitizedHeaders; const QStringList headers = _header.split(QRegExp(QStringLiteral("[\r\n]"))); for (QStringList::ConstIterator it = headers.begin(); it != headers.end(); ++it) { // Do not allow Request line to be specified and ignore // the other HTTP headers. if (!(*it).contains(QLatin1Char(':')) || (*it).startsWith(QLatin1String("host"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("via"), Qt::CaseInsensitive)) { continue; } sanitizedHeaders += (*it); sanitizedHeaders += QLatin1String("\r\n"); } sanitizedHeaders.chop(2); return sanitizedHeaders; } static bool isPotentialSpoofingAttack(const HTTPProtocol::HTTPRequest &request, const KConfigGroup *config) { qCDebug(KIO_HTTP) << request.url << "response code: " << request.responseCode << "previous response code:" << request.prevResponseCode; if (config->readEntry("no-spoof-check", false)) { return false; } if (request.url.userName().isEmpty()) { return false; } // We already have cached authentication. if (config->readEntry(QStringLiteral("cached-www-auth"), false)) { return false; } const QString userName = config->readEntry(QStringLiteral("LastSpoofedUserName"), QString()); return ((userName.isEmpty() || userName != request.url.userName()) && request.responseCode != 401 && request.prevResponseCode != 401); } // for a given response code, conclude if the response is going to/likely to have a response body static bool canHaveResponseBody(int responseCode, KIO::HTTP_METHOD method) { /* RFC 2616 says... 1xx: false 200: method HEAD: false, otherwise:true 201: true 202: true 203: see 200 204: false 205: false 206: true 300: see 200 301: see 200 302: see 200 303: see 200 304: false 305: probably like 300, RFC seems to expect disconnection afterwards... 306: (reserved), for simplicity do it just like 200 307: see 200 4xx: see 200 5xx :see 200 */ if (responseCode >= 100 && responseCode < 200) { return false; } switch (responseCode) { case 201: case 202: case 206: // RFC 2616 does not mention HEAD in the description of the above. if the assert turns out // to be a problem the response code should probably be treated just like 200 and friends. Q_ASSERT(method != HTTP_HEAD); return true; case 204: case 205: case 304: return false; default: break; } // safe (and for most remaining response codes exactly correct) default return method != HTTP_HEAD; } static bool isEncryptedHttpVariety(const QByteArray &p) { return p == "https" || p == "webdavs"; } static bool isValidProxy(const QUrl &u) { return u.isValid() && !u.host().isEmpty(); } static bool isHttpProxy(const QUrl &u) { return isValidProxy(u) && u.scheme() == QLatin1String("http"); } static QIODevice *createPostBufferDeviceFor(KIO::filesize_t size) { QIODevice *device; if (size > static_cast(s_MaxInMemPostBufSize)) { device = new QTemporaryFile; } else { device = new QBuffer; } if (!device->open(QIODevice::ReadWrite)) { return nullptr; } return device; } QByteArray HTTPProtocol::HTTPRequest::methodString() const { if (!methodStringOverride.isEmpty()) { return (methodStringOverride).toLatin1(); } switch (method) { case HTTP_GET: return "GET"; case HTTP_PUT: return "PUT"; case HTTP_POST: return "POST"; case HTTP_HEAD: return "HEAD"; case HTTP_DELETE: return "DELETE"; case HTTP_OPTIONS: return "OPTIONS"; case DAV_PROPFIND: return "PROPFIND"; case DAV_PROPPATCH: return "PROPPATCH"; case DAV_MKCOL: return "MKCOL"; case DAV_COPY: return "COPY"; case DAV_MOVE: return "MOVE"; case DAV_LOCK: return "LOCK"; case DAV_UNLOCK: return "UNLOCK"; case DAV_SEARCH: return "SEARCH"; case DAV_SUBSCRIBE: return "SUBSCRIBE"; case DAV_UNSUBSCRIBE: return "UNSUBSCRIBE"; case DAV_POLL: return "POLL"; case DAV_NOTIFY: return "NOTIFY"; case DAV_REPORT: return "REPORT"; default: Q_ASSERT(false); return QByteArray(); } } static QString formatHttpDate(const QDateTime &date) { return QLocale::c().toString(date, QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'")); } static bool isAuthenticationRequired(int responseCode) { return (responseCode == 401) || (responseCode == 407); } static void changeProtocolToHttp(QUrl* url) { const QString protocol(url->scheme()); if (protocol == QLatin1String("webdavs")) { url->setScheme(QStringLiteral("https")); } else if (protocol == QLatin1String("webdav")) { url->setScheme(QStringLiteral("http")); } } #define NO_SIZE ((KIO::filesize_t) -1) #if HAVE_STRTOLL #define STRTOLL strtoll #else #define STRTOLL strtol #endif /************************************** HTTPProtocol **********************************************/ HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) : TCPSlaveBase(protocol, pool, app, isEncryptedHttpVariety(protocol)) , m_iSize(NO_SIZE) , m_iPostDataSize(NO_SIZE) , m_isBusy(false) , m_POSTbuf(nullptr) , m_maxCacheAge(DEFAULT_MAX_CACHE_AGE) , m_maxCacheSize(DEFAULT_MAX_CACHE_SIZE) , m_protocol(protocol) , m_wwwAuth(nullptr) , m_triedWwwCredentials(NoCredentials) , m_proxyAuth(nullptr) , m_triedProxyCredentials(NoCredentials) , m_socketProxyAuth(nullptr) , m_networkConfig(nullptr) , m_kioError(0) , m_isLoadingErrorPage(false) , m_remoteRespTimeout(DEFAULT_RESPONSE_TIMEOUT) , m_iEOFRetryCount(0) { reparseConfiguration(); setBlocking(true); connect(socket(), SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), this, SLOT(proxyAuthenticationForSocket(QNetworkProxy,QAuthenticator*))); } HTTPProtocol::~HTTPProtocol() { httpClose(false); } void HTTPProtocol::reparseConfiguration() { qCDebug(KIO_HTTP); delete m_proxyAuth; delete m_wwwAuth; m_proxyAuth = nullptr; m_wwwAuth = nullptr; m_request.proxyUrl.clear(); //TODO revisit m_request.proxyUrls.clear(); TCPSlaveBase::reparseConfiguration(); } void HTTPProtocol::resetConnectionSettings() { m_isEOF = false; m_kioError = 0; m_isLoadingErrorPage = false; } quint16 HTTPProtocol::defaultPort() const { return isEncryptedHttpVariety(m_protocol) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT; } void HTTPProtocol::resetResponseParsing() { m_isRedirection = false; m_isChunked = false; m_iSize = NO_SIZE; clearUnreadBuffer(); m_responseHeaders.clear(); m_contentEncodings.clear(); m_transferEncodings.clear(); m_contentMD5.clear(); m_mimeType.clear(); setMetaData(QStringLiteral("request-id"), m_request.id); } void HTTPProtocol::resetSessionSettings() { // Follow HTTP/1.1 spec and enable keep-alive by default // unless the remote side tells us otherwise or we determine // the persistent link has been terminated by the remote end. m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; m_request.redirectUrl = QUrl(); m_request.useCookieJar = config()->readEntry("Cookies", false); m_request.cacheTag.useCache = config()->readEntry("UseCache", true); m_request.preferErrorPage = config()->readEntry("errorPage", true); const bool noAuth = config()->readEntry("no-auth", false); m_request.doNotWWWAuthenticate = config()->readEntry("no-www-auth", noAuth); m_request.doNotProxyAuthenticate = config()->readEntry("no-proxy-auth", noAuth); m_strCacheDir = config()->readPathEntry("CacheDir", QString()); m_maxCacheAge = config()->readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); m_request.windowId = config()->readEntry("window-id"); m_request.methodStringOverride = metaData(QStringLiteral("CustomHTTPMethod")); m_request.sentMethodString.clear(); qCDebug(KIO_HTTP) << "Window Id =" << m_request.windowId; qCDebug(KIO_HTTP) << "ssl_was_in_use =" << metaData(QStringLiteral("ssl_was_in_use")); m_request.referrer.clear(); // RFC 2616: do not send the referrer if the referrer page was served using SSL and // the current page does not use SSL. if (config()->readEntry("SendReferrer", true) && (isEncryptedHttpVariety(m_protocol) || metaData(QStringLiteral("ssl_was_in_use")) != QLatin1String("TRUE"))) { QUrl refUrl(metaData(QStringLiteral("referrer"))); if (refUrl.isValid()) { // Sanitize QString protocol = refUrl.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, 6, QStringLiteral("http")); refUrl.setScheme(protocol); } if (protocol.startsWith(QLatin1String("http"))) { m_request.referrer = toQString(refUrl.toEncoded(QUrl::RemoveUserInfo | QUrl::RemoveFragment)); } } } if (config()->readEntry("SendLanguageSettings", true)) { m_request.charsets = config()->readEntry("Charsets", DEFAULT_PARTIAL_CHARSET_HEADER); if (!m_request.charsets.contains(QLatin1String("*;"), Qt::CaseInsensitive)) { m_request.charsets += QLatin1String(",*;q=0.5"); } m_request.languages = config()->readEntry("Languages", DEFAULT_LANGUAGE_HEADER); } else { m_request.charsets.clear(); m_request.languages.clear(); } // Adjust the offset value based on the "range-start" meta-data. QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { m_request.offset = resumeOffset.toULongLong(); } else { m_request.offset = 0; } // Same procedure for endoffset. QString resumeEndOffset = metaData(QStringLiteral("range-end")); if (resumeEndOffset.isEmpty()) { resumeEndOffset = metaData(QStringLiteral("resume_until")); // old name } if (!resumeEndOffset.isEmpty()) { m_request.endoffset = resumeEndOffset.toULongLong(); } else { m_request.endoffset = 0; } m_request.disablePassDialog = config()->readEntry("DisablePassDlg", false); m_request.allowTransferCompression = config()->readEntry("AllowCompressedPage", true); m_request.id = metaData(QStringLiteral("request-id")); // Store user agent for this host. if (config()->readEntry("SendUserAgent", true)) { m_request.userAgent = metaData(QStringLiteral("UserAgent")); } else { m_request.userAgent.clear(); } m_request.cacheTag.etag.clear(); m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); m_request.responseCode = 0; m_request.prevResponseCode = 0; delete m_wwwAuth; m_wwwAuth = nullptr; delete m_socketProxyAuth; m_socketProxyAuth = nullptr; m_blacklistedWwwAuthMethods.clear(); m_triedWwwCredentials = NoCredentials; m_blacklistedProxyAuthMethods.clear(); m_triedProxyCredentials = NoCredentials; // Obtain timeout values m_remoteRespTimeout = responseTimeout(); // Bounce back the actual referrer sent setMetaData(QStringLiteral("referrer"), m_request.referrer); // Reset the post data size m_iPostDataSize = NO_SIZE; // Reset the EOF retry counter m_iEOFRetryCount = 0; } void HTTPProtocol::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) { // Reset the webdav-capable flags for this host if (m_request.url.host() != host) { m_davHostOk = m_davHostUnsupported = false; } m_request.url.setHost(host); // is it an IPv6 address? if (host.indexOf(QLatin1Char(':')) == -1) { m_request.encoded_hostname = toQString(QUrl::toAce(host)); } else { int pos = host.indexOf(QLatin1Char('%')); if (pos == -1) { m_request.encoded_hostname = QLatin1Char('[') + host + QLatin1Char(']'); } else // don't send the scope-id in IPv6 addresses to the server { m_request.encoded_hostname = QLatin1Char('[') + host.left(pos) + QLatin1Char(']'); } } m_request.url.setPort((port > 0 && port != defaultPort()) ? port : -1); m_request.url.setUserName(user); m_request.url.setPassword(pass); // On new connection always clear previous proxy information... m_request.proxyUrl.clear(); m_request.proxyUrls.clear(); qCDebug(KIO_HTTP) << "Hostname is now:" << m_request.url.host() << "(" << m_request.encoded_hostname << ")"; } bool HTTPProtocol::maybeSetRequestUrl(const QUrl &u) { qCDebug(KIO_HTTP) << u; m_request.url = u; m_request.url.setPort(u.port(defaultPort()) != defaultPort() ? u.port() : -1); if (u.host().isEmpty()) { error(KIO::ERR_UNKNOWN_HOST, i18n("No host specified.")); return false; } if (u.path().isEmpty()) { QUrl newUrl(u); newUrl.setPath(QStringLiteral("/")); redirection(newUrl); finished(); return false; } return true; } void HTTPProtocol::proceedUntilResponseContent(bool dataInternal /* = false */) { qCDebug(KIO_HTTP); const bool status = proceedUntilResponseHeader() && readBody(dataInternal || m_kioError); // If not an error condition or internal request, close // the connection based on the keep alive settings... if (!m_kioError && !dataInternal) { httpClose(m_request.isKeepAlive); } // if data is required internally or we got error, don't finish, // it is processed before we finish() if (dataInternal || !status) { return; } if (!sendHttpError()) { finished(); } } bool HTTPProtocol::proceedUntilResponseHeader() { qCDebug(KIO_HTTP); // Retry the request until it succeeds or an unrecoverable error occurs. // Recoverable errors are, for example: // - Proxy or server authentication required: Ask for credentials and try again, // this time with an authorization header in the request. // - Server-initiated timeout on keep-alive connection: Reconnect and try again while (true) { if (!sendQuery()) { return false; } if (readResponseHeader()) { // Success, finish the request. break; } // If not loading error page and the response code requires us to resend the query, // then throw away any error message that might have been sent by the server. if (!m_isLoadingErrorPage && isAuthenticationRequired(m_request.responseCode)) { // This gets rid of any error page sent with 401 or 407 authentication required response... readBody(true); } // no success, close the cache file so the cache state is reset - that way most other code // doesn't have to deal with the cache being in various states. cacheFileClose(); if (m_kioError || m_isLoadingErrorPage) { // Unrecoverable error, abort everything. // Also, if we've just loaded an error page there is nothing more to do. // In that case we abort to avoid loops; some webservers manage to send 401 and // no authentication request. Or an auth request we don't understand. setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); return false; } if (!m_request.isKeepAlive) { httpCloseConnection(); m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; } } // Do not save authorization if the current response code is // 4xx (client error) or 5xx (server error). qCDebug(KIO_HTTP) << "Previous Response:" << m_request.prevResponseCode; qCDebug(KIO_HTTP) << "Current Response:" << m_request.responseCode; setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); setMetaData(QStringLiteral("content-type"), m_mimeType); // At this point sendBody() should have delivered any POST data. clearPostDataBuffer(); return true; } void HTTPProtocol::stat(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); if (m_protocol != "webdav" && m_protocol != "webdavs") { QString statSide = metaData(QStringLiteral("statSide")); if (statSide != QLatin1String("source")) { // When uploading we assume the file doesn't exit error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } // When downloading we assume it exists UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, url.fileName()); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody statEntry(entry); finished(); return; } davStatList(url); } void HTTPProtocol::listDir(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url, false); } void HTTPProtocol::davSetRequest(const QByteArray &requestXML) { // insert the document into the POST buffer, kill trailing zero byte cachePostData(requestXML); } void HTTPProtocol::davStatList(const QUrl &url, bool stat) { UDSEntry entry; // check to make sure this host supports WebDAV if (!davHostOk()) { return; } QMimeDatabase db; // Maybe it's a disguised SEARCH... QString query = metaData(QStringLiteral("davSearchQuery")); if (!query.isEmpty()) { QByteArray request = "\r\n"; request.append("\r\n"); request.append(query.toUtf8()); request.append("\r\n"); davSetRequest(request); } else { // We are only after certain features... QByteArray request; request = "" ""; // insert additional XML request from the davRequestResponse metadata if (hasMetaData(QStringLiteral("davRequestResponse"))) { request += metaData(QStringLiteral("davRequestResponse")).toUtf8(); } else { // No special request, ask for default properties request += "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } request += ""; davSetRequest(request); } // WebDAV Stat or List... m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = stat ? 0 : 1; if (!stat) { if (!m_request.url.path().endsWith(QLatin1Char('/'))) { m_request.url.setPath(m_request.url.path() + QLatin1Char('/')); } } proceedUntilResponseContent(true); infoMessage(QLatin1String("")); // Has a redirection already been called? If so, we're done. if (m_isRedirection || m_kioError) { if (m_isRedirection) { davFinished(); } return; } QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); bool hasResponse = false; qCDebug(KIO_HTTP) << endl << multiResponse.toString(2); for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement thisResponse = n.toElement(); if (thisResponse.isNull()) { continue; } hasResponse = true; QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { entry.clear(); const QUrl thisURL(href.text()); // href.text() is a percent-encoded url. if (thisURL.isValid()) { const QUrl adjustedThisURL = thisURL.adjusted(QUrl::StripTrailingSlash); const QUrl adjustedUrl = url.adjusted(QUrl::StripTrailingSlash); // base dir of a listDir(): name should be "." QString name; if (!stat && adjustedThisURL.path() == adjustedUrl.path()) { name = QLatin1Char('.'); } else { name = adjustedThisURL.fileName(); } entry.fastInsert(KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name); } QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat")); davParsePropstats(propstats, entry); // Since a lot of webdav servers seem not to send the content-type information // for the requested directory listings, we attempt to guess the mime-type from // the resource name so long as the resource is not a directory. if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) { QMimeType mime = db.mimeTypeForFile(thisURL.path(), QMimeDatabase::MatchExtension); if (mime.isValid() && !mime.isDefault()) { qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed mime type for" << thisURL.path(); entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime.name()); } } if (stat) { // return an item statEntry(entry); davFinished(); return; } listEntry(entry); } else { qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url; } } if (stat || !hasResponse) { error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } davFinished(); } void HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // WebDAV method m_request.method = method; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } int HTTPProtocol::codeFromResponse(const QString &response) { const int firstSpace = response.indexOf(QLatin1Char(' ')); const int secondSpace = response.indexOf(QLatin1Char(' '), firstSpace + 1); return response.midRef(firstSpace + 1, secondSpace - firstSpace - 1).toInt(); } void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, UDSEntry &entry) { QString mimeType; bool foundExecutable = false; bool isDirectory = false; uint lockCount = 0; uint supportedLockCount = 0; qlonglong quotaUsed = -1; qlonglong quotaAvailable = -1; for (int i = 0; i < propstats.count(); i++) { QDomElement propstat = propstats.item(i).toElement(); QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement(); if (status.isNull()) { // error, no status code in this propstat qCDebug(KIO_HTTP) << "Error, no status code in this propstat"; return; } int code = codeFromResponse(status.text()); if (code != 200) { qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)"; continue; } QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement(); if (prop.isNull()) { qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat."; return; } if (hasMetaData(QStringLiteral("davRequestResponse"))) { QDomDocument doc; doc.appendChild(prop); entry.replace(KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString()); } for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement property = n.toElement(); if (property.isNull()) { continue; } if (property.namespaceURI() != QLatin1String("DAV:")) { // break out - we're only interested in properties from the DAV namespace continue; } if (property.tagName() == QLatin1String("creationdate")) { // Resource creation date. Should be is ISO 8601 format. entry.replace(KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toTime_t()); } else if (property.tagName() == QLatin1String("getcontentlength")) { // Content length (file size) entry.replace(KIO::UDSEntry::UDS_SIZE, property.text().toULong()); } else if (property.tagName() == QLatin1String("displayname")) { // Name suitable for presentation to the user setMetaData(QStringLiteral("davDisplayName"), property.text()); } else if (property.tagName() == QLatin1String("source")) { // Source template location QDomElement source = property.namedItem(QStringLiteral("link")).toElement() .namedItem(QStringLiteral("dst")).toElement(); if (!source.isNull()) { setMetaData(QStringLiteral("davSource"), source.text()); } } else if (property.tagName() == QLatin1String("getcontentlanguage")) { // equiv. to Content-Language header on a GET setMetaData(QStringLiteral("davContentLanguage"), property.text()); } else if (property.tagName() == QLatin1String("getcontenttype")) { // Content type (mime type) // This may require adjustments for other server-side webdav implementations // (tested with Apache + mod_dav 1.0.3) if (property.text() == QLatin1String("httpd/unix-directory")) { isDirectory = true; } else { mimeType = property.text(); } } else if (property.tagName() == QLatin1String("executable")) { // File executable status if (property.text() == QLatin1String("T")) { foundExecutable = true; } } else if (property.tagName() == QLatin1String("getlastmodified")) { // Last modification date entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toTime_t()); } else if (property.tagName() == QLatin1String("getetag")) { // Entity tag setMetaData(QStringLiteral("davEntityTag"), property.text()); } else if (property.tagName() == QLatin1String("supportedlock")) { // Supported locking specifications for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { QDomElement lockEntry = n2.toElement(); if (lockEntry.tagName() == QLatin1String("lockentry")) { QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement(); QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement(); if (!lockScope.isNull() && !lockType.isNull()) { // Lock type was properly specified supportedLockCount++; const QString lockCountStr = QString::number(supportedLockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); setMetaData(QLatin1String("davSupportedLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davSupportedLockType") + lockCountStr, type); } } } } else if (property.tagName() == QLatin1String("lockdiscovery")) { // Lists the available locks davParseActiveLocks(property.elementsByTagName(QStringLiteral("activelock")), lockCount); } else if (property.tagName() == QLatin1String("resourcetype")) { // Resource type. "Specifies the nature of the resource." if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) { // This is a collection (directory) isDirectory = true; } } else if (property.tagName() == QLatin1String("quota-used-bytes")) { // Quota-used-bytes. "Contains the amount of storage already in use." quotaUsed = property.text().toLongLong(); } else if (property.tagName() == QLatin1String("quota-available-bytes")) { // Quota-available-bytes. "Indicates the maximum amount of additional storage available." quotaAvailable = property.text().toLongLong(); } else { qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName(); } } } setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); setMetaData(QStringLiteral("davSupportedLockCount"), QString::number(supportedLockCount)); entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG); if (foundExecutable || isDirectory) { // File was executable, or is a directory. entry.replace(KIO::UDSEntry::UDS_ACCESS, 0700); } else { entry.replace(KIO::UDSEntry::UDS_ACCESS, 0600); } if (!isDirectory && !mimeType.isEmpty()) { entry.replace(KIO::UDSEntry::UDS_MIME_TYPE, mimeType); } if (quotaUsed >= 0 && quotaAvailable >= 0) { // Only used and available storage properties exist, the total storage size has to be calculated. setMetaData(QStringLiteral("total"), QString::number(quotaUsed + quotaAvailable)); setMetaData(QStringLiteral("available"), QString::number(quotaAvailable)); } } void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount) { for (int i = 0; i < activeLocks.count(); i++) { const QDomElement activeLock = activeLocks.item(i).toElement(); lockCount++; // required const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement(); const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement(); const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement(); // optional const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement(); const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement(); const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement(); if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) { // lock was properly specified lockCount++; const QString lockCountStr = QString::number(lockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); const QString depth = lockDepth.text(); setMetaData(QLatin1String("davLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davLockType") + lockCountStr, type); setMetaData(QLatin1String("davLockDepth") + lockCountStr, depth); if (!lockOwner.isNull()) { setMetaData(QLatin1String("davLockOwner") + lockCountStr, lockOwner.text()); } if (!lockTimeout.isNull()) { setMetaData(QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text()); } if (!lockToken.isNull()) { QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement(); if (!tokenVal.isNull()) { setMetaData(QLatin1String("davLockToken") + lockCountStr, tokenVal.text()); } } } } } QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type) { if (type == QLatin1String("dateTime.tz")) { return QDateTime::fromString(input, Qt::ISODate); } else if (type == QLatin1String("dateTime.rfc1123")) { return QDateTime::fromString(input, Qt::RFC2822Date); } // format not advertised... try to parse anyway QDateTime time = QDateTime::fromString(input, Qt::RFC2822Date); if (time.isValid()) { return time; } return QDateTime::fromString(input, Qt::ISODate); } QString HTTPProtocol::davProcessLocks() { if (hasMetaData(QStringLiteral("davLockCount"))) { QString response = QStringLiteral("If:"); int numLocks = metaData(QStringLiteral("davLockCount")).toInt(); bool bracketsOpen = false; for (int i = 0; i < numLocks; i++) { const QString countStr = QString::number(i); if (hasMetaData(QLatin1String("davLockToken") + countStr)) { if (hasMetaData(QLatin1String("davLockURL") + countStr)) { if (bracketsOpen) { response += QLatin1Char(')'); bracketsOpen = false; } response += QLatin1String(" <") + metaData(QLatin1String("davLockURL") + countStr) + QLatin1Char('>'); } if (!bracketsOpen) { response += QLatin1String(" ("); bracketsOpen = true; } else { response += QLatin1Char(' '); } if (hasMetaData(QLatin1String("davLockNot") + countStr)) { response += QLatin1String("Not "); } response += QLatin1Char('<') + metaData(QLatin1String("davLockToken") + countStr) + QLatin1Char('>'); } } if (bracketsOpen) { response += QLatin1Char(')'); } response += QLatin1String("\r\n"); return response; } return QString(); } bool HTTPProtocol::davHostOk() { // FIXME needs to be reworked. Switched off for now. return true; // cached? if (m_davHostOk) { qCDebug(KIO_HTTP) << "true"; return true; } else if (m_davHostUnsupported) { qCDebug(KIO_HTTP) << " false"; davError(-2); return false; } m_request.method = HTTP_OPTIONS; // query the server's capabilities generally, not for a specific URL m_request.url.setPath(QStringLiteral("*")); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; // clear davVersions variable, which holds the response to the DAV: header m_davCapabilities.clear(); proceedUntilResponseHeader(); if (m_davCapabilities.count()) { for (int i = 0; i < m_davCapabilities.count(); i++) { bool ok; uint verNo = m_davCapabilities[i].toUInt(&ok); if (ok && verNo > 0 && verNo < 3) { m_davHostOk = true; qCDebug(KIO_HTTP) << "Server supports DAV version" << verNo; } } if (m_davHostOk) { return true; } } m_davHostUnsupported = true; davError(-2); return false; } // This function is for closing proceedUntilResponseHeader(); requests // Required because there may or may not be further info expected void HTTPProtocol::davFinished() { // TODO: Check with the DAV extension developers httpClose(m_request.isKeepAlive); finished(); } void HTTPProtocol::mkdir(const QUrl &url, int) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_MKCOL; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::get(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_GET; QString tmp(metaData(QStringLiteral("cache"))); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } proceedUntilResponseContent(); } void HTTPProtocol::put(const QUrl &url, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // Webdav hosts are capable of observing overwrite == false if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { error(ERR_FILE_ALREADY_EXIST, QString()); return; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; } } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } void HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; const bool isSourceLocal = src.isLocalFile(); const bool isDestinationLocal = dest.isLocalFile(); if (isSourceLocal && !isDestinationLocal) { copyPut(src, dest, flags); } else { if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http(s)://..." QUrl newDest (dest); changeProtocolToHttp(&newDest); m_request.method = DAV_COPY; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion if (m_request.responseCode == 201 || m_request.responseCode == 204) { davFinished(); } else { davError(); } } } void HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http://..." QUrl newDest(dest); changeProtocolToHttp(&newDest); m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // Work around strict Apache-2 WebDAV implementation which refuses to cooperate // with webdav://host/directory, instead requiring webdav://host/directory/ // (strangely enough it accepts Destination: without a trailing slash) // See BR# 209508 and BR#187970 if (m_request.responseCode == 301) { m_request.url = m_request.redirectUrl; m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; proceedUntilResponseHeader(); } if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::del(const QUrl &url, bool) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_DELETE; m_request.cacheTag.policy = CC_Reload; if (m_protocol.startsWith("webdav")) { //krazy:exclude=strings due to QByteArray m_request.url.setQuery(QString()); if (!proceedUntilResponseHeader()) { return; } // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content // on successful completion. if (m_request.responseCode == 200 || m_request.responseCode == 204 || m_isRedirection) { davFinished(); } else { davError(); } return; } proceedUntilResponseContent(); } void HTTPProtocol::post(const QUrl &url, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_POST; m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } void HTTPProtocol::davLock(const QUrl &url, const QString &scope, const QString &type, const QString &owner) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_LOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; /* Create appropriate lock XML request. */ QDomDocument lockReq; QDomElement lockInfo = lockReq.createElementNS(QStringLiteral("DAV:"), QStringLiteral("lockinfo")); lockReq.appendChild(lockInfo); QDomElement lockScope = lockReq.createElement(QStringLiteral("lockscope")); lockInfo.appendChild(lockScope); lockScope.appendChild(lockReq.createElement(scope)); QDomElement lockType = lockReq.createElement(QStringLiteral("locktype")); lockInfo.appendChild(lockType); lockType.appendChild(lockReq.createElement(type)); if (!owner.isNull()) { QDomElement ownerElement = lockReq.createElement(QStringLiteral("owner")); lockReq.appendChild(ownerElement); QDomElement ownerHref = lockReq.createElement(QStringLiteral("href")); ownerElement.appendChild(ownerHref); ownerHref.appendChild(lockReq.createTextNode(owner)); } // insert the document into the POST buffer cachePostData(lockReq.toByteArray()); proceedUntilResponseContent(true); if (m_request.responseCode == 200) { // success QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement prop = multiResponse.documentElement().namedItem(QStringLiteral("prop")).toElement(); QDomElement lockdiscovery = prop.namedItem(QStringLiteral("lockdiscovery")).toElement(); uint lockCount = 0; davParseActiveLocks(lockdiscovery.elementsByTagName(QStringLiteral("activelock")), lockCount); setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); finished(); } else { davError(); } } void HTTPProtocol::davUnlock(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_UNLOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 200) { finished(); } else { davError(); } } QString HTTPProtocol::davError(int code /* = -1 */, const QString &_url) { bool callError = false; if (code == -1) { code = m_request.responseCode; callError = true; } if (code == -2) { callError = true; } QString url = _url; if (!url.isNull()) { url = m_request.url.toDisplayString(); } QString action, errorString; int errorCode = ERR_SLAVE_DEFINED; // for 412 Precondition Failed QString ow = i18n("Otherwise, the request would have succeeded."); switch (m_request.method) { case DAV_PROPFIND: action = i18nc("request type", "retrieve property values"); break; case DAV_PROPPATCH: action = i18nc("request type", "set property values"); break; case DAV_MKCOL: action = i18nc("request type", "create the requested folder"); break; case DAV_COPY: action = i18nc("request type", "copy the specified file or folder"); break; case DAV_MOVE: action = i18nc("request type", "move the specified file or folder"); break; case DAV_SEARCH: action = i18nc("request type", "search in the specified folder"); break; case DAV_LOCK: action = i18nc("request type", "lock the specified file or folder"); break; case DAV_UNLOCK: action = i18nc("request type", "unlock the specified file or folder"); break; case HTTP_DELETE: action = i18nc("request type", "delete the specified file or folder"); break; case HTTP_OPTIONS: action = i18nc("request type", "query the server's capabilities"); break; case HTTP_GET: action = i18nc("request type", "retrieve the contents of the specified file or folder"); break; case DAV_REPORT: action = i18nc("request type", "run a report in the specified folder"); break; case HTTP_PUT: case HTTP_POST: case HTTP_HEAD: default: // this should not happen, this function is for webdav errors only Q_ASSERT(0); } // default error message if the following code fails errorString = i18nc("%1: code, %2: request type", "An unexpected error (%1) occurred " "while attempting to %2.", code, action); switch (code) { case -2: // internal error: OPTIONS request did not specify DAV compliance // ERR_UNSUPPORTED_PROTOCOL errorString = i18n("The server does not support the WebDAV protocol."); break; case 207: // 207 Multi-status { // our error info is in the returned XML document. // retrieve the XML document // there was an error retrieving the XML document. if (!readBody(true) && m_kioError) { return QString(); } QStringList errors; QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement(); QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response")); for (int i = 0; i < responses.count(); i++) { int errCode; QString errUrl; QDomElement response = responses.item(i).toElement(); QDomElement code = response.namedItem(QStringLiteral("status")).toElement(); if (!code.isNull()) { errCode = codeFromResponse(code.text()); QDomElement href = response.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { errUrl = href.text(); } errors << davError(errCode, errUrl); } } //kError = ERR_SLAVE_DEFINED; errorString = i18nc("%1: request type, %2: url", "An error occurred while attempting to %1, %2. A " "summary of the reasons is below.", action, url); errorString += QLatin1String("
    "); Q_FOREACH (const QString &error, errors) { errorString += QLatin1String("
  • ") + error + QLatin1String("
  • "); } errorString += QLatin1String("
"); } break; case 403: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); break; case 405: // 405 Method Not Allowed if (m_request.method == DAV_MKCOL) { // ERR_DIR_ALREADY_EXIST errorString = url; errorCode = ERR_DIR_ALREADY_EXIST; } break; case 409: // 409 Conflict // ERR_ACCESS_DENIED errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); break; case 412: // 412 Precondition failed if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_ACCESS_DENIED errorString = i18n("The server was unable to maintain the liveness of " "the properties listed in the propertybehavior XML " "element\n or you attempted to overwrite a file while " "requesting that files are not overwritten.\n %1", ow); } else if (m_request.method == DAV_LOCK) { // ERR_ACCESS_DENIED errorString = i18n("The requested lock could not be granted. %1", ow); } break; case 415: // 415 Unsupported Media Type // ERR_ACCESS_DENIED errorString = i18n("The server does not support the request type of the body."); break; case 423: // 423 Locked // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); break; case 425: // 424 Failed Dependency errorString = i18n("This action was prevented by another error."); break; case 502: // 502 Bad Gateway if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_WRITE_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); } break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); break; default: break; } // if ( kError != ERR_SLAVE_DEFINED ) //errorString += " (" + url + ')'; if (callError) { error(errorCode, errorString); } return errorString; } // HTTP generic error static int httpGenericError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; errorString->clear(); if (request.responseCode == 204) { errorCode = ERR_NO_CONTENT; } return errorCode; } // HTTP DELETE specific errors static int httpDelError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; errorString->clear(); switch (responseCode) { case 204: errorCode = ERR_NO_CONTENT; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18n("The resource cannot be deleted."); } return errorCode; } // HTTP PUT specific errors static int httpPutError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; const QString action(i18nc("request type", "upload %1", request.url.toDisplayString())); switch (responseCode) { case 403: case 405: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // 405 Method Not Allowed // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); errorCode = ERR_SLAVE_DEFINED; break; case 409: // 409 Conflict // ERR_ACCESS_DENIED *errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); errorCode = ERR_SLAVE_DEFINED; break; case 423: // 423 Locked // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); errorCode = ERR_SLAVE_DEFINED; break; case 502: // 502 Bad Gateway // ERR_WRITE_ACCESS_DENIED; *errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); errorCode = ERR_SLAVE_DEFINED; break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL *errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); errorCode = ERR_SLAVE_DEFINED; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action); } return errorCode; } bool HTTPProtocol::sendHttpError() { QString errorString; int errorCode = 0; switch (m_request.method) { case HTTP_GET: case HTTP_POST: errorCode = httpGenericError(m_request, &errorString); break; case HTTP_PUT: errorCode = httpPutError(m_request, &errorString); break; case HTTP_DELETE: errorCode = httpDelError(m_request, &errorString); break; default: break; } // Force any message previously shown by the client to be cleared. infoMessage(QLatin1String("")); if (errorCode) { error(errorCode, errorString); return true; } return false; } bool HTTPProtocol::sendErrorPageNotification() { if (!m_request.preferErrorPage) { return false; } if (m_isLoadingErrorPage) { qCWarning(KIO_HTTP) << "called twice during one request, something is probably wrong."; } m_isLoadingErrorPage = true; SlaveBase::errorPage(); return true; } bool HTTPProtocol::isOffline() { if (!m_networkConfig) { m_networkConfig = new QNetworkConfigurationManager(this); } return !m_networkConfig->isOnline(); } void HTTPProtocol::multiGet(const QByteArray &data) { QDataStream stream(data); quint32 n; stream >> n; qCDebug(KIO_HTTP) << n; HTTPRequest saveRequest; if (m_isBusy) { saveRequest = m_request; } resetSessionSettings(); for (unsigned i = 0; i < n; ++i) { QUrl url; stream >> url >> mIncomingMetaData; if (!maybeSetRequestUrl(url)) { continue; } //### should maybe call resetSessionSettings() if the server/domain is // different from the last request! qCDebug(KIO_HTTP) << url; m_request.method = HTTP_GET; m_request.isKeepAlive = true; //readResponseHeader clears it if necessary QString tmp = metaData(QStringLiteral("cache")); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } m_requestQueue.append(m_request); } if (m_isBusy) { m_request = saveRequest; } #if 0 if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); while (it.hasNext()) { m_request = it.next(); it.remove(); proceedUntilResponseContent(); } m_isBusy = false; } #endif if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); // send the requests while (it.hasNext()) { m_request = it.next(); sendQuery(); // save the request state so we can pick it up again in the collection phase it.setValue(m_request); qCDebug(KIO_HTTP) << "check one: isKeepAlive =" << m_request.isKeepAlive; if (m_request.cacheTag.ioMode != ReadFromCache) { m_server.initFrom(m_request); } } // collect the responses //### for the moment we use a hack: instead of saving and restoring request-id // we just count up like ParallelGetJobs does. int requestId = 0; Q_FOREACH (const HTTPRequest &r, m_requestQueue) { m_request = r; qCDebug(KIO_HTTP) << "check two: isKeepAlive =" << m_request.isKeepAlive; setMetaData(QStringLiteral("request-id"), QString::number(requestId++)); sendAndKeepMetaData(); if (!(readResponseHeader() && readBody())) { return; } // the "next job" signal for ParallelGetJob is data of size zero which // readBody() sends without our intervention. qCDebug(KIO_HTTP) << "check three: isKeepAlive =" << m_request.isKeepAlive; httpClose(m_request.isKeepAlive); //actually keep-alive is mandatory for pipelining } finished(); m_requestQueue.clear(); m_isBusy = false; } } ssize_t HTTPProtocol::write(const void *_buf, size_t nbytes) { size_t sent = 0; const char *buf = static_cast(_buf); while (sent < nbytes) { int n = TCPSlaveBase::write(buf + sent, nbytes - sent); if (n < 0) { // some error occurred return -1; } sent += n; } return sent; } void HTTPProtocol::clearUnreadBuffer() { m_unreadBuf.clear(); } // Note: the implementation of unread/readBuffered assumes that unread will only // be used when there is extra data we don't want to handle, and not to wait for more data. void HTTPProtocol::unread(char *buf, size_t size) { // implement LIFO (stack) semantics const int newSize = m_unreadBuf.size() + size; m_unreadBuf.resize(newSize); for (size_t i = 0; i < size; i++) { m_unreadBuf.data()[newSize - i - 1] = buf[i]; } if (size) { //hey, we still have data, closed connection or not! m_isEOF = false; } } size_t HTTPProtocol::readBuffered(char *buf, size_t size, bool unlimited) { size_t bytesRead = 0; if (!m_unreadBuf.isEmpty()) { const int bufSize = m_unreadBuf.size(); bytesRead = qMin((int)size, bufSize); for (size_t i = 0; i < bytesRead; i++) { buf[i] = m_unreadBuf.constData()[bufSize - i - 1]; } m_unreadBuf.truncate(bufSize - bytesRead); // If we have an unread buffer and the size of the content returned by the // server is unknown, e.g. chuncked transfer, return the bytes read here since // we may already have enough data to complete the response and don't want to // wait for more. See BR# 180631. if (unlimited) { return bytesRead; } } if (bytesRead < size) { int rawRead = TCPSlaveBase::read(buf + bytesRead, size - bytesRead); if (rawRead < 1) { m_isEOF = true; return bytesRead; } bytesRead += rawRead; } return bytesRead; } //### this method will detect an n*(\r\n) sequence if it crosses invocations. // it will look (n*2 - 1) bytes before start at most and never before buf, naturally. // supported number of newlines are one and two, in line with HTTP syntax. // return true if numNewlines newlines were found. bool HTTPProtocol::readDelimitedText(char *buf, int *idx, int end, int numNewlines) { Q_ASSERT(numNewlines >= 1 && numNewlines <= 2); char mybuf[64]; //somewhere close to the usual line length to avoid unread()ing too much int pos = *idx; while (pos < end && !m_isEOF) { int step = qMin((int)sizeof(mybuf), end - pos); if (m_isChunked) { //we might be reading the end of the very last chunk after which there is no data. //don't try to read any more bytes than there are because it causes stalls //(yes, it shouldn't stall but it does) step = 1; } size_t bufferFill = readBuffered(mybuf, step); for (size_t i = 0; i < bufferFill; ++i, ++pos) { // we copy the data from mybuf to buf immediately and look for the newlines in buf. // that way we don't miss newlines split over several invocations of this method. buf[pos] = mybuf[i]; // did we just copy one or two times the (usually) \r\n delimiter? // until we find even more broken webservers in the wild let's assume that they either // send \r\n (RFC compliant) or \n (broken) as delimiter... if (buf[pos] == '\n') { bool found = numNewlines == 1; if (!found) { // looking for two newlines // Detect \n\n and \n\r\n. The other cases (\r\n\n, \r\n\r\n) are covered by the first two. found = ((pos >= 1 && buf[pos - 1] == '\n') || (pos >= 2 && buf[pos - 2] == '\n' && buf[pos - 1] == '\r')); } if (found) { i++; // unread bytes *after* CRLF unread(&mybuf[i], bufferFill - i); *idx = pos + 1; return true; } } } } *idx = pos; return false; } static bool isCompatibleNextUrl(const QUrl &previous, const QUrl &now) { if (previous.host() != now.host() || previous.port() != now.port()) { return false; } if (previous.userName().isEmpty() && previous.password().isEmpty()) { return true; } return previous.userName() == now.userName() && previous.password() == now.password(); } bool HTTPProtocol::httpShouldCloseConnection() { qCDebug(KIO_HTTP); if (!isConnected()) { return false; } if (!m_request.proxyUrls.isEmpty() && !isAutoSsl()) { Q_FOREACH (const QString &url, m_request.proxyUrls) { if (url != QLatin1String("DIRECT")) { if (isCompatibleNextUrl(m_server.proxyUrl, QUrl(url))) { return false; } } } return true; } return !isCompatibleNextUrl(m_server.url, m_request.url); } bool HTTPProtocol::httpOpenConnection() { qCDebug(KIO_HTTP); m_server.clear(); // Only save proxy auth information after proxy authentication has // actually taken place, which will set up exactly this connection. disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); clearUnreadBuffer(); int connectError = 0; QString errorString; // Get proxy information... if (m_request.proxyUrls.isEmpty()) { m_request.proxyUrls = config()->readEntry("ProxyUrls", QStringList()); qCDebug(KIO_HTTP) << "Proxy URLs:" << m_request.proxyUrls; } if (m_request.proxyUrls.isEmpty()) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); } else { QList badProxyUrls; Q_FOREACH (const QString &proxyUrl, m_request.proxyUrls) { if (proxyUrl == QLatin1String("DIRECT")) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { //qDebug() << "Connected DIRECT: host=" << m_request.url.host() << "port=" << m_request.url.port(defaultPort()); break; } else { continue; } } const QUrl url(proxyUrl); const QString proxyScheme(url.scheme()); if (!supportedProxyScheme(proxyScheme)) { connectError = ERR_CANNOT_CONNECT; errorString = url.toDisplayString(); badProxyUrls << url; continue; } QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy; if (proxyScheme == QLatin1String("socks")) { proxyType = QNetworkProxy::Socks5Proxy; } else if (isAutoSsl()) { proxyType = QNetworkProxy::HttpProxy; } qCDebug(KIO_HTTP) << "Connecting to proxy: address=" << proxyUrl << "type=" << proxyType; if (proxyType == QNetworkProxy::NoProxy) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(url.host(), url.port(), &errorString); if (connectError == 0) { m_request.proxyUrl = url; //qDebug() << "Connected to proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } //qDebug() << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; } } else { QNetworkProxy proxy(proxyType, url.host(), url.port(), url.userName(), url.password()); QNetworkProxy::setApplicationProxy(proxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { qCDebug(KIO_HTTP) << "Tunneling thru proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } qCDebug(KIO_HTTP) << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); } } } if (!badProxyUrls.isEmpty()) { //TODO: Notify the client of BAD proxy addresses (needed for PAC setups). } } if (connectError != 0) { error(connectError, errorString); return false; } // Disable Nagle's algorithm, i.e turn on TCP_NODELAY. KTcpSocket *sock = qobject_cast(socket()); if (sock) { qCDebug(KIO_HTTP) << "TCP_NODELAY:" << sock->socketOption(QAbstractSocket::LowDelayOption); sock->setSocketOption(QAbstractSocket::LowDelayOption, 1); } m_server.initFrom(m_request); connected(); return true; } bool HTTPProtocol::satisfyRequestFromCache(bool *cacheHasPage) { qCDebug(KIO_HTTP); if (m_request.cacheTag.useCache) { const bool offline = isOffline(); if (offline && m_request.cacheTag.policy != KIO::CC_Reload) { m_request.cacheTag.policy = KIO::CC_CacheOnly; } const bool isCacheOnly = m_request.cacheTag.policy == KIO::CC_CacheOnly; const CacheTag::CachePlan plan = m_request.cacheTag.plan(m_maxCacheAge); bool openForReading = false; if (plan == CacheTag::UseCached || plan == CacheTag::ValidateCached) { openForReading = cacheFileOpenRead(); if (!openForReading && (isCacheOnly || offline)) { // cache-only or offline -> we give a definite answer and it is "no" *cacheHasPage = false; if (isCacheOnly) { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } else if (offline) { error(ERR_CANNOT_CONNECT, m_request.url.toDisplayString()); } return true; } } if (openForReading) { m_request.cacheTag.ioMode = ReadFromCache; *cacheHasPage = true; // return false if validation is required, so a network request will be sent return m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached; } } *cacheHasPage = false; return false; } QString HTTPProtocol::formatRequestUri() const { // Only specify protocol, host and port when they are not already clear, i.e. when // we handle HTTP proxying ourself and the proxy server needs to know them. // Sending protocol/host/port in other cases confuses some servers, and it's not their fault. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QUrl u; QString protocol = m_request.url.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, qstrlen("webdav"), QStringLiteral("http")); } u.setScheme(protocol); u.setHost(m_request.url.host()); // if the URL contained the default port it should have been stripped earlier Q_ASSERT(m_request.url.port() != defaultPort()); u.setPort(m_request.url.port()); u.setPath(m_request.url.path(QUrl::FullyEncoded)); u.setQuery(m_request.url.query(QUrl::FullyEncoded)); return u.toString(QUrl::FullyEncoded); } else { QString result = m_request.url.path(QUrl::FullyEncoded); if (m_request.url.hasQuery()) { result += QLatin1Char('?') + m_request.url.query(QUrl::FullyEncoded); } return result; } } /** * This function is responsible for opening up the connection to the remote * HTTP server and sending the header. If this requires special * authentication or other such fun stuff, then it will handle it. This * function will NOT receive anything from the server, however. This is in * contrast to previous incarnations of 'httpOpen' as this method used to be * called. * * The basic process now is this: * * 1) Open up the socket and port * 2) Format our request/header * 3) Send the header to the remote server * 4) Call sendBody() if the HTTP method requires sending body data */ bool HTTPProtocol::sendQuery() { qCDebug(KIO_HTTP); // Cannot have an https request without autoSsl! This can // only happen if the current installation does not support SSL... if (isEncryptedHttpVariety(m_protocol) && !isAutoSsl()) { error(ERR_UNSUPPORTED_PROTOCOL, toQString(m_protocol)); return false; } // Check the reusability of the current connection. if (httpShouldCloseConnection()) { httpCloseConnection(); } // Create a new connection to the remote machine if we do // not already have one... // NB: the !m_socketProxyAuth condition is a workaround for a proxied Qt socket sometimes // looking disconnected after receiving the initial 407 response. // I guess the Qt socket fails to hide the effect of proxy-connection: close after receiving // the 407 header. if ((!isConnected() && !m_socketProxyAuth)) { if (!httpOpenConnection()) { qCDebug(KIO_HTTP) << "Couldn't connect, oopsie!"; return false; } } m_request.cacheTag.ioMode = NoCache; m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); QString header; bool hasBodyData = false; bool hasDavData = false; { m_request.sentMethodString = m_request.methodString(); header = toQString(m_request.sentMethodString) + QLatin1Char(' '); QString davHeader; // Fill in some values depending on the HTTP method to guide further processing switch (m_request.method) { case HTTP_GET: { bool cacheHasPage = false; if (satisfyRequestFromCache(&cacheHasPage)) { qCDebug(KIO_HTTP) << "cacheHasPage =" << cacheHasPage; return cacheHasPage; } if (!cacheHasPage) { // start a new cache file later if appropriate m_request.cacheTag.ioMode = WriteToCache; } break; } case HTTP_HEAD: break; case HTTP_PUT: case HTTP_POST: hasBodyData = true; break; case HTTP_DELETE: case HTTP_OPTIONS: break; case DAV_PROPFIND: hasDavData = true; davHeader = QStringLiteral("Depth: "); if (hasMetaData(QStringLiteral("davDepth"))) { qCDebug(KIO_HTTP) << "Reading DAV depth from metadata:" << metaData( QStringLiteral("davDepth") ); davHeader += metaData(QStringLiteral("davDepth")); } else { if (m_request.davData.depth == 2) { davHeader += QLatin1String("infinity"); } else { davHeader += QString::number(m_request.davData.depth); } } davHeader += QLatin1String("\r\n"); break; case DAV_PROPPATCH: hasDavData = true; break; case DAV_MKCOL: break; case DAV_COPY: case DAV_MOVE: davHeader = QLatin1String("Destination: ") + m_request.davData.desturl; // infinity depth means copy recursively // (optional for copy -> but is the desired action) davHeader += QLatin1String("\r\nDepth: infinity\r\nOverwrite: "); davHeader += QLatin1Char(m_request.davData.overwrite ? 'T' : 'F'); davHeader += QLatin1String("\r\n"); break; case DAV_LOCK: davHeader = QStringLiteral("Timeout: "); { uint timeout = 0; if (hasMetaData(QStringLiteral("davTimeout"))) { timeout = metaData(QStringLiteral("davTimeout")).toUInt(); } if (timeout == 0) { davHeader += QLatin1String("Infinite"); } else { davHeader += QLatin1String("Seconds-") + QString::number(timeout); } } davHeader += QLatin1String("\r\n"); hasDavData = true; break; case DAV_UNLOCK: davHeader = QLatin1String("Lock-token: ") + metaData(QStringLiteral("davLockToken")) + QLatin1String("\r\n"); break; case DAV_SEARCH: case DAV_REPORT: hasDavData = true; /* fall through */ case DAV_SUBSCRIBE: case DAV_UNSUBSCRIBE: case DAV_POLL: break; default: error(ERR_UNSUPPORTED_ACTION, QString()); return false; } // DAV_POLL; DAV_NOTIFY header += formatRequestUri() + QLatin1String(" HTTP/1.1\r\n"); /* start header */ /* support for virtual hosts and required by HTTP 1.1 */ header += QLatin1String("Host: ") + m_request.encoded_hostname; if (m_request.url.port(defaultPort()) != defaultPort()) { header += QLatin1Char(':') + QString::number(m_request.url.port()); } header += QLatin1String("\r\n"); // Support old HTTP/1.0 style keep-alive header for compatibility // purposes as well as performance improvements while giving end // users the ability to disable this feature for proxy servers that // don't support it, e.g. junkbuster proxy server. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { header += QLatin1String("Proxy-Connection: "); } else { header += QLatin1String("Connection: "); } if (m_request.isKeepAlive) { header += QLatin1String("keep-alive\r\n"); } else { header += QLatin1String("close\r\n"); } if (!m_request.userAgent.isEmpty()) { header += QLatin1String("User-Agent: "); header += m_request.userAgent; header += QLatin1String("\r\n"); } if (!m_request.referrer.isEmpty()) { header += QLatin1String("Referer: "); //Don't try to correct spelling! header += m_request.referrer; header += QLatin1String("\r\n"); } if (m_request.endoffset > m_request.offset) { header += QLatin1String("Range: bytes="); header += KIO::number(m_request.offset); header += QLatin1Char('-'); header += KIO::number(m_request.endoffset); header += 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="); header += KIO::number(m_request.offset); header += 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 += QStringLiteral("If-Modified-Since: ") + httpDate + QStringLiteral("\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); header += 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; header += QLatin1String("\r\n"); } // DoNotTrack feature... if (config()->readEntry("DoNotTrack", false)) { header += QLatin1String("DNT: 1\r\n"); } // Remember that at least one failed (with 401 or 407) request/response // roundtrip is necessary for the server to tell us that it requires // authentication. However, we proactively add authentication headers if when // we have cached credentials to avoid the extra roundtrip where possible. header += authenticationHeader(); if (m_protocol == "webdav" || m_protocol == "webdavs") { header += davProcessLocks(); // add extra webdav headers, if supplied davHeader += metaData(QStringLiteral("davHeader")); // Set content type of webdav data if (hasDavData) { davHeader += QStringLiteral("Content-Type: text/xml; charset=utf-8\r\n"); } // add extra header elements for WebDAV header += davHeader; } } qCDebug(KIO_HTTP) << "============ Sending Header:"; Q_FOREACH (const QString &s, header.split(QStringLiteral("\r\n"), QString::SkipEmptyParts)) { qCDebug(KIO_HTTP) << s; } // End the header iff there is no payload data. If we do have payload data // sendBody() will add another field to the header, Content-Length. if (!hasBodyData && !hasDavData) { header += QStringLiteral("\r\n"); } // Now that we have our formatted header, let's send it! // Clear out per-connection settings... resetConnectionSettings(); // Send the data to the remote machine... const QByteArray headerBytes = header.toLatin1(); ssize_t written = write(headerBytes.constData(), headerBytes.length()); bool sendOk = (written == (ssize_t) headerBytes.length()); if (!sendOk) { qCDebug(KIO_HTTP) << "Connection broken! (" << m_request.url.host() << ")" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "sendOk == false. Connection broken !" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } else { qCDebug(KIO_HTTP) << "sent it!"; } bool res = true; if (hasBodyData || hasDavData) { res = sendBody(); } infoMessage(i18n("%1 contacted. Waiting for reply...", m_request.url.host())); return res; } void HTTPProtocol::forwardHttpResponseHeader(bool forwardImmediately) { // Send the response header if it was requested... if (!config()->readEntry("PropagateHttpHeader", false)) { return; } setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QString(QLatin1Char('\n')))); if (forwardImmediately) { sendMetaData(); } } bool HTTPProtocol::parseHeaderFromCache() { qCDebug(KIO_HTTP); if (!cacheFileReadTextHeader2()) { return false; } Q_FOREACH (const QString &str, m_responseHeaders) { const QString header = str.trimmed(); if (header.startsWith(QLatin1String("content-type:"), Qt::CaseInsensitive)) { int pos = header.indexOf(QLatin1String("charset="), Qt::CaseInsensitive); if (pos != -1) { const QString charset = header.mid(pos + 8).toLower(); m_request.cacheTag.charset = charset; setMetaData(QStringLiteral("charset"), charset); } } else if (header.startsWith(QLatin1String("content-language:"), Qt::CaseInsensitive)) { const QString language = header.mid(17).trimmed().toLower(); setMetaData(QStringLiteral("content-language"), language); } else if (header.startsWith(QLatin1String("content-disposition:"), Qt::CaseInsensitive)) { parseContentDisposition(header.mid(20).toLower()); } } if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), formatHttpDate(m_request.cacheTag.lastModifiedDate)); } // this header comes from the cache, so the response must have been cacheable :) setCacheabilityMetadata(true); qCDebug(KIO_HTTP) << "Emitting mimeType" << m_mimeType; forwardHttpResponseHeader(false); mimeType(m_mimeType); // IMPORTANT: Do not remove the call below or the http response headers will // not be available to the application if this slave is put on hold. forwardHttpResponseHeader(); return true; } void HTTPProtocol::fixupResponseMimetype() { if (m_mimeType.isEmpty()) { return; } qCDebug(KIO_HTTP) << "before fixup" << m_mimeType; // Convert some common mimetypes to standard mimetypes if (m_mimeType == QLatin1String("application/x-targz")) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("image/x-png")) { m_mimeType = QStringLiteral("image/png"); } else if (m_mimeType == QLatin1String("audio/x-mp3") || m_mimeType == QLatin1String("audio/x-mpeg") || m_mimeType == QLatin1String("audio/mp3")) { m_mimeType = QStringLiteral("audio/mpeg"); } else if (m_mimeType == QLatin1String("audio/microsoft-wave")) { m_mimeType = QStringLiteral("audio/x-wav"); } else if (m_mimeType == QLatin1String("image/x-ms-bmp")) { m_mimeType = QStringLiteral("image/bmp"); } // Crypto ones.... else if (m_mimeType == QLatin1String("application/pkix-cert") || m_mimeType == QLatin1String("application/binary-certificate")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } // Prefer application/x-compressed-tar or x-gzpostscript over application/x-gzip. else if (m_mimeType == QLatin1String("application/x-gzip")) { if ((m_request.url.path().endsWith(QLatin1String(".tar.gz"))) || (m_request.url.path().endsWith(QLatin1String(".tar")))) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } if ((m_request.url.path().endsWith(QLatin1String(".ps.gz")))) { m_mimeType = QStringLiteral("application/x-gzpostscript"); } } // Prefer application/x-xz-compressed-tar over application/x-xz for LMZA compressed // tar files. Arch Linux AUR servers notoriously send the wrong mimetype for this. else if (m_mimeType == QLatin1String("application/x-xz")) { if (m_request.url.path().endsWith(QLatin1String(".tar.xz")) || m_request.url.path().endsWith(QLatin1String(".txz"))) { m_mimeType = QStringLiteral("application/x-xz-compressed-tar"); } } // Some webservers say "text/plain" when they mean "application/x-bzip" else if ((m_mimeType == QLatin1String("text/plain")) || (m_mimeType == QLatin1String("application/octet-stream"))) { const QString ext = QFileInfo(m_request.url.path()).suffix().toUpper(); if (ext == QLatin1String("BZ2")) { m_mimeType = QStringLiteral("application/x-bzip"); } else if (ext == QLatin1String("PEM")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } else if (ext == QLatin1String("SWF")) { m_mimeType = QStringLiteral("application/x-shockwave-flash"); } else if (ext == QLatin1String("PLS")) { m_mimeType = QStringLiteral("audio/x-scpls"); } else if (ext == QLatin1String("WMV")) { m_mimeType = QStringLiteral("video/x-ms-wmv"); } else if (ext == QLatin1String("WEBM")) { m_mimeType = QStringLiteral("video/webm"); } else if (ext == QLatin1String("DEB")) { m_mimeType = QStringLiteral("application/x-deb"); } } qCDebug(KIO_HTTP) << "after fixup" << m_mimeType; } void HTTPProtocol::fixupResponseContentEncoding() { // WABA: Correct for tgz files with a gzip-encoding. // They really shouldn't put gzip in the Content-Encoding field! // Web-servers really shouldn't do this: They let Content-Size refer // to the size of the tgz file, not to the size of the tar file, // while the Content-Type refers to "tar" instead of "tgz". if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("gzip")) { if (m_mimeType == QLatin1String("application/x-tar")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("application/postscript")) { // LEONB: Adding another exception for psgz files. // Could we use the mimelnk files instead of hardcoding all this? m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzpostscript"); } else if ((m_request.allowTransferCompression && m_mimeType == QLatin1String("text/html")) || (m_request.allowTransferCompression && m_mimeType != QLatin1String("application/x-compressed-tar") && m_mimeType != QLatin1String("application/x-tgz") && // deprecated name m_mimeType != QLatin1String("application/x-targz") && // deprecated name m_mimeType != QLatin1String("application/x-gzip"))) { // Unzip! } else { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzip"); } } // We can't handle "bzip2" encoding (yet). So if we get something with // bzip2 encoding, we change the mimetype to "application/x-bzip". // Note for future changes: some web-servers send both "bzip2" as // encoding and "application/x-bzip[2]" as mimetype. That is wrong. // currently that doesn't bother us, because we remove the encoding // and set the mimetype to x-bzip anyway. if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("bzip2")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-bzip"); } } #ifdef Q_CC_MSVC // strncasecmp does not exist on windows, have to use _strnicmp static inline int strncasecmp(const char *c1, const char* c2, size_t max) { return _strnicmp(c1, c2, max); } #endif //Return true if the term was found, false otherwise. Advance *pos. //If (*pos + strlen(term) >= end) just advance *pos to end and return false. //This means that users should always search for the shortest terms first. static bool consume(const char input[], int *pos, int end, const char *term) { // note: gcc/g++ is quite good at optimizing away redundant strlen()s int idx = *pos; if (idx + (int)strlen(term) >= end) { *pos = end; return false; } if (strncasecmp(&input[idx], term, strlen(term)) == 0) { *pos = idx + strlen(term); return true; } return false; } /** * This function will read in the return header from the server. It will * not read in the body of the return message. It will also not transmit * the header to our client as the client doesn't need to know the gory * details of HTTP headers. */ bool HTTPProtocol::readResponseHeader() { resetResponseParsing(); if (m_request.cacheTag.ioMode == ReadFromCache && m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached) { // parseHeaderFromCache replaces this method in case of cached content return parseHeaderFromCache(); } try_again: qCDebug(KIO_HTTP); bool upgradeRequired = false; // Server demands that we upgrade to something // This is also true if we ask to upgrade and // the server accepts, since we are now // committed to doing so bool noHeadersFound = false; m_request.cacheTag.charset.clear(); m_responseHeaders.clear(); static const int maxHeaderSize = 128 * 1024; char buffer[maxHeaderSize]; bool cont = false; bool bCanResume = false; if (!isConnected()) { qCDebug(KIO_HTTP) << "No connection."; return false; // Reestablish connection and try again } #if 0 // NOTE: This is unnecessary since TCPSlaveBase::read does the same exact // thing. Plus, if we are unable to read from the socket we need to resend // the request as done below, not error out! Do not assume remote server // will honor persistent connections!! if (!waitForResponse(m_remoteRespTimeout)) { qCDebug(KIO_HTTP) << "Got socket error:" << socket()->errorString(); // No response error error(ERR_SERVER_TIMEOUT, m_request.url.host()); return false; } #endif int bufPos = 0; bool foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 1); if (!foundDelimiter && bufPos < maxHeaderSize) { qCDebug(KIO_HTTP) << "EOF while waiting for header start."; if (m_request.isKeepAlive && m_iEOFRetryCount < 2) { m_iEOFRetryCount++; httpCloseConnection(); // Try to reestablish connection. return false; // Reestablish connection and try again. } if (m_request.method == HTTP_HEAD) { // HACK // Some web-servers fail to respond properly to a HEAD request. // We compensate for their failure to properly implement the HTTP standard // by assuming that they will be sending html. qCDebug(KIO_HTTP) << "HEAD -> returned mimetype:" << DEFAULT_MIME_TYPE; mimeType(QStringLiteral(DEFAULT_MIME_TYPE)); return true; } qCDebug(KIO_HTTP) << "Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } if (!foundDelimiter) { //### buffer too small for first line of header(!) Q_ASSERT(0); } qCDebug(KIO_HTTP) << "============ Received Status Response:"; qCDebug(KIO_HTTP) << QByteArray(buffer, bufPos).trimmed(); HTTP_REV httpRev = HTTP_None; int idx = 0; if (idx != bufPos && buffer[idx] == '<') { qCDebug(KIO_HTTP) << "No valid HTTP header found! Document starts with XML/HTML tag"; // document starts with a tag, assume HTML instead of text/plain m_mimeType = QStringLiteral("text/html"); m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; // put string back unread(buffer, bufPos); goto endParsing; } // "HTTP/1.1" or similar if (consume(buffer, &idx, bufPos, "ICY ")) { httpRev = SHOUTCAST; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "HTTP/")) { if (consume(buffer, &idx, bufPos, "1.0")) { httpRev = HTTP_10; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "1.1")) { httpRev = HTTP_11; } } if (httpRev == HTTP_None && bufPos != 0) { // Remote server does not seem to speak HTTP at all // Put the crap back into the buffer and hope for the best qCDebug(KIO_HTTP) << "DO NOT WANT." << bufPos; unread(buffer, bufPos); if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; goto endParsing; } // response code //### maybe wrong if we need several iterations for this response... //### also, do multiple iterations (cf. try_again) to parse one header work w/ pipelining? if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } skipSpace(buffer, &idx, bufPos); //TODO saner handling of invalid response code strings if (idx != bufPos) { m_request.responseCode = atoi(&buffer[idx]); } else { m_request.responseCode = 200; } // move idx to start of (yet to be fetched) next line, skipping the "OK" idx = bufPos; // (don't bother parsing the "OK", what do we do if it isn't there anyway?) // immediately act on most response codes... // Protect users against bogus username intended to fool them into visiting // sites they had no intention of visiting. if (isPotentialSpoofingAttack(m_request, config())) { qCDebug(KIO_HTTP) << "**** POTENTIAL ADDRESS SPOOFING:" << m_request.url; const int result = messageBox(WarningYesNo, i18nc("@info Security check on url being accessed", "

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

" "

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

", m_request.url.host(), m_request.url.userName()), i18nc("@title:window", "Confirm Website Access")); if (result == SlaveBase::No) { error(ERR_USER_CANCELED, m_request.url.toDisplayString()); return false; } setMetaData(QStringLiteral("{internal~currenthost}LastSpoofedUserName"), m_request.url.userName()); } if (m_request.responseCode != 200 && m_request.responseCode != 304) { m_request.cacheTag.ioMode = NoCache; if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { // Server side errors if (m_request.method == HTTP_HEAD) { ; // Ignore error } else { if (!sendErrorPageNotification()) { error(ERR_INTERNAL_SERVER, m_request.url.toDisplayString()); return false; } } } else if (m_request.responseCode == 416) { // Range not supported m_request.offset = 0; return false; // Try again. } else if (m_request.responseCode == 426) { // Upgrade Required upgradeRequired = true; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { // Any other client errors // Tell that we will only get an error page here. if (!sendErrorPageNotification()) { if (m_request.responseCode == 403) { error(ERR_ACCESS_DENIED, m_request.url.toDisplayString()); } else { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } } } else if (m_request.responseCode >= 301 && m_request.responseCode <= 308) { // NOTE: According to RFC 2616 (section 10.3.[2-4,8]), 301 and 302 // redirects for a POST operation should not be converted to a GET // request. That should only be done for a 303 response. However, // because almost all other client implementations do exactly that // in violation of the spec, many servers have simply adapted to // this way of doing things! Thus, we are forced to do the same // thing here. Otherwise, we loose compatibility and might not be // able to correctly retrieve sites that redirect. switch (m_request.responseCode) { case 301: // Moved Permanently setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); // fall through case 302: // Found if (m_request.sentMethodString == "POST") { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 303: // See Other if (m_request.method != HTTP_HEAD) { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 308: // Permanent Redirect setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); break; default: break; } } else if (m_request.responseCode == 204) { // No content // error(ERR_NO_CONTENT, i18n("Data have been successfully sent.")); // Short circuit and do nothing! // The original handling here was wrong, this is not an error: eg. in the // example of a 204 No Content response to a PUT completing. // return false; } else if (m_request.responseCode == 206) { if (m_request.offset) { bCanResume = true; } } else if (m_request.responseCode == 102) { // Processing (for WebDAV) /*** * This status code is given when the server expects the * command to take significant time to complete. So, inform * the user. */ infoMessage(i18n("Server processing request, please wait...")); cont = true; } else if (m_request.responseCode == 100) { // We got 'Continue' - ignore it cont = true; } } // (m_request.responseCode != 200 && m_request.responseCode != 304) endParsing: bool authRequiresAnotherRoundtrip = false; // Skip the whole header parsing if we got no HTTP headers at all if (!noHeadersFound) { // Auth handling const bool wasAuthError = isAuthenticationRequired(m_request.prevResponseCode); const bool isAuthError = isAuthenticationRequired(m_request.responseCode); const bool sameAuthError = (m_request.responseCode == m_request.prevResponseCode); qCDebug(KIO_HTTP) << "wasAuthError=" << wasAuthError << "isAuthError=" << isAuthError << "sameAuthError=" << sameAuthError; // Not the same authorization error as before and no generic error? // -> save the successful credentials. if (wasAuthError && (m_request.responseCode < 400 || (isAuthError && !sameAuthError))) { saveAuthenticationData(); } // done with the first line; now tokenize the other lines // TODO review use of STRTOLL vs. QByteArray::toInt() foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 2); qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).trimmed(); // Use this to see newlines: //qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).replace("\r", "\\r").replace("\n", "\\n\n"); Q_ASSERT(foundDelimiter); //NOTE because tokenizer will overwrite newlines in case of line continuations in the header // unread(buffer, bufSize) will not generally work anymore. we don't need it either. // either we have a http response line -> try to parse the header, fail if it doesn't work // or we have garbage -> fail. HeaderTokenizer tokenizer(buffer); tokenizer.tokenize(idx, sizeof(buffer)); // Note that not receiving "accept-ranges" means that all bets are off // wrt the server supporting ranges. TokenIterator tIt = tokenizer.iterator("accept-ranges"); if (tIt.hasNext() && tIt.next().toLower().startsWith("none")) { // krazy:exclude=strings bCanResume = false; } tIt = tokenizer.iterator("keep-alive"); while (tIt.hasNext()) { QByteArray ka = tIt.next().trimmed().toLower(); if (ka.startsWith("timeout=")) { // krazy:exclude=strings int ka_timeout = ka.mid(qstrlen("timeout=")).trimmed().toInt(); if (ka_timeout > 0) { m_request.keepAliveTimeout = ka_timeout; } if (httpRev == HTTP_10) { m_request.isKeepAlive = true; } break; // we want to fetch ka timeout only } } // get the size of our data tIt = tokenizer.iterator("content-length"); if (tIt.hasNext()) { m_iSize = STRTOLL(tIt.next().constData(), nullptr, 10); } tIt = tokenizer.iterator("content-location"); if (tIt.hasNext()) { setMetaData(QStringLiteral("content-location"), toQString(tIt.next().trimmed())); } // which type of data do we have? QString mediaValue; QString mediaAttribute; tIt = tokenizer.iterator("content-type"); if (tIt.hasNext()) { QList l = tIt.next().split(';'); if (!l.isEmpty()) { // Assign the mime-type. m_mimeType = toQString(l.first().trimmed().toLower()); if (m_mimeType.startsWith(QLatin1Char('"'))) { m_mimeType.remove(0, 1); } if (m_mimeType.endsWith(QLatin1Char('"'))) { m_mimeType.chop(1); } qCDebug(KIO_HTTP) << "Content-type:" << m_mimeType; l.removeFirst(); } // If we still have text, then it means we have a mime-type with a // parameter (eg: charset=iso-8851) ; so let's get that... Q_FOREACH (const QByteArray &statement, l) { const int index = statement.indexOf('='); if (index <= 0) { mediaAttribute = toQString(statement.mid(0, index)); } else { mediaAttribute = toQString(statement.mid(0, index)); mediaValue = toQString(statement.mid(index + 1)); } mediaAttribute = mediaAttribute.trimmed(); mediaValue = mediaValue.trimmed(); bool quoted = false; if (mediaValue.startsWith(QLatin1Char('"'))) { quoted = true; mediaValue.remove(0, 1); } if (mediaValue.endsWith(QLatin1Char('"'))) { mediaValue.chop(1); } qCDebug(KIO_HTTP) << "Encoding-type:" << mediaAttribute << "=" << mediaValue; if (mediaAttribute == QLatin1String("charset")) { mediaValue = mediaValue.toLower(); m_request.cacheTag.charset = mediaValue; setMetaData(QStringLiteral("charset"), mediaValue); } else { setMetaData(QLatin1String("media-") + mediaAttribute, mediaValue); if (quoted) { setMetaData(QLatin1String("media-") + mediaAttribute + QLatin1String("-kio-quoted"), QStringLiteral("true")); } } } } // content? tIt = tokenizer.iterator("content-encoding"); while (tIt.hasNext()) { // This is so wrong !! No wonder kio_http is stripping the // gzip encoding from downloaded files. This solves multiple // bug reports and caitoo's problem with downloads when such a // header is encountered... // A quote from RFC 2616: // " When present, its (Content-Encoding) value indicates what additional // content have been applied to the entity body, and thus what decoding // mechanism must be applied to obtain the media-type referenced by the // Content-Type header field. Content-Encoding is primarily used to allow // a document to be compressed without loosing the identity of its underlying // media type. Simply put if it is specified, this is the actual mime-type // we should use when we pull the resource !!! addEncoding(toQString(tIt.next()), m_contentEncodings); } // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183 tIt = tokenizer.iterator("content-disposition"); if (tIt.hasNext()) { parseContentDisposition(toQString(tIt.next())); } tIt = tokenizer.iterator("content-language"); if (tIt.hasNext()) { QString language = toQString(tIt.next().trimmed()); if (!language.isEmpty()) { setMetaData(QStringLiteral("content-language"), language); } } tIt = tokenizer.iterator("proxy-connection"); if (tIt.hasNext() && isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QByteArray pc = tIt.next().toLower(); if (pc.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (pc.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } tIt = tokenizer.iterator("link"); if (tIt.hasNext()) { // We only support Link: ; rel="type" so far QStringList link = toQString(tIt.next()).split(QLatin1Char(';'), QString::SkipEmptyParts); if (link.count() == 2) { QString rel = link[1].trimmed(); if (rel.startsWith(QLatin1String("rel=\""))) { rel = rel.mid(5, rel.length() - 6); if (rel.toLower() == QLatin1String("pageservices")) { //### the remove() part looks fishy! QString url = link[0].remove(QRegExp(QStringLiteral("[<>]"))).trimmed(); setMetaData(QStringLiteral("PageServices"), url); } } } } tIt = tokenizer.iterator("p3p"); if (tIt.hasNext()) { // P3P privacy policy information QStringList policyrefs, compact; while (tIt.hasNext()) { QStringList policy = toQString(tIt.next().simplified()) .split(QLatin1Char('='), QString::SkipEmptyParts); if (policy.count() == 2) { if (policy[0].toLower() == QLatin1String("policyref")) { policyrefs << policy[1].remove(QRegExp(QStringLiteral("[\")\']"))).trimmed(); } else if (policy[0].toLower() == QLatin1String("cp")) { // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with // other metadata sent in strings. This could be a bit more // efficient but I'm going for correctness right now. const QString s = policy[1].remove(QRegExp(QStringLiteral("[\")\']"))); const QStringList cps = s.split(QLatin1Char(' '), QString::SkipEmptyParts); compact << cps; } } } if (!policyrefs.isEmpty()) { setMetaData(QStringLiteral("PrivacyPolicy"), policyrefs.join(QStringLiteral("\n"))); } if (!compact.isEmpty()) { setMetaData(QStringLiteral("PrivacyCompactPolicy"), compact.join(QStringLiteral("\n"))); } } // continue only if we know that we're at least HTTP/1.0 if (httpRev == HTTP_11 || httpRev == HTTP_10) { // let them tell us if we should stay alive or not tIt = tokenizer.iterator("connection"); while (tIt.hasNext()) { QByteArray connection = tIt.next().toLower(); if (!(isHttpProxy(m_request.proxyUrl) && !isAutoSsl())) { if (connection.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (connection.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } if (connection.startsWith("upgrade")) { // krazy:exclude=strings if (m_request.responseCode == 101) { // Ok, an upgrade was accepted, now we must do it upgradeRequired = true; } else if (upgradeRequired) { // 426 // Nothing to do since we did it above already } } } // what kind of encoding do we have? transfer? tIt = tokenizer.iterator("transfer-encoding"); while (tIt.hasNext()) { // If multiple encodings have been applied to an entity, the // transfer-codings MUST be listed in the order in which they // were applied. addEncoding(toQString(tIt.next().trimmed()), m_transferEncodings); } // md5 signature tIt = tokenizer.iterator("content-md5"); if (tIt.hasNext()) { m_contentMD5 = toQString(tIt.next().trimmed()); } // *** Responses to the HTTP OPTIONS method follow // WebDAV capabilities tIt = tokenizer.iterator("dav"); while (tIt.hasNext()) { m_davCapabilities << toQString(tIt.next()); } // *** Responses to the HTTP OPTIONS method finished } // Now process the HTTP/1.1 upgrade QStringList upgradeOffers; tIt = tokenizer.iterator("upgrade"); if (tIt.hasNext()) { // Now we have to check to see what is offered for the upgrade QString offered = toQString(tIt.next()); upgradeOffers = offered.split(QRegExp(QStringLiteral("[ \n,\r\t]")), QString::SkipEmptyParts); } Q_FOREACH (const QString &opt, upgradeOffers) { if (opt == QLatin1String("TLS/1.0")) { if (!startSsl() && upgradeRequired) { error(ERR_UPGRADE_REQUIRED, opt); return false; } } else if (opt == QLatin1String("HTTP/1.1")) { httpRev = HTTP_11; } else if (upgradeRequired) { // we are told to do an upgrade we don't understand error(ERR_UPGRADE_REQUIRED, opt); return false; } } // Harvest cookies (mmm, cookie fields!) QByteArray cookieStr; // In case we get a cookie. tIt = tokenizer.iterator("set-cookie"); while (tIt.hasNext()) { cookieStr += "Set-Cookie: "; cookieStr += tIt.next(); cookieStr += '\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(QString(QLatin1Char('\n')))); } // Let the app know about the mime-type iff this is not a redirection and // the mime-type string is not empty. if (!m_isRedirection && m_request.responseCode != 204 && (!m_mimeType.isEmpty() || m_request.method == HTTP_HEAD) && !m_kioError && (m_isLoadingErrorPage || !authRequiresAnotherRoundtrip)) { qCDebug(KIO_HTTP) << "Emitting mimetype " << m_mimeType; mimeType(m_mimeType); } // IMPORTANT: Do not move the function call below before doing any // redirection. Otherwise it might mess up some sites, see BR# 150904. forwardHttpResponseHeader(); if (m_request.method == HTTP_HEAD) { return true; } return !authRequiresAnotherRoundtrip; // return true if no more credentials need to be sent } void HTTPProtocol::parseContentDisposition(const QString &disposition) { const QMap parameters = contentDispositionParser(disposition); QMap::const_iterator i = parameters.constBegin(); while (i != parameters.constEnd()) { setMetaData(QLatin1String("content-disposition-") + i.key(), i.value()); qCDebug(KIO_HTTP) << "Content-Disposition:" << i.key() << "=" << i.value(); ++i; } } void HTTPProtocol::addEncoding(const QString &_encoding, QStringList &encs) { QString encoding = _encoding.trimmed().toLower(); // Identity is the same as no encoding if (encoding == QLatin1String("identity")) { return; } else if (encoding == QLatin1String("8bit")) { // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de return; } else if (encoding == QLatin1String("chunked")) { m_isChunked = true; // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints? //if ( m_cmd != CMD_COPY ) m_iSize = NO_SIZE; } else if ((encoding == QLatin1String("x-gzip")) || (encoding == QLatin1String("gzip"))) { encs.append(QStringLiteral("gzip")); } else if ((encoding == QLatin1String("x-bzip2")) || (encoding == QLatin1String("bzip2"))) { encs.append(QStringLiteral("bzip2")); // Not yet supported! } else if ((encoding == QLatin1String("x-deflate")) || (encoding == QLatin1String("deflate"))) { encs.append(QStringLiteral("deflate")); } else { qCDebug(KIO_HTTP) << "Unknown encoding encountered. " << "Please write code. Encoding =" << encoding; } } void HTTPProtocol::cacheParseResponseHeader(const HeaderTokenizer &tokenizer) { if (!m_request.cacheTag.useCache) { return; } // might have to add more response codes if (m_request.responseCode != 200 && m_request.responseCode != 304) { return; } m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); const QDateTime currentDate = QDateTime::currentDateTime(); bool mayCache = m_request.cacheTag.ioMode != NoCache; TokenIterator tIt = tokenizer.iterator("last-modified"); if (tIt.hasNext()) { m_request.cacheTag.lastModifiedDate = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); //### might be good to canonicalize the date by using QDateTime::toString() if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), toQString(tIt.current())); } } // determine from available information when the response was served by the origin server { QDateTime dateHeader; tIt = tokenizer.iterator("date"); if (tIt.hasNext()) { dateHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); // -1 on error } qint64 ageHeader = 0; tIt = tokenizer.iterator("age"); if (tIt.hasNext()) { ageHeader = tIt.next().toLongLong(); // 0 on error } if (dateHeader.isValid()) { m_request.cacheTag.servedDate = dateHeader; } else if (ageHeader) { m_request.cacheTag.servedDate = currentDate.addSecs(-ageHeader); } else { m_request.cacheTag.servedDate = currentDate; } } bool hasCacheDirective = false; // determine when the response "expires", i.e. becomes stale and needs revalidation { // (we also parse other cache directives here) qint64 maxAgeHeader = 0; tIt = tokenizer.iterator("cache-control"); while (tIt.hasNext()) { QByteArray cacheStr = tIt.next().toLower(); if (cacheStr.startsWith("no-cache") || cacheStr.startsWith("no-store")) { // krazy:exclude=strings // Don't put in cache mayCache = false; hasCacheDirective = true; } else if (cacheStr.startsWith("max-age=")) { // krazy:exclude=strings QByteArray ba = cacheStr.mid(qstrlen("max-age=")).trimmed(); bool ok = false; maxAgeHeader = ba.toLongLong(&ok); if (ok) { hasCacheDirective = true; } } } QDateTime expiresHeader; tIt = tokenizer.iterator("expires"); if (tIt.hasNext()) { expiresHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); qCDebug(KIO_HTTP) << "parsed expire date from 'expires' header:" << tIt.current(); } if (maxAgeHeader) { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(maxAgeHeader); } else if (expiresHeader.isValid()) { m_request.cacheTag.expireDate = expiresHeader; } else { // heuristic expiration date if (m_request.cacheTag.lastModifiedDate.isValid()) { // expAge is following the RFC 2616 suggestion for heuristic expiration qint64 expAge = (m_request.cacheTag.lastModifiedDate.secsTo(m_request.cacheTag.servedDate)) / 10; // not in the RFC: make sure not to have a huge heuristic cache lifetime expAge = qMin(expAge, qint64(3600 * 24)); m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(expAge); } else { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(DEFAULT_CACHE_EXPIRE); } } // make sure that no future clock monkey business causes the cache entry to un-expire if (m_request.cacheTag.expireDate < currentDate) { m_request.cacheTag.expireDate.setMSecsSinceEpoch(0); // January 1, 1970 :) } } tIt = tokenizer.iterator("etag"); if (tIt.hasNext()) { QString prevEtag = m_request.cacheTag.etag; m_request.cacheTag.etag = toQString(tIt.next()); if (m_request.cacheTag.etag != prevEtag && m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "304 Not Modified but new entity tag - I don't think this is legal HTTP."; } } // whoops.. we received a warning tIt = tokenizer.iterator("warning"); if (tIt.hasNext()) { //Don't use warning() here, no need to bother the user. //Those warnings are mostly about caches. infoMessage(toQString(tIt.next())); } // Cache management (HTTP 1.0) tIt = tokenizer.iterator("pragma"); while (tIt.hasNext()) { if (tIt.next().toLower().startsWith("no-cache")) { // krazy:exclude=strings mayCache = false; hasCacheDirective = true; } } // The deprecated Refresh Response tIt = tokenizer.iterator("refresh"); if (tIt.hasNext()) { mayCache = false; setMetaData(QStringLiteral("http-refresh"), toQString(tIt.next().trimmed())); } // We don't cache certain text objects if (m_mimeType.startsWith(QLatin1String("text/")) && (m_mimeType != QLatin1String("text/css")) && (m_mimeType != QLatin1String("text/x-javascript")) && !hasCacheDirective) { // Do not cache secure pages or pages // originating from password protected sites // unless the webserver explicitly allows it. if (isUsingSsl() || m_wwwAuth) { mayCache = false; } } // note that we've updated cacheTag, so the plan() is with current data if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { qCDebug(KIO_HTTP) << "Cache needs validation"; if (m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "...was revalidated by response code but not by updated expire times. " "We're going to set the expire date to 60 seconds in the future..."; m_request.cacheTag.expireDate = currentDate.addSecs(60); if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { // "apparently" because we /could/ have made an error ourselves, but the errors I // witnessed were all the server's fault. qCDebug(KIO_HTTP) << "this proxy or server apparently sends bogus expiry information."; } } } // validation handling if (mayCache && m_request.responseCode == 200 && !m_mimeType.isEmpty()) { qCDebug(KIO_HTTP) << "Cache, adding" << m_request.url; // ioMode can still be ReadFromCache here if we're performing a conditional get // aka validation m_request.cacheTag.ioMode = WriteToCache; if (!cacheFileOpenWrite()) { qCDebug(KIO_HTTP) << "Error creating cache entry for" << m_request.url << "!"; } m_maxCacheSize = config()->readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE); } else if (m_request.responseCode == 304 && m_request.cacheTag.file) { if (!mayCache) { qCDebug(KIO_HTTP) << "This webserver is confused about the cacheability of the data it sends."; } // the cache file should still be open for reading, see satisfyRequestFromCache(). Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); } else { cacheFileClose(); } setCacheabilityMetadata(mayCache); } void HTTPProtocol::setCacheabilityMetadata(bool cachingAllowed) { if (!cachingAllowed) { setMetaData(QStringLiteral("no-cache"), QStringLiteral("true")); setMetaData(QStringLiteral("expire-date"), QStringLiteral("1")); // Expired } else { QString tmp; tmp.setNum(m_request.cacheTag.expireDate.toTime_t()); setMetaData(QStringLiteral("expire-date"), tmp); // slightly changed semantics from old creationDate, probably more correct now tmp.setNum(m_request.cacheTag.servedDate.toTime_t()); setMetaData(QStringLiteral("cache-creation-date"), tmp); } } bool HTTPProtocol::sendCachedBody() { infoMessage(i18n("Sending data to %1", m_request.url.host())); const qint64 size = m_POSTbuf->size(); QByteArray cLength("Content-Length: "); cLength += QByteArray::number(size); cLength += "\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())); QByteArray cLength("Content-Length: "); cLength += QByteArray::number(m_iPostDataSize); cLength += "\r\n\r\n"; qCDebug(KIO_HTTP) << cLength.trimmed(); // Send the content length... bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size()); if (!sendOk) { // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "Connection broken while sending POST content size to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // Send the amount totalSize(m_iPostDataSize); // If content-length is 0, then do nothing but simply return true. if (m_iPostDataSize == 0) { return true; } sendOk = true; KIO::filesize_t bytesSent = 0; while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); // On done... if (bytesRead == 0) { sendOk = (bytesSent == m_iPostDataSize); break; } // On error return false... if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); sendOk = false; break; } // Cache the POST data in case of a repost request. cachePostData(buffer); // This will only happen if transmitting the data fails, so we will simply // cache the content locally for the potential re-transmit... if (!sendOk) { continue; } if (write(buffer.data(), bytesRead) == static_cast(bytesRead)) { bytesSent += bytesRead; processedSize(bytesSent); // Send update status... continue; } qCDebug(KIO_HTTP) << "Connection broken while sending POST content to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); sendOk = false; } return sendOk; } void HTTPProtocol::httpClose(bool keepAlive) { qCDebug(KIO_HTTP) << "keepAlive =" << keepAlive; cacheFileClose(); // Only allow persistent connections for GET requests. // NOTE: we might even want to narrow this down to non-form // based submit requests which will require a meta-data from // khtml. if (keepAlive) { if (!m_request.keepAliveTimeout) { m_request.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; } else if (m_request.keepAliveTimeout > 2 * DEFAULT_KEEP_ALIVE_TIMEOUT) { m_request.keepAliveTimeout = 2 * DEFAULT_KEEP_ALIVE_TIMEOUT; } qCDebug(KIO_HTTP) << "keep alive (" << m_request.keepAliveTimeout << ")"; QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << int(99); // special: Close connection setTimeoutSpecialCommand(m_request.keepAliveTimeout, data); return; } httpCloseConnection(); } void HTTPProtocol::closeConnection() { qCDebug(KIO_HTTP); httpCloseConnection(); } void HTTPProtocol::httpCloseConnection() { qCDebug(KIO_HTTP); m_server.clear(); disconnectFromHost(); clearUnreadBuffer(); setTimeoutSpecialCommand(-1); // Cancel any connection timeout } void HTTPProtocol::slave_status() { qCDebug(KIO_HTTP); if (!isConnected()) { httpCloseConnection(); } slaveStatus(m_server.url.host(), isConnected()); } void HTTPProtocol::mimetype(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_HEAD; m_request.cacheTag.policy = CC_Cache; if (proceedUntilResponseHeader()) { httpClose(m_request.isKeepAlive); finished(); } qCDebug(KIO_HTTP) << m_mimeType; } void HTTPProtocol::special(const QByteArray &data) { qCDebug(KIO_HTTP); int tmp; QDataStream stream(data); stream >> tmp; switch (tmp) { case 1: { // HTTP POST QUrl url; qint64 size; stream >> url >> size; post(url, size); break; } case 2: { // cache_update QUrl url; bool no_cache; qint64 expireDate; stream >> url >> no_cache >> expireDate; if (no_cache) { QString filename = cacheFilePathFromUrl(url); // there is a tiny risk of deleting the wrong file due to hash collisions here. // this is an unimportant performance issue. // FIXME on Windows we may be unable to delete the file if open QFile::remove(filename); finished(); break; } // let's be paranoid and inefficient here... HTTPRequest savedRequest = m_request; m_request.url = url; if (cacheFileOpenRead()) { m_request.cacheTag.expireDate.setTime_t(expireDate); cacheFileClose(); // this sends an update command to the cache cleaner process } m_request = savedRequest; finished(); break; } case 5: { // WebDAV lock QUrl url; QString scope, type, owner; stream >> url >> scope >> type >> owner; davLock(url, scope, type, owner); break; } case 6: { // WebDAV unlock QUrl url; stream >> url; davUnlock(url); break; } case 7: { // Generic WebDAV QUrl url; int method; qint64 size; stream >> url >> method >> size; davGeneric(url, (KIO::HTTP_METHOD) method, size); break; } case 99: { // Close Connection httpCloseConnection(); break; } default: // Some command we don't understand. // Just ignore it, it may come from some future version of KDE. break; } } /** * Read a chunk from the data stream. */ int HTTPProtocol::readChunked() { if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) { // discard CRLF from previous chunk, if any, and read size of next chunk int bufPos = 0; m_receiveBuf.resize(4096); bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); if (foundCrLf && bufPos == 2) { // The previous read gave us the CRLF from the previous chunk. As bufPos includes // the trailing CRLF it has to be > 2 to possibly include the next chunksize. bufPos = 0; foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); } if (!foundCrLf) { qCDebug(KIO_HTTP) << "Failed to read chunk header."; return -1; } Q_ASSERT(bufPos > 2); long long nextChunkSize = STRTOLL(m_receiveBuf.data(), nullptr, 16); if (nextChunkSize < 0) { qCDebug(KIO_HTTP) << "Negative chunk size"; return -1; } m_iBytesLeft = nextChunkSize; qCDebug(KIO_HTTP) << "Chunk size =" << m_iBytesLeft << "bytes"; if (m_iBytesLeft == 0) { // Last chunk; read and discard chunk trailer. // The last trailer line ends with CRLF and is followed by another CRLF // so we have CRLFCRLF like at the end of a standard HTTP header. // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes. //NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over. char trash[4096]; trash[0] = m_receiveBuf.constData()[bufPos - 2]; trash[1] = m_receiveBuf.constData()[bufPos - 1]; int trashBufPos = 2; bool done = false; while (!done && !m_isEOF) { if (trashBufPos > 3) { // shift everything but the last three bytes out of the buffer for (int i = 0; i < 3; i++) { trash[i] = trash[trashBufPos - 3 + i]; } trashBufPos = 3; } done = readDelimitedText(trash, &trashBufPos, 4096, 2); } if (m_isEOF && !done) { qCDebug(KIO_HTTP) << "Failed to read chunk trailer."; return -1; } return 0; } } int bytesReceived = readLimited(); if (!m_iBytesLeft) { m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk } return bytesReceived; } int HTTPProtocol::readLimited() { if (!m_iBytesLeft) { return 0; } m_receiveBuf.resize(4096); int bytesToReceive; if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size())) { bytesToReceive = m_receiveBuf.size(); } else { bytesToReceive = m_iBytesLeft; } const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false); if (bytesReceived <= 0) { return -1; // Error: connection lost } m_iBytesLeft -= bytesReceived; return bytesReceived; } int HTTPProtocol::readUnlimited() { if (m_request.isKeepAlive) { qCDebug(KIO_HTTP) << "Unbounded datastream on a Keep-alive connection!"; m_request.isKeepAlive = false; } m_receiveBuf.resize(4096); int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size()); if (result > 0) { return result; } m_isEOF = true; m_iBytesLeft = 0; return 0; } void HTTPProtocol::slotData(const QByteArray &_d) { if (!_d.size()) { m_isEOD = true; return; } if (m_iContentLeft != NO_SIZE) { if (m_iContentLeft >= KIO::filesize_t(_d.size())) { m_iContentLeft -= _d.size(); } else { m_iContentLeft = NO_SIZE; } } QByteArray d = _d; if (!m_dataInternal) { // If a broken server does not send the mime-type, // we try to id it from the content before dealing // with the content itself. if (m_mimeType.isEmpty() && !m_isRedirection && !(m_request.responseCode >= 300 && m_request.responseCode <= 399)) { qCDebug(KIO_HTTP) << "Determining mime-type from content..."; int old_size = m_mimeTypeBuffer.size(); m_mimeTypeBuffer.resize(old_size + d.size()); memcpy(m_mimeTypeBuffer.data() + old_size, d.data(), d.size()); if ((m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) && (m_mimeTypeBuffer.size() < 1024)) { m_cpMimeBuffer = true; return; // Do not send up the data since we do not yet know its mimetype! } qCDebug(KIO_HTTP) << "Mimetype buffer size:" << m_mimeTypeBuffer.size(); QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(m_request.url.adjusted(QUrl::StripTrailingSlash).path(), m_mimeTypeBuffer); if (mime.isValid() && !mime.isDefault()) { m_mimeType = mime.name(); qCDebug(KIO_HTTP) << "Mimetype from content:" << m_mimeType; } if (m_mimeType.isEmpty()) { m_mimeType = QStringLiteral(DEFAULT_MIME_TYPE); qCDebug(KIO_HTTP) << "Using default mimetype:" << m_mimeType; } //### we could also open the cache file here if (m_cpMimeBuffer) { d.resize(0); d.resize(m_mimeTypeBuffer.size()); memcpy(d.data(), m_mimeTypeBuffer.data(), d.size()); } mimeType(m_mimeType); m_mimeTypeBuffer.resize(0); } //qDebug() << "Sending data of size" << d.size(); data(d); if (m_request.cacheTag.ioMode == WriteToCache) { cacheFileWritePayload(d); } } else { uint old_size = m_webDavDataBuf.size(); m_webDavDataBuf.resize(old_size + d.size()); memcpy(m_webDavDataBuf.data() + old_size, d.data(), d.size()); } } /** * This function is our "receive" function. It is responsible for * downloading the message (not the header) from the HTTP server. It * is called either as a response to a client's KIOJob::dataEnd() * (meaning that the client is done sending data) or by 'sendQuery()' * (if we are in the process of a PUT/POST request). It can also be * called by a webDAV function, to receive stat/list/property/etc. * data; in this case the data is stored in m_webDavDataBuf. */ bool HTTPProtocol::readBody(bool dataInternal /* = false */) { // special case for reading cached body since we also do it in this function. oh well. if (!canHaveResponseBody(m_request.responseCode, m_request.method) && !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 && m_request.method != HTTP_HEAD)) { return true; } m_isEOD = false; // Note that when dataInternal is true, we are going to: // 1) save the body data to a member variable, m_webDavDataBuf // 2) _not_ advertise the data, speed, size, etc., through the // corresponding functions. // This is used for returning data to WebDAV. m_dataInternal = dataInternal; if (dataInternal) { m_webDavDataBuf.clear(); } // Check if we need to decode the data. // If we are in copy mode, then use only transfer decoding. bool useMD5 = !m_contentMD5.isEmpty(); // Deal with the size of the file. KIO::filesize_t sz = m_request.offset; if (sz) { m_iSize += sz; } if (!m_isRedirection) { // Update the application with total size except when // it is compressed, or when the data is to be handled // internally (webDAV). If compressed we have to wait // until we uncompress to find out the actual data size if (!dataInternal) { if ((m_iSize > 0) && (m_iSize != NO_SIZE)) { totalSize(m_iSize); infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize), m_request.url.host())); } else { totalSize(0); } } if (m_request.cacheTag.ioMode == ReadFromCache) { qCDebug(KIO_HTTP) << "reading data from cache..."; m_iContentLeft = NO_SIZE; QByteArray d; while (true) { d = cacheFileReadPayload(MAX_IPC_SIZE); if (d.isEmpty()) { break; } slotData(d); sz += d.size(); if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); if (!dataInternal) { data(QByteArray()); } return true; } } if (m_iSize != NO_SIZE) { m_iBytesLeft = m_iSize - sz; } else { m_iBytesLeft = NO_SIZE; } m_iContentLeft = m_iBytesLeft; if (m_isChunked) { m_iBytesLeft = NO_SIZE; } qCDebug(KIO_HTTP) << KIO::number(m_iBytesLeft) << "bytes left."; // Main incoming loop... Gather everything while we can... m_cpMimeBuffer = false; m_mimeTypeBuffer.resize(0); HTTPFilterChain chain; // redirection ignores the body if (!m_isRedirection) { - QObject::connect(&chain, SIGNAL(output(QByteArray)), - this, SLOT(slotData(QByteArray))); + QObject::connect(&chain, &HTTPFilterBase::output, + this, &HTTPProtocol::slotData); } - QObject::connect(&chain, SIGNAL(error(QString)), - this, SLOT(slotFilterError(QString))); + 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(QString(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; QByteArray baseName = fileName.mid(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: "; ret += m_wwwAuth->headerFragment(); } if (m_proxyAuth && !m_proxyAuth->isError()) { ret += "Proxy-Authorization: "; ret += m_proxyAuth->headerFragment(); } return toQString(ret); // ## encoding ok? } static QString protocolForProxyType(QNetworkProxy::ProxyType type) { switch (type) { case QNetworkProxy::DefaultProxy: break; case QNetworkProxy::Socks5Proxy: return QStringLiteral("socks"); case QNetworkProxy::NoProxy: break; case QNetworkProxy::HttpProxy: case QNetworkProxy::HttpCachingProxy: case QNetworkProxy::FtpCachingProxy: break; } return QStringLiteral("http"); } void HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator) { qCDebug(KIO_HTTP) << "realm:" << authenticator->realm() << "user:" << authenticator->user(); // Set the proxy URL... m_request.proxyUrl.setScheme(protocolForProxyType(proxy.type())); m_request.proxyUrl.setUserName(proxy.user()); m_request.proxyUrl.setHost(proxy.hostName()); m_request.proxyUrl.setPort(proxy.port()); AuthInfo info; info.url = m_request.proxyUrl; info.realmValue = authenticator->realm(); info.username = authenticator->user(); info.verifyPath = info.realmValue.isEmpty(); const bool haveCachedCredentials = checkCachedAuthentication(info); const bool retryAuth = (m_socketProxyAuth != nullptr); // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, // and it was not successful. see below and saveProxyAuthenticationForSocket(). if (!haveCachedCredentials || retryAuth) { // Save authentication info if the connection succeeds. We need to disconnect // this after saving the auth data (or an error) so we won't save garbage afterwards! connect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); //### fillPromptInfo(&info); info.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); info.keepPassword = true; info.commentLabel = i18n("Proxy:"); info.comment = i18n("%1 at %2", info.realmValue.toHtmlEscaped(), m_request.proxyUrl.host()); const QString errMsg((retryAuth ? i18n("Proxy Authentication Failed.") : QString())); const int errorCode = openPasswordDialogV2(info, errMsg); if (errorCode) { qCDebug(KIO_HTTP) << "proxy auth cancelled by user, or communication error"; error(errorCode, QString()); delete m_proxyAuth; m_proxyAuth = nullptr; return; } } authenticator->setUser(info.username); authenticator->setPassword(info.password); authenticator->setOption(QStringLiteral("keepalive"), info.keepPassword); if (m_socketProxyAuth) { *m_socketProxyAuth = *authenticator; } else { m_socketProxyAuth = new QAuthenticator(*authenticator); } if (!m_request.proxyUrl.userName().isEmpty()) { m_request.proxyUrl.setUserName(info.username); } } void HTTPProtocol::saveProxyAuthenticationForSocket() { qCDebug(KIO_HTTP) << "Saving authenticator"; disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); Q_ASSERT(m_socketProxyAuth); if (m_socketProxyAuth) { qCDebug(KIO_HTTP) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); KIO::AuthInfo a; a.verifyPath = true; a.url = m_request.proxyUrl; a.realmValue = m_socketProxyAuth->realm(); a.username = m_socketProxyAuth->user(); a.password = m_socketProxyAuth->password(); a.keepPassword = m_socketProxyAuth->option(QStringLiteral("keepalive")).toBool(); cacheAuthentication(a); } delete m_socketProxyAuth; m_socketProxyAuth = nullptr; } void HTTPProtocol::saveAuthenticationData() { KIO::AuthInfo authinfo; bool alreadyCached = false; KAbstractHttpAuthentication *auth = nullptr; switch (m_request.prevResponseCode) { case 401: auth = m_wwwAuth; alreadyCached = config()->readEntry("cached-www-auth", false); break; case 407: auth = m_proxyAuth; alreadyCached = config()->readEntry("cached-proxy-auth", false); break; default: Q_ASSERT(false); // should never happen! } // Prevent recaching of the same credentials over and over again. if (auth && (!auth->realm().isEmpty() || !alreadyCached)) { auth->fillKioAuthInfo(&authinfo); if (auth == m_wwwAuth) { setMetaData(QStringLiteral("{internal~currenthost}cached-www-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo); } } else { setMetaData(QStringLiteral("{internal~allhosts}cached-proxy-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo); } } qCDebug(KIO_HTTP) << "Cache authentication info ?" << authinfo.keepPassword; if (authinfo.keepPassword) { cacheAuthentication(authinfo); qCDebug(KIO_HTTP) << "Cached authentication for" << m_request.url; } } // Update our server connection state which includes www and proxy username and password. m_server.updateCredentials(m_request); } bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer *tokenizer) { KIO::AuthInfo authinfo; QList authTokens; KAbstractHttpAuthentication **auth; QList *blacklistedAuthTokens; TriedCredentials *triedCredentials; if (m_request.responseCode == 401) { auth = &m_wwwAuth; blacklistedAuthTokens = &m_blacklistedWwwAuthMethods; triedCredentials = &m_triedWwwCredentials; authTokens = tokenizer->iterator("www-authenticate").all(); authinfo.url = m_request.url; authinfo.username = m_server.url.userName(); authinfo.prompt = i18n("You need to supply a username and a " "password to access this site."); authinfo.commentLabel = i18n("Site:"); } else { // make sure that the 407 header hasn't escaped a lower layer when it shouldn't. // this may break proxy chains which were never tested anyway, and AFAIK they are // rare to nonexistent in the wild. Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy); auth = &m_proxyAuth; blacklistedAuthTokens = &m_blacklistedProxyAuthMethods; triedCredentials = &m_triedProxyCredentials; authTokens = tokenizer->iterator("proxy-authenticate").all(); authinfo.url = m_request.proxyUrl; authinfo.username = m_request.proxyUrl.userName(); authinfo.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); authinfo.commentLabel = i18n("Proxy:"); } bool authRequiresAnotherRoundtrip = false; // Workaround brain dead server responses that violate the spec and // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate // header fields. See bug 215736... if (!authTokens.isEmpty()) { QString errorMsg; authRequiresAnotherRoundtrip = true; if (m_request.responseCode == m_request.prevResponseCode && *auth) { // Authentication attempt failed. Retry... if ((*auth)->wasFinalStage()) { errorMsg = (m_request.responseCode == 401 ? i18n("Authentication Failed.") : i18n("Proxy Authentication Failed.")); // The authentication failed in its final stage. If the chosen method didn't use a password or // if it failed with both the supplied and prompted password then blacklist this method and try // again with another one if possible. if (!(*auth)->needCredentials() || *triedCredentials > JobCredentials) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; blacklistedAuthTokens->append(scheme); } delete *auth; *auth = nullptr; } else { // Create authentication header // WORKAROUND: The following piece of code prevents brain dead IIS // servers that send back multiple "WWW-Authenticate" headers from // screwing up our authentication logic during the challenge // phase (Type 2) of NTLM authentication. QMutableListIterator it(authTokens); const QByteArray authScheme((*auth)->scheme().trimmed()); while (it.hasNext()) { if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) { it.remove(); } } } } QList::iterator it = authTokens.begin(); while (it != authTokens.end()) { QByteArray scheme = *it; // Separate the method name from any additional parameters (for ex. nonce or realm). int index = it->indexOf(' '); if (index > 0) { scheme.truncate(index); } if (blacklistedAuthTokens->contains(scheme)) { it = authTokens.erase(it); } else { it++; } } try_next_auth_scheme: QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens); if (*auth) { const QByteArray authScheme((*auth)->scheme().trimmed()); if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) { // huh, the strongest authentication scheme offered has changed. delete *auth; *auth = nullptr; } } if (!(*auth)) { *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config()); } if (*auth) { qCDebug(KIO_HTTP) << "Trying authentication scheme:" << (*auth)->scheme(); // remove trailing space from the method string, or digest auth will fail (*auth)->setChallenge(bestOffer, authinfo.url, m_request.sentMethodString); QString username, password; bool generateAuthHeader = true; if ((*auth)->needCredentials()) { // use credentials supplied by the application if available if (!m_request.url.userName().isEmpty() && !m_request.url.password().isEmpty() && *triedCredentials == NoCredentials) { username = m_request.url.userName(); password = m_request.url.password(); // don't try this password any more *triedCredentials = JobCredentials; } else { // try to get credentials from kpasswdserver's cache, then try asking the user. authinfo.verifyPath = false; // we have realm, no path based checking please! authinfo.realmValue = (*auth)->realm(); if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching()) { authinfo.realmValue = QLatin1String((*auth)->scheme()); } // Save the current authinfo url because it can be modified by the call to // checkCachedAuthentication. That way we can restore it if the call // modified it. const QUrl reqUrl = authinfo.url; if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) { // Reset url to the saved url... authinfo.url = reqUrl; authinfo.keepPassword = true; authinfo.comment = i18n("%1 at %2", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host()); const int errorCode = openPasswordDialogV2(authinfo, errorMsg); if (errorCode) { generateAuthHeader = false; authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_ACCESS_DENIED, reqUrl.host()); } qCDebug(KIO_HTTP) << "looks like the user canceled the authentication dialog"; delete *auth; *auth = nullptr; } *triedCredentials = UserInputCredentials; } else { *triedCredentials = CachedCredentials; } username = authinfo.username; password = authinfo.password; } } if (generateAuthHeader) { (*auth)->generateResponse(username, password); (*auth)->setCachePasswordEnabled(authinfo.keepPassword); qCDebug(KIO_HTTP) << "isError=" << (*auth)->isError() << "needCredentials=" << (*auth)->needCredentials() << "forceKeepAlive=" << (*auth)->forceKeepAlive() << "forceDisconnect=" << (*auth)->forceDisconnect(); if ((*auth)->isError()) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; authTokens.removeOne(scheme); blacklistedAuthTokens->append(scheme); if (!authTokens.isEmpty()) { goto try_next_auth_scheme; } else { if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed.")); } authRequiresAnotherRoundtrip = false; } //### return false; ? } else if ((*auth)->forceKeepAlive()) { //### think this through for proxied / not proxied m_request.isKeepAlive = true; } else if ((*auth)->forceDisconnect()) { //### think this through for proxied / not proxied m_request.isKeepAlive = false; httpCloseConnection(); } } } else { authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method.")); } } } return authRequiresAnotherRoundtrip; } void HTTPProtocol::copyPut(const QUrl& src, const QUrl& dest, JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest)) { return; } resetSessionSettings(); if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { return; } } m_POSTbuf = new QFile (src.toLocalFile()); if (!m_POSTbuf->open(QFile::ReadOnly)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, QString()); return; } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } bool HTTPProtocol::davStatDestination() { const QByteArray request ("" "" "" "" "" "" ""); davSetRequest(request); // WebDAV Stat or List... m_request.method = DAV_PROPFIND; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = 0; proceedUntilResponseContent(true); if (!m_request.isKeepAlive) { httpCloseConnection(); // close connection if server requested it. m_request.isKeepAlive = true; // reset the keep alive flag. } if (m_request.responseCode == 207) { error(ERR_FILE_ALREADY_EXIST, QString()); return false; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; return true; } void HTTPProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url); } void HTTPProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; default: SlaveBase::virtual_hook(id, data); } } // needed for JSON file embedding #include "http.moc" diff --git a/src/ioslaves/http/httpfilter.cpp b/src/ioslaves/http/httpfilter.cpp index e5156274..642338bb 100644 --- a/src/ioslaves/http/httpfilter.cpp +++ b/src/ioslaves/http/httpfilter.cpp @@ -1,208 +1,208 @@ /* This file is part of the KDE libraries Copyright (c) 2002 Waldo Bastian Copyright 2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "httpfilter.h" #include #include #include #include #include Q_LOGGING_CATEGORY(KIO_HTTP_FILTER, "kf5.kio.kio_http.filter") /* Testcases: - http://david.fullrecall.com/browser-http-compression-test?compression=deflate-http (bug 160289) - http://demo.serv-u.com/?user=demo-WC&password=demo-WC (Content-Encoding: deflate) (bug 188935) - http://web.davidfaure.fr/cgi-bin/deflate_test (Content-Encoding: deflate) (bug 114830) - http://www.zlib.net/zlib_faq.html#faq39 (Content-Encoding: gzip) - wikipedia (Content-Encoding: gzip) - cnn.com (Content-Encoding: gzip) - http://arstechnica.com/ (Content-Encoding: gzip) - mailman admin interface on mail.kde.org (see r266769, but can't confirm these days) */ HTTPFilterBase::HTTPFilterBase() : last(nullptr) { } HTTPFilterBase::~HTTPFilterBase() { delete last; } void HTTPFilterBase::chain(HTTPFilterBase *previous) { last = previous; - connect(last, SIGNAL(output(QByteArray)), - this, SLOT(slotInput(QByteArray))); + connect(last, &HTTPFilterBase::output, + this, &HTTPFilterBase::slotInput); } HTTPFilterChain::HTTPFilterChain() : first(nullptr) { } void HTTPFilterChain::addFilter(HTTPFilterBase *filter) { if (!last) { first = filter; } else { disconnect(last, SIGNAL(output(QByteArray)), nullptr, nullptr); filter->chain(last); } last = filter; - connect(filter, SIGNAL(output(QByteArray)), - this, SIGNAL(output(QByteArray))); - connect(filter, SIGNAL(error(QString)), - this, SIGNAL(error(QString))); + connect(filter, &HTTPFilterBase::output, + this, &HTTPFilterBase::output); + connect(filter, &HTTPFilterBase::error, + this, &HTTPFilterBase::error); } void HTTPFilterChain::slotInput(const QByteArray &d) { if (first) { first->slotInput(d); } else { emit output(d); } } HTTPFilterMD5::HTTPFilterMD5() : context(QCryptographicHash::Md5) { } QString HTTPFilterMD5::md5() { return QString::fromLatin1(context.result().toBase64().constData()); } void HTTPFilterMD5::slotInput(const QByteArray &d) { context.addData(d); emit output(d); } HTTPFilterGZip::HTTPFilterGZip(bool deflate) : m_deflateMode(deflate), m_firstData(true), m_finished(false) { // We can't use KFilterDev because it assumes it can read as much data as necessary // from the underlying device. It's a pull strategy, while we have to do // a push strategy. m_gzipFilter = KCompressionDevice::filterForCompressionType(KCompressionDevice::GZip); } HTTPFilterGZip::~HTTPFilterGZip() { m_gzipFilter->terminate(); delete m_gzipFilter; } /* The data format used by the zlib library is described by RFCs (Request for Comments) 1950 to 1952 in the files ftp://ds.internic.net/rfc/rfc1950.txt (zlib format), rfc1951.txt (deflate format) and rfc1952.txt (gzip format). Use /usr/include/zlib.h as the primary source of documentation though. */ void HTTPFilterGZip::slotInput(const QByteArray &d) { if (d.isEmpty()) { return; } //qDebug() << "Got" << d.size() << "bytes as input"; if (m_firstData) { if (m_deflateMode) { bool zlibHeader = true; // Autodetect broken webservers (thanks Microsoft) who send raw-deflate // instead of zlib-headers-deflate when saying Content-Encoding: deflate. const unsigned char firstChar = d[0]; if ((firstChar & 0x0f) != 8) { // In a zlib header, CM should be 8 (cf RFC 1950) zlibHeader = false; } else if (d.size() > 1) { const unsigned char flg = d[1]; if ((firstChar * 256 + flg) % 31 != 0) { // Not a multiple of 31? invalid zlib header then zlibHeader = false; } } //if (!zlibHeader) // qDebug() << "Bad webserver, uses raw-deflate instead of zlib-deflate..."; if (zlibHeader) { m_gzipFilter->setFilterFlags(KFilterBase::ZlibHeaders); } else { m_gzipFilter->setFilterFlags(KFilterBase::NoHeaders); } m_gzipFilter->init(QIODevice::ReadOnly); } else { m_gzipFilter->setFilterFlags(KFilterBase::WithHeaders); m_gzipFilter->init(QIODevice::ReadOnly); } m_firstData = false; } m_gzipFilter->setInBuffer(d.constData(), d.size()); while (!m_gzipFilter->inBufferEmpty() && !m_finished) { char buf[8192]; m_gzipFilter->setOutBuffer(buf, sizeof(buf)); KFilterBase::Result result = m_gzipFilter->uncompress(); //qDebug() << "uncompress returned" << result; switch (result) { case KFilterBase::Ok: case KFilterBase::End: { const int bytesOut = sizeof(buf) - m_gzipFilter->outBufferAvailable(); if (bytesOut) { emit output(QByteArray(buf, bytesOut)); } if (result == KFilterBase::End) { //qDebug() << "done, bHasFinished=true"; emit output(QByteArray()); m_finished = true; } break; } case KFilterBase::Error: qCDebug(KIO_HTTP_FILTER) << "Error from KGZipFilter"; emit error(i18n("Receiving corrupt data.")); m_finished = true; // exit this while loop break; } } } HTTPFilterDeflate::HTTPFilterDeflate() : HTTPFilterGZip(true) { } #include "moc_httpfilter.cpp" diff --git a/src/ioslaves/http/kcookiejar/kcookieserver.cpp b/src/ioslaves/http/kcookiejar/kcookieserver.cpp index f12737fe..3de5b276 100644 --- a/src/ioslaves/http/kcookiejar/kcookieserver.cpp +++ b/src/ioslaves/http/kcookiejar/kcookieserver.cpp @@ -1,602 +1,602 @@ /* This file is part of KDE Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //---------------------------------------------------------------------------- // // KDE Cookie Server #include "kcookieserver.h" #define SAVE_DELAY 3 // Save after 3 minutes #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_COOKIEJAR) #include "kcookiejar.h" #include "kcookiewin.h" #include "kcookieserveradaptor.h" K_PLUGIN_FACTORY_WITH_JSON(KdedCookieServerFactory, "kcookiejar.json", registerPlugin();) static QDir getOrCreateCookieJarDir() { const QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)); const QString kcookiejarDirName = dataDir.absoluteFilePath(QStringLiteral("kcookiejar")); if (dataDir.exists(QStringLiteral("kcookiejar"))) { const QFileInfo cookiejarDirInfo(kcookiejarDirName); if (!cookiejarDirInfo.isDir()) { QFile kcookieBogusFile(kcookiejarDirName); if (!kcookieBogusFile.remove()) { QMessageBox::warning(nullptr, i18n("Cannot Save Cookies"), i18n("Could not remove %1, check permissions", cookiejarDirInfo.absoluteFilePath())); } } else { return QDir(kcookiejarDirName); } } if (!dataDir.mkpath(QStringLiteral("kcookiejar"))) { QMessageBox::warning(nullptr, i18n("Cannot Save Cookies"), i18n("Could not create directory %1", kcookiejarDirName)); } return QDir(kcookiejarDirName); } // Cookie field indexes enum CookieDetails { CF_DOMAIN = 0, CF_PATH, CF_NAME, CF_HOST, CF_VALUE, CF_EXPIRE, CF_PROVER, CF_SECURE }; class CookieRequest { public: QDBusMessage reply; QString url; bool DOM; qlonglong windowId; }; template class QList; class RequestList : public QList { public: RequestList() : QList() { } }; KCookieServer::KCookieServer(QObject *parent, const QList &) : KDEDModule(parent) { (void)new KCookieServerAdaptor(this); mCookieJar = new KCookieJar; mPendingCookies = new KHttpCookieList; mRequestList = new RequestList; mAdvicePending = false; mTimer = new QTimer(); mTimer->setSingleShot(true); - connect(mTimer, SIGNAL(timeout()), SLOT(slotSave())); + connect(mTimer, &QTimer::timeout, this, &KCookieServer::slotSave); mConfig = new KConfig(QStringLiteral("kcookiejarrc")); mCookieJar->loadConfig(mConfig); mFilename = getOrCreateCookieJarDir().absoluteFilePath(QStringLiteral("cookies")); mCookieJar->loadCookies(mFilename); - connect(this, SIGNAL(windowUnregistered(qlonglong)), - this, SLOT(slotDeleteSessionCookies(qlonglong))); + connect(this, &KDEDModule::windowUnregistered, + this, &KCookieServer::slotDeleteSessionCookies); } KCookieServer::~KCookieServer() { slotSave(); delete mCookieJar; delete mTimer; delete mPendingCookies; delete mConfig; } bool KCookieServer::cookiesPending(const QString &url, KHttpCookieList *cookieList) { QString fqdn; QString path; // Check whether 'url' has cookies on the pending list if (mPendingCookies->isEmpty()) { return false; } if (!KCookieJar::parseUrl(url, fqdn, path)) { return false; } QStringList domains; mCookieJar->extractDomains(fqdn, domains); Q_FOREACH (const KHttpCookie &cookie, *mPendingCookies) { if (cookie.match(fqdn, domains, path)) { if (!cookieList) { return true; } cookieList->append(cookie); } } if (!cookieList) { return false; } return cookieList->isEmpty(); } void KCookieServer::addCookies(const QString &url, const QByteArray &cookieHeader, qlonglong windowId, bool useDOMFormat) { KHttpCookieList cookieList; if (useDOMFormat) { cookieList = mCookieJar->makeDOMCookies(url, cookieHeader, windowId); } else { cookieList = mCookieJar->makeCookies(url, cookieHeader, windowId); } checkCookies(&cookieList, windowId); *mPendingCookies += cookieList; if (!mAdvicePending) { mAdvicePending = true; while (!mPendingCookies->isEmpty()) { checkCookies(nullptr, windowId); } mAdvicePending = false; } } void KCookieServer::checkCookies(KHttpCookieList *cookieList) { checkCookies(cookieList, 0); } void KCookieServer::checkCookies(KHttpCookieList *cookieList, qlonglong windowId) { KHttpCookieList *list; if (cookieList) { list = cookieList; } else { list = mPendingCookies; } QMutableListIterator cookieIterator(*list); while (cookieIterator.hasNext()) { KHttpCookie &cookie = cookieIterator.next(); const KCookieAdvice advice = mCookieJar->cookieAdvice(cookie); switch (advice) { case KCookieAccept: case KCookieAcceptForSession: mCookieJar->addCookie(cookie); cookieIterator.remove(); break; case KCookieReject: cookieIterator.remove(); break; case KCookieDunno: case KCookieAsk: break; } } if (cookieList || list->isEmpty()) { return; } // Collect all pending cookies with the same host as the first pending cookie const KHttpCookie ¤tCookie = mPendingCookies->first(); KHttpCookieList currentList; currentList.append(currentCookie); const QString currentHost = currentCookie.host(); QList shownCookies; shownCookies << 0; for (int i = 1 /*first already done*/; i < mPendingCookies->count(); ++i) { const KHttpCookie &cookie = (*mPendingCookies)[i]; if (cookie.host() == currentHost) { currentList.append(cookie); shownCookies << i; } } //qDebug() << shownCookies; KCookieWin *kw = new KCookieWin(nullptr, currentList, mCookieJar->preferredDefaultPolicy(), mCookieJar->showCookieDetails()); if (windowId > 0) { KWindowSystem::setMainWindow(kw, windowId); } KCookieAdvice userAdvice = kw->advice(mCookieJar, currentCookie); delete kw; // Save the cookie config if it has changed mCookieJar->saveConfig(mConfig); // Apply the user's choice to all cookies that are currently // queued for this host (or just the first one, if the user asks for that). QMutableListIterator cookieIterator2(*mPendingCookies); int pendingCookieIndex = -1; while (cookieIterator2.hasNext()) { ++pendingCookieIndex; KHttpCookie &cookie = cookieIterator2.next(); if (cookie.host() != currentHost) { continue; } if (mCookieJar->preferredDefaultPolicy() == KCookieJar::ApplyToShownCookiesOnly && !shownCookies.contains(pendingCookieIndex)) { // User chose "only those cookies", and this one was added while the dialog was up -> skip break; } switch (userAdvice) { case KCookieAccept: case KCookieAcceptForSession: // Store the user's choice for the cookie. // This is only used to check later if the cookie should expire // at the end of the session. The choice is not saved on disk. cookie.setUserSelectedAdvice(userAdvice); mCookieJar->addCookie(cookie); cookieIterator2.remove(); break; case KCookieReject: cookieIterator2.remove(); break; case KCookieDunno: case KCookieAsk: qCWarning(KIO_COOKIEJAR) << "userAdvice not accept or reject, this should never happen!"; break; } } // Check if we can handle any request QMutableListIterator requestIterator(*mRequestList); while (requestIterator.hasNext()) { CookieRequest *request = requestIterator.next(); if (!cookiesPending(request->url)) { const QString res = mCookieJar->findCookies(request->url, request->DOM, request->windowId); QDBusConnection::sessionBus().send(request->reply.createReply(res)); delete request; requestIterator.remove(); } } saveCookieJar(); } void KCookieServer::slotSave() { if (mCookieJar->changed()) { mCookieJar->saveCookies(mFilename); } } void KCookieServer::saveCookieJar() { if (mTimer->isActive()) { return; } mTimer->start(1000 * 60 * SAVE_DELAY); } void KCookieServer::putCookie(QStringList &out, const KHttpCookie &cookie, const QList &fields) { foreach (int i, fields) { switch (i) { case CF_DOMAIN : out << cookie.domain(); break; case CF_NAME : out << cookie.name(); break; case CF_PATH : out << cookie.path(); break; case CF_HOST : out << cookie.host(); break; case CF_VALUE : out << cookie.value(); break; case CF_EXPIRE : out << QString::number(cookie.expireDate()); break; case CF_PROVER : out << QString::number(cookie.protocolVersion()); break; case CF_SECURE : out << QString::number(cookie.isSecure() ? 1 : 0); break; default : out << QString(); } } } bool KCookieServer::cookieMatches(const KHttpCookie &c, const QString &domain, const QString &fqdn, const QString &path, const QString &name) { const bool hasDomain = !domain.isEmpty(); return (((hasDomain && c.domain() == domain) || fqdn == c.host()) && (c.path() == path) && (c.name() == name) && (!c.isExpired())); } // DBUS function QString KCookieServer::listCookies(const QString &url) { return findCookies(url, 0); } // DBUS function QString KCookieServer::findCookies(const QString &url, qlonglong windowId) { if (cookiesPending(url)) { CookieRequest *request = new CookieRequest; message().setDelayedReply(true); request->reply = message(); request->url = url; request->DOM = false; request->windowId = windowId; mRequestList->append(request); return QString(); // Talk to you later :-) } QString cookies = mCookieJar->findCookies(url, false, windowId); saveCookieJar(); return cookies; } // DBUS function QStringList KCookieServer::findDomains() { QStringList result; Q_FOREACH (const QString &domain, mCookieJar->getDomainList()) { // Ignore domains that have policy set for but contain // no cookies whatsoever... const KHttpCookieList *list = mCookieJar->getCookieList(domain, QString()); if (list && !list->isEmpty()) { result << domain; } } return result; } // DBUS function QStringList KCookieServer::findCookies(const QList &fields, const QString &_domain, const QString &fqdn, const QString &path, const QString &name) { QStringList result; const bool allCookies = name.isEmpty(); const QStringList domainList = _domain.split(QLatin1Char(' ')); if (allCookies) { Q_FOREACH (const QString &domain, domainList) { const KHttpCookieList *list = mCookieJar->getCookieList(domain, fqdn); if (!list) { continue; } Q_FOREACH (const KHttpCookie &cookie, *list) { if (cookie.isExpired()) { continue; } putCookie(result, cookie, fields); } } } else { Q_FOREACH (const QString &domain, domainList) { const KHttpCookieList *list = mCookieJar->getCookieList(domain, fqdn); if (!list) { continue; } Q_FOREACH (const KHttpCookie &cookie, *list) { if (cookie.isExpired()) { continue; } if (cookieMatches(cookie, domain, fqdn, path, name)) { putCookie(result, cookie, fields); break; } } } } return result; } // DBUS function QString KCookieServer::findDOMCookies(const QString &url) { return findDOMCookies(url, 0); } // DBUS function QString KCookieServer::findDOMCookies(const QString &url, qlonglong windowId) { // We don't wait for pending cookies because it locks up konqueror // which can cause a deadlock if it happens to have a popup-menu up. // Instead we just return pending cookies as if they had been accepted already. KHttpCookieList pendingCookies; cookiesPending(url, &pendingCookies); return mCookieJar->findCookies(url, true, windowId, &pendingCookies); } // DBUS function void KCookieServer::addCookies(const QString &arg1, const QByteArray &arg2, qlonglong arg3) { addCookies(arg1, arg2, arg3, false); } // DBUS function void KCookieServer::deleteCookie(const QString &domain, const QString &fqdn, const QString &path, const QString &name) { KHttpCookieList *cookieList = mCookieJar->getCookieList(domain, fqdn); if (cookieList && !cookieList->isEmpty()) { KHttpCookieList::Iterator itEnd = cookieList->end(); for (KHttpCookieList::Iterator it = cookieList->begin(); it != itEnd; ++it) { if (cookieMatches(*it, domain, fqdn, path, name)) { mCookieJar->eatCookie(it); saveCookieJar(); break; } } } } // DBUS function void KCookieServer::deleteCookiesFromDomain(const QString &domain) { mCookieJar->eatCookiesForDomain(domain); saveCookieJar(); } // Qt function void KCookieServer::slotDeleteSessionCookies(qlonglong windowId) { deleteSessionCookies(windowId); } // DBUS function void KCookieServer::deleteSessionCookies(qlonglong windowId) { mCookieJar->eatSessionCookies(windowId); saveCookieJar(); } void KCookieServer::deleteSessionCookiesFor(const QString &fqdn, qlonglong windowId) { mCookieJar->eatSessionCookies(fqdn, windowId); saveCookieJar(); } // DBUS function void KCookieServer::deleteAllCookies() { mCookieJar->eatAllCookies(); saveCookieJar(); } // DBUS function void KCookieServer::addDOMCookies(const QString &url, const QByteArray &cookieHeader, qlonglong windowId) { addCookies(url, cookieHeader, windowId, true); } // DBUS function bool KCookieServer::setDomainAdvice(const QString &url, const QString &advice) { QString fqdn; QString dummy; if (KCookieJar::parseUrl(url, fqdn, dummy)) { QStringList domains; mCookieJar->extractDomains(fqdn, domains); mCookieJar->setDomainAdvice(domains[domains.count() > 3 ? 3 : 0], KCookieJar::strToAdvice(advice)); // Save the cookie config if it has changed mCookieJar->saveConfig(mConfig); return true; } return false; } // DBUS function QString KCookieServer::getDomainAdvice(const QString &url) { KCookieAdvice advice = KCookieDunno; QString fqdn; QString dummy; if (KCookieJar::parseUrl(url, fqdn, dummy)) { QStringList domains; mCookieJar->extractDomains(fqdn, domains); QStringListIterator it(domains); while ((advice == KCookieDunno) && it.hasNext()) { // Always check advice in both ".domain" and "domain". Note // that we only want to check "domain" if it matches the // fqdn of the requested URL. const QString &domain = it.next(); if (domain.at(0) == '.' || domain == fqdn) { advice = mCookieJar->getDomainAdvice(domain); } } if (advice == KCookieDunno) { advice = mCookieJar->getGlobalAdvice(); } } return KCookieJar::adviceToStr(advice); } // DBUS function void KCookieServer::reloadPolicy() { mCookieJar->loadConfig(mConfig, true); } // DBUS function void KCookieServer::shutdown() { deleteLater(); } #include "kcookieserver.moc" diff --git a/src/ioslaves/http/kcookiejar/kcookiewin.cpp b/src/ioslaves/http/kcookiejar/kcookiewin.cpp index 163955b9..07b50a43 100644 --- a/src/ioslaves/http/kcookiejar/kcookiewin.cpp +++ b/src/ioslaves/http/kcookiejar/kcookiewin.cpp @@ -1,387 +1,387 @@ /* This file is part of KDE Copyright (C) 2000- Waldo Bastian Copyright (C) 2000- Dawit Alemayehu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //---------------------------------------------------------------------------- // // KDE File Manager -- HTTP Cookie Dialogs // The purpose of the QT_NO_TOOLTIP and QT_NO_WHATSTHIS ifdefs is because // this file is also used in Konqueror/Embedded. One of the aims of // Konqueror/Embedded is to be a small as possible to fit on embedded // devices. For this it's also useful to strip out unneeded features of // Qt, like for example QToolTip or QWhatsThis. The availability (or the // lack thereof) can be determined using these preprocessor defines. // The same applies to the QT_NO_ACCEL ifdef below. I hope it doesn't make // too much trouble... (Simon) #include "kcookiewin.h" #include "kcookiejar.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum { AcceptedForSession = QDialog::Accepted + 1 }; KCookieWin::KCookieWin(QWidget *parent, KHttpCookieList cookieList, int defaultButton, bool showDetails) : QDialog(parent) { setModal(true); setObjectName(QStringLiteral("cookiealert")); setWindowTitle(i18n("Cookie Alert")); setWindowIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-cookies"))); // all cookies in the list should have the same window at this time, so let's take the first if (cookieList.first().windowIds().count() > 0) { KWindowSystem::setMainWindow(this, cookieList.first().windowIds().first()); } else { // No window associated... make sure the user notices our dialog. KWindowSystem::setState(winId(), NET::KeepAbove); KUserTimestamp::updateUserTimestamp(); } const int count = cookieList.count(); const KHttpCookie &cookie = cookieList.first(); QString host(cookie.host()); int pos = host.indexOf(':'); if (pos > 0) { QString portNum = host.left(pos); host.remove(0, pos + 1); host += ':'; host += portNum; } QString txt = QStringLiteral(""); txt += i18ncp("%2 hostname, %3 optional cross domain suffix (translated below)", "

You received a cookie from
" "%2%3
" "Do you want to accept or reject this cookie?

", "

You received %1 cookies from
" "%2%3
" "Do you want to accept or reject these cookies?

", count, QUrl::fromAce(host.toLatin1()), cookie.isCrossDomain() ? i18nc("@item:intext cross domain cookie", " [Cross Domain]") : QString()); txt += QLatin1String(""); QVBoxLayout *topLayout = new QVBoxLayout; // This may look wrong, but it makes the dialogue automatically // shrink when the details are shown and then hidden again. topLayout->setSizeConstraint(QLayout::SetFixedSize); setLayout(topLayout); QFrame *vBox1 = new QFrame(this); topLayout->addWidget(vBox1); m_detailsButton = new QPushButton; m_detailsButton->setText(i18n("Details") + " >>"); m_detailsButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); #ifndef QT_NO_TOOLTIP m_detailsButton->setToolTip(i18n("See or modify the cookie information")); #endif - connect(m_detailsButton, SIGNAL(clicked()), this, SLOT(slotToggleDetails())); + connect(m_detailsButton, &QAbstractButton::clicked, this, &KCookieWin::slotToggleDetails); QPushButton *sessionOnlyButton = new QPushButton; sessionOnlyButton->setText(i18n("Accept for this &session")); sessionOnlyButton->setIcon(QIcon::fromTheme(QStringLiteral("chronometer"))); #ifndef QT_NO_TOOLTIP sessionOnlyButton->setToolTip(i18n("Accept cookie(s) until the end of the current session")); #endif - connect(sessionOnlyButton, SIGNAL(clicked()), this, SLOT(slotSessionOnlyClicked())); + connect(sessionOnlyButton, &QAbstractButton::clicked, this, &KCookieWin::slotSessionOnlyClicked); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->addButton(m_detailsButton, QDialogButtonBox::ActionRole); buttonBox->addButton(sessionOnlyButton, QDialogButtonBox::ActionRole); buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); QPushButton *but = buttonBox->button(QDialogButtonBox::Yes); but->setText(i18n("&Accept")); - connect(but, SIGNAL(clicked(bool)), SLOT(accept())); + connect(but, &QAbstractButton::clicked, this, &QDialog::accept); but = buttonBox->button(QDialogButtonBox::No); but->setText(i18n("&Reject")); - connect(but, SIGNAL(clicked(bool)), SLOT(reject())); + connect(but, &QAbstractButton::clicked, this, &QDialog::reject); topLayout->addWidget(buttonBox); QVBoxLayout *vBox1Layout = new QVBoxLayout(vBox1); vBox1Layout->setSpacing(-1); vBox1Layout->setMargin(0); // Cookie image and message to user QFrame *hBox = new QFrame(vBox1); vBox1Layout->addWidget(hBox); QHBoxLayout *hBoxLayout = new QHBoxLayout(hBox); hBoxLayout->setSpacing(0); hBoxLayout->setMargin(0); QLabel *icon = new QLabel(hBox); hBoxLayout->addWidget(icon); icon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(style()->pixelMetric(QStyle::PM_LargeIconSize))); icon->setAlignment(Qt::AlignCenter); icon->setFixedSize(2 * icon->sizeHint()); QFrame *vBox = new QFrame(hBox); QVBoxLayout *vBoxLayout = new QVBoxLayout(vBox); vBoxLayout->setSpacing(0); vBoxLayout->setMargin(0); hBoxLayout->addWidget(vBox); QLabel *lbl = new QLabel(txt, vBox); vBoxLayout->addWidget(lbl); lbl->setAlignment(Qt::AlignCenter); // Cookie Details dialog... m_detailView = new KCookieDetail(cookieList, count, vBox1); vBox1Layout->addWidget(m_detailView); m_detailView->hide(); // Cookie policy choice... QGroupBox *m_btnGrp = new QGroupBox(i18n("Apply Choice To"), vBox1); vBox1Layout->addWidget(m_btnGrp); QVBoxLayout *vbox = new QVBoxLayout; txt = (count == 1) ? i18n("&Only this cookie") : i18n("&Only these cookies"); m_onlyCookies = new QRadioButton(txt, m_btnGrp); vbox->addWidget(m_onlyCookies); #ifndef QT_NO_WHATSTHIS m_onlyCookies->setWhatsThis(i18n("Select this option to only accept or reject this cookie. " "You will be prompted again if you receive another cookie.")); #endif m_allCookiesDomain = new QRadioButton(i18n("All cookies from this do&main"), m_btnGrp); vbox->addWidget(m_allCookiesDomain); #ifndef QT_NO_WHATSTHIS m_allCookiesDomain->setWhatsThis(i18n("Select this option to accept or reject all cookies from " "this site. Choosing this option will add a new policy for " "the site this cookie originated from. This policy will be " "permanent until you manually change it from the System Settings.")); #endif m_allCookies = new QRadioButton(i18n("All &cookies"), m_btnGrp); vbox->addWidget(m_allCookies); #ifndef QT_NO_WHATSTHIS m_allCookies->setWhatsThis(i18n("Select this option to accept/reject all cookies from " "anywhere. Choosing this option will change the global " "cookie policy for all cookies until you manually change " "it from the System Settings.")); #endif m_btnGrp->setLayout(vbox); switch (defaultButton) { case KCookieJar::ApplyToShownCookiesOnly: m_onlyCookies->setChecked(true); break; case KCookieJar::ApplyToCookiesFromDomain: m_allCookiesDomain->setChecked(true); break; case KCookieJar::ApplyToAllCookies: m_allCookies->setChecked(true); break; default: m_onlyCookies->setChecked(true); break; } if (showDetails) { slotToggleDetails(); } } KCookieWin::~KCookieWin() { } KCookieAdvice KCookieWin::advice(KCookieJar *cookiejar, const KHttpCookie &cookie) { const int result = exec(); cookiejar->setShowCookieDetails(!m_detailView->isHidden()); KCookieAdvice advice; switch (result) { case QDialog::Accepted: advice = KCookieAccept; break; case AcceptedForSession: advice = KCookieAcceptForSession; break; default: advice = KCookieReject; break; } KCookieJar::KCookieDefaultPolicy preferredPolicy = KCookieJar::ApplyToShownCookiesOnly; if (m_allCookiesDomain->isChecked()) { preferredPolicy = KCookieJar::ApplyToCookiesFromDomain; cookiejar->setDomainAdvice(cookie, advice); } else if (m_allCookies->isChecked()) { preferredPolicy = KCookieJar::ApplyToAllCookies; cookiejar->setGlobalAdvice(advice); } cookiejar->setPreferredDefaultPolicy(preferredPolicy); return advice; } KCookieDetail::KCookieDetail(const KHttpCookieList &cookieList, int cookieCount, QWidget *parent) : QGroupBox(parent) { setTitle(i18n("Cookie Details")); QGridLayout *grid = new QGridLayout(this); grid->addItem(new QSpacerItem(0, fontMetrics().lineSpacing()), 0, 0); grid->setColumnStretch(1, 3); QLabel *label = new QLabel(i18n("Name:"), this); grid->addWidget(label, 1, 0); m_name = new QLineEdit(this); m_name->setReadOnly(true); m_name->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_name, 1, 1); //Add the value label = new QLabel(i18n("Value:"), this); grid->addWidget(label, 2, 0); m_value = new QLineEdit(this); m_value->setReadOnly(true); m_value->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_value, 2, 1); label = new QLabel(i18n("Expires:"), this); grid->addWidget(label, 3, 0); m_expires = new QLineEdit(this); m_expires->setReadOnly(true); m_expires->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_expires, 3, 1); label = new QLabel(i18n("Path:"), this); grid->addWidget(label, 4, 0); m_path = new QLineEdit(this); m_path->setReadOnly(true); m_path->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_path, 4, 1); label = new QLabel(i18n("Domain:"), this); grid->addWidget(label, 5, 0); m_domain = new QLineEdit(this); m_domain->setReadOnly(true); m_domain->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_domain, 5, 1); label = new QLabel(i18n("Exposure:"), this); grid->addWidget(label, 6, 0); m_secure = new QLineEdit(this); m_secure->setReadOnly(true); m_secure->setMaximumWidth(fontMetrics().maxWidth() * 25); grid->addWidget(m_secure, 6, 1); if (cookieCount > 1) { QPushButton *btnNext = new QPushButton(i18nc("Next cookie", "&Next >>"), this); btnNext->setFixedSize(btnNext->sizeHint()); grid->addWidget(btnNext, 8, 0, 1, 2); - connect(btnNext, SIGNAL(clicked()), SLOT(slotNextCookie())); + connect(btnNext, &QAbstractButton::clicked, this, &KCookieDetail::slotNextCookie); #ifndef QT_NO_TOOLTIP btnNext->setToolTip(i18n("Show details of the next cookie")); #endif } m_cookieList = cookieList; m_cookieNumber = 0; slotNextCookie(); } KCookieDetail::~KCookieDetail() { } void KCookieDetail::slotNextCookie() { if (m_cookieNumber == m_cookieList.count() - 1) { m_cookieNumber = 0; } else { ++m_cookieNumber; } displayCookieDetails(); } void KCookieDetail::displayCookieDetails() { const KHttpCookie &cookie = m_cookieList.at(m_cookieNumber); m_name->setText(cookie.name()); m_value->setText((cookie.value())); if (cookie.domain().isEmpty()) { m_domain->setText(i18n("Not specified")); } else { m_domain->setText(cookie.domain()); } m_path->setText(cookie.path()); QDateTime cookiedate; cookiedate.setTime_t(cookie.expireDate()); if (cookie.expireDate()) { m_expires->setText(cookiedate.toString()); } else { m_expires->setText(i18n("End of Session")); } QString sec; if (cookie.isSecure()) { if (cookie.isHttpOnly()) { sec = i18n("Secure servers only"); } else { sec = i18n("Secure servers, page scripts"); } } else { if (cookie.isHttpOnly()) { sec = i18n("Servers"); } else { sec = i18n("Servers, page scripts"); } } m_secure->setText(sec); } void KCookieWin::slotSessionOnlyClicked() { done(AcceptedForSession); } void KCookieWin::slotToggleDetails() { const QString baseText = i18n("Details"); if (!m_detailView->isHidden()) { m_detailsButton->setText(baseText + " >>"); m_detailView->hide(); } else { m_detailsButton->setText(baseText + " <<"); m_detailView->show(); } } diff --git a/src/ioslaves/trash/kcmtrash.cpp b/src/ioslaves/trash/kcmtrash.cpp index 3cde6988..6f3515b5 100644 --- a/src/ioslaves/trash/kcmtrash.cpp +++ b/src/ioslaves/trash/kcmtrash.cpp @@ -1,315 +1,315 @@ /*************************************************************************** * Copyright (C) 2008 by Tobias Koenig * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmtrash.h" #include "discspaceutil.h" #include "trashimpl.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(KCMTrashConfigFactory, registerPlugin(QStringLiteral("trash"));) K_EXPORT_PLUGIN(KCMTrashConfigFactory("kcmtrash")) TrashConfigModule::TrashConfigModule(QWidget *parent, const QVariantList &) : KCModule( //KCMTrashConfigFactory::componentData(), parent), trashInitialize(false) { mTrashImpl = new TrashImpl(); mTrashImpl->init(); readConfig(); setupGui(); useTypeChanged(); - connect(mUseTimeLimit, SIGNAL(toggled(bool)), - this, SLOT(changed())); - connect(mUseTimeLimit, SIGNAL(toggled(bool)), - this, SLOT(useTypeChanged())); - connect(mDays, SIGNAL(valueChanged(int)), - this, SLOT(changed())); - connect(mUseSizeLimit, SIGNAL(toggled(bool)), - this, SLOT(changed())); - connect(mUseSizeLimit, SIGNAL(toggled(bool)), - this, SLOT(useTypeChanged())); - connect(mPercent, SIGNAL(valueChanged(double)), - this, SLOT(percentChanged(double))); - connect(mPercent, SIGNAL(valueChanged(double)), - this, SLOT(changed())); - connect(mLimitReachedAction, SIGNAL(currentIndexChanged(int)), - this, SLOT(changed())); + connect(mUseTimeLimit, &QAbstractButton::toggled, + this, QOverload<>::of(&TrashConfigModule::changed)); + connect(mUseTimeLimit, &QAbstractButton::toggled, + this, &TrashConfigModule::useTypeChanged); + connect(mDays, QOverload::of(&QSpinBox::valueChanged), + this, QOverload<>::of(&TrashConfigModule::changed)); + connect(mUseSizeLimit, &QAbstractButton::toggled, + this, QOverload<>::of(&TrashConfigModule::changed)); + connect(mUseSizeLimit, &QAbstractButton::toggled, + this, &TrashConfigModule::useTypeChanged); + connect(mPercent, QOverload::of(&QDoubleSpinBox::valueChanged), + this, &TrashConfigModule::percentChanged); + connect(mPercent, QOverload::of(&QDoubleSpinBox::valueChanged), + this, QOverload<>::of(&TrashConfigModule::changed)); + connect(mLimitReachedAction, QOverload::of(&QComboBox::currentIndexChanged), + this, QOverload<>::of(&TrashConfigModule::changed)); trashChanged(0); trashInitialize = true; } TrashConfigModule::~TrashConfigModule() { } void TrashConfigModule::save() { if (!mCurrentTrash.isEmpty()) { ConfigEntry entry; entry.useTimeLimit = mUseTimeLimit->isChecked(); entry.days = mDays->value(); entry.useSizeLimit = mUseSizeLimit->isChecked(); entry.percent = mPercent->value(), entry.actionType = mLimitReachedAction->currentIndex(); mConfigMap.insert(mCurrentTrash, entry); } writeConfig(); } void TrashConfigModule::defaults() { ConfigEntry entry; entry.useTimeLimit = false; entry.days = 7; entry.useSizeLimit = true; entry.percent = 10.0; entry.actionType = 0; mConfigMap.insert(mCurrentTrash, entry); trashInitialize = false; trashChanged(0); } void TrashConfigModule::percentChanged(double percent) { DiscSpaceUtil util(mCurrentTrash); qulonglong partitionSize = util.size(); double size = ((double)(partitionSize / 100)) * percent; KFormat format; mSizeLabel->setText(QLatin1Char('(') + format.formatByteSize(size, 2) + QLatin1Char(')')); } void TrashConfigModule::trashChanged(QListWidgetItem *item) { trashChanged(item->data(Qt::UserRole).toInt()); } void TrashConfigModule::trashChanged(int value) { const TrashImpl::TrashDirMap map = mTrashImpl->trashDirectories(); if (!mCurrentTrash.isEmpty() && trashInitialize) { ConfigEntry entry; entry.useTimeLimit = mUseTimeLimit->isChecked(); entry.days = mDays->value(); entry.useSizeLimit = mUseSizeLimit->isChecked(); entry.percent = mPercent->value(), entry.actionType = mLimitReachedAction->currentIndex(); mConfigMap.insert(mCurrentTrash, entry); } mCurrentTrash = map[ value ]; if (mConfigMap.contains(mCurrentTrash)) { const ConfigEntry entry = mConfigMap[ mCurrentTrash ]; mUseTimeLimit->setChecked(entry.useTimeLimit); mDays->setValue(entry.days); mUseSizeLimit->setChecked(entry.useSizeLimit); mPercent->setValue(entry.percent); mLimitReachedAction->setCurrentIndex(entry.actionType); } else { mUseTimeLimit->setChecked(false); mDays->setValue(7); mUseSizeLimit->setChecked(true); mPercent->setValue(10.0); mLimitReachedAction->setCurrentIndex(0); } mDays->setSuffix(i18n(" days")); // missing in Qt: plural form handling percentChanged(mPercent->value()); } void TrashConfigModule::useTypeChanged() { mDays->setEnabled(mUseTimeLimit->isChecked()); mPercent->setEnabled(mUseSizeLimit->isChecked()); mSizeLabel->setEnabled(mUseSizeLimit->isChecked()); } void TrashConfigModule::readConfig() { KConfig config(QStringLiteral("ktrashrc")); mConfigMap.clear(); const QStringList groups = config.groupList(); for (int i = 0; i < groups.count(); ++i) { if (groups[ i ].startsWith(QLatin1Char('/'))) { const KConfigGroup group = config.group(groups[ i ]); ConfigEntry entry; entry.useTimeLimit = group.readEntry("UseTimeLimit", false); entry.days = group.readEntry("Days", 7); entry.useSizeLimit = group.readEntry("UseSizeLimit", true); entry.percent = group.readEntry("Percent", 10.0); entry.actionType = group.readEntry("LimitReachedAction", 0); mConfigMap.insert(groups[ i ], entry); } } } void TrashConfigModule::writeConfig() { KConfig config(QStringLiteral("ktrashrc")); // first delete all existing groups const QStringList groups = config.groupList(); for (int i = 0; i < groups.count(); ++i) if (groups[ i ].startsWith(QLatin1Char('/'))) { config.deleteGroup(groups[ i ]); } QMapIterator it(mConfigMap); while (it.hasNext()) { it.next(); KConfigGroup group = config.group(it.key()); group.writeEntry("UseTimeLimit", it.value().useTimeLimit); group.writeEntry("Days", it.value().days); group.writeEntry("UseSizeLimit", it.value().useSizeLimit); group.writeEntry("Percent", it.value().percent); group.writeEntry("LimitReachedAction", it.value().actionType); } config.sync(); } void TrashConfigModule::setupGui() { QVBoxLayout *layout = new QVBoxLayout(this); #ifdef Q_OS_OSX QLabel *infoText = new QLabel( i18n( "KDE's wastebin is configured to use the Finder's Trash.
" ) ); infoText->setWhatsThis( i18nc( "@info:whatsthis", "Emptying KDE's wastebin will remove only KDE's trash items, while
" "emptying the Trash through the Finder will delete everything.
" "KDE's trash items will show up in a folder called KDE.trash, in the Trash can." ) ); layout->addWidget( infoText ); #endif TrashImpl::TrashDirMap map = mTrashImpl->trashDirectories(); if (map.count() != 1) { // If we have multiple trashes, we setup a widget to choose // which trash to configure QListWidget *mountPoints = new QListWidget(this); layout->addWidget(mountPoints); QMapIterator it(map); while (it.hasNext()) { it.next(); DiscSpaceUtil util(it.value()); QListWidgetItem *item = new QListWidgetItem(QIcon(QStringLiteral("folder")), util.mountPoint()); item->setData(Qt::UserRole, it.key()); mountPoints->addItem(item); } mountPoints->setCurrentRow(0); - connect(mountPoints, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), - this, SLOT(trashChanged(QListWidgetItem*))); + connect(mountPoints, &QListWidget::currentItemChanged, + this, QOverload::of(&TrashConfigModule::trashChanged)); } else { mCurrentTrash = map.value(0); } QFormLayout* formLayout = new QFormLayout(); layout->addLayout(formLayout); QHBoxLayout *daysLayout = new QHBoxLayout(); mUseTimeLimit = new QCheckBox(i18n("Delete files older than"), this); mUseTimeLimit->setWhatsThis(xi18nc("@info:whatsthis", "Check this box to allow automatic deletion of files that are older than the value specified. " "Leave this disabled to not automatically delete any items after a certain timespan")); daysLayout->addWidget(mUseTimeLimit); mDays = new QSpinBox(this); mDays->setRange(1, 365); mDays->setSingleStep(1); mDays->setSuffix(i18np(" day", " days", mDays->value())); mDays->setWhatsThis(xi18nc("@info:whatsthis", "Set the number of days that files can remain in the trash. " "Any files older than this will be automatically deleted.")); daysLayout->addWidget(mDays); daysLayout->addStretch(); formLayout->addRow(i18n("Cleanup:"), daysLayout); QHBoxLayout *maximumSizeLayout = new QHBoxLayout(); mUseSizeLimit = new QCheckBox(i18n("Limit to"), this); mUseSizeLimit->setWhatsThis(xi18nc("@info:whatsthis", "Check this box to limit the trash to the maximum amount of disk space that you specify below. " "Otherwise, it will be unlimited.")); maximumSizeLayout->addWidget(mUseSizeLimit); formLayout->addRow(i18n("Size:"), maximumSizeLayout); mPercent = new QDoubleSpinBox(this); mPercent->setRange(0.001, 100); mPercent->setDecimals(3); mPercent->setSingleStep(1); mPercent->setSuffix(QStringLiteral(" %")); mPercent->setWhatsThis(xi18nc("@info:whatsthis", "This is the maximum percent of disk space that will be used for the trash.")); maximumSizeLayout->addWidget(mPercent); mSizeLabel = new QLabel(this); mSizeLabel->setWhatsThis(xi18nc("@info:whatsthis", "This is the calculated amount of disk space that will be allowed for the trash, the maximum.")); maximumSizeLayout->addWidget(mSizeLabel); mLimitReachedAction = new QComboBox(); mLimitReachedAction->addItem(i18n("Show a Warning")); mLimitReachedAction->addItem(i18n("Delete Oldest Files From Trash")); mLimitReachedAction->addItem(i18n("Delete Biggest Files From Trash")); mLimitReachedAction->setWhatsThis(xi18nc("@info:whatsthis", "When the size limit is reached, it will prefer to delete the type of files that you specify, first. " "If this is set to warn you, it will do so instead of automatically deleting files.")); formLayout->addRow(i18n("Full Trash:"), mLimitReachedAction); layout->addStretch(); } #include "kcmtrash.moc" diff --git a/src/ioslaves/trash/kinterprocesslock.cpp b/src/ioslaves/trash/kinterprocesslock.cpp index 2726bc55..9d8f02f4 100644 --- a/src/ioslaves/trash/kinterprocesslock.cpp +++ b/src/ioslaves/trash/kinterprocesslock.cpp @@ -1,93 +1,93 @@ /* This file is part of the KDE Copyright (C) 2009 Tobias Koenig (tokoe@kde.org) This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This software 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this library; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kinterprocesslock.h" #include "kiotrashdebug.h" #include #include #include class KInterProcessLockPrivate { Q_DECLARE_PUBLIC(KInterProcessLock) KInterProcessLock * const q_ptr; public: KInterProcessLockPrivate(const QString &resource, KInterProcessLock *q) : q_ptr(q) , m_resource(resource) { m_serviceName = QStringLiteral("org.kde.private.lock-%1").arg(m_resource); q_ptr->connect(QDBusConnection::sessionBus().interface(), SIGNAL(serviceRegistered(QString)), q_ptr, SLOT(_k_serviceRegistered(QString))); } ~KInterProcessLockPrivate() { } void _k_serviceRegistered(const QString &service) { if (service == m_serviceName) { emit q_ptr->lockGranted(q_ptr); } } QString m_resource; QString m_serviceName; }; KInterProcessLock::KInterProcessLock(const QString &resource) : d_ptr(new KInterProcessLockPrivate(resource, this)) { } KInterProcessLock::~KInterProcessLock() { delete d_ptr; } QString KInterProcessLock::resource() const { return d_ptr->m_resource; } void KInterProcessLock::lock() { QDBusConnection::sessionBus().interface()->registerService(d_ptr->m_serviceName, QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } void KInterProcessLock::unlock() { QDBusConnection::sessionBus().interface()->unregisterService(d_ptr->m_serviceName); } void KInterProcessLock::waitForLockGranted() { QEventLoop loop; - connect(this, SIGNAL(lockGranted(KInterProcessLock*)), &loop, SLOT(quit())); + connect(this, &KInterProcessLock::lockGranted, &loop, &QEventLoop::quit); loop.exec(); } #include "moc_kinterprocesslock.cpp" diff --git a/src/ioslaves/trash/kio_trash.cpp b/src/ioslaves/trash/kio_trash.cpp index 1bfbf9c6..400f9900 100644 --- a/src/ioslaves/trash/kio_trash.cpp +++ b/src/ioslaves/trash/kio_trash.cpp @@ -1,673 +1,673 @@ /* 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 #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, SIGNAL(leaveModality()), - &eventLoop, SLOT(quit())); + 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; } copyOrMove(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; } copyOrMove(oldURL, newURL, (flags & KIO::Overwrite), Move); } 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; } copyOrMove(src, dest, (flags & KIO::Overwrite), Copy); } void TrashProtocol::copyOrMove(const QUrl &src, const QUrl &dest, bool overwrite, CopyOrMove action) { if (src.scheme() == QLatin1String("trash") && dest.isLocalFile()) { // 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(); } return; } else if (src.isLocalFile() && dest.scheme() == QLatin1String("trash")) { 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(); } } return; } 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()); return; } } else { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Internal error in copyOrMove, should never happen")); } } void TrashProtocol::createTopLevelDirEntry(KIO::UDSEntry &entry) { entry.clear(); entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); 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_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('/'); info.physicalPath += 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; 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('/'); infoForItem.origPath += 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()); entry.clear(); const QString fileDisplayName = (*it).fileId; if (createUDSEntry((*it).physicalPath, fileDisplayName, url.fileName(), entry, *it)) { 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::Job *job = KIO::get(fileURL, KIO::NoReload, KIO::HideProgressInfo); - connect(job, SIGNAL(data(KIO::Job*,QByteArray)), - this, SLOT(slotData(KIO::Job*,QByteArray))); - connect(job, SIGNAL(mimetype(KIO::Job*,QString)), - this, SLOT(slotMimetype(KIO::Job*,QString))); - connect(job, SIGNAL(result(KJob*)), - this, SLOT(jobFinished(KJob*))); + 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 527e4ae8..afb8309c 100644 --- a/src/ioslaves/trash/tests/testtrash.cpp +++ b/src/ioslaves/trash/tests/testtrash.cpp @@ -1,1307 +1,1307 @@ /* 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('/')) { tempDir.append('/'); } 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(QStringLiteral("umlaut"))) { QVERIFY(origPath.contains(QLatin1Char('%'))); } else { QVERIFY(!origPath.contains(QLatin1Char('%'))); } const QString date = group.readEntry("DeletionDate"); QVERIFY(!date.isEmpty()); QVERIFY(date.contains(QStringLiteral("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)) == 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.readLink() == 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), 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::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::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 + "/testfile")); QVERIFY(QFile::exists(destPath + "/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 + "/info/" + fileId + ".trashinfo"); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + "/files/" + fileId); QVERIFY(!filesItem.exists()); // Check new exists now QString newInfoFile(m_trashDir + "/info/" + destFileId + ".trashinfo"); QVERIFY(QFile::exists(newInfoFile)); QFileInfo newFilesItem(m_trashDir + "/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 + "/info/" + fileId + ".trashinfo"); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + "/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() + "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 + "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() + "testfile_restored"; copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile")); QVERIFY(QFileInfo(destPath).isFile()); QVERIFY(QFileInfo(destPath).size() == 12); } void TestTrash::moveDirectoryFromTrash() { const QString destPath = otherTmpDir() + "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 + "/info/" + fileId + ".trashinfo"); QVERIFY(!QFile::exists(infoPath)); QFileInfo files(m_trashDir + "/files/" + fileId); QVERIFY(!files.exists()); QVERIFY(QFile::exists(u.path())); } void TestTrash::moveSymlinkFromTrash() { const QString destPath = otherTmpDir() + "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 + "/info/" + fileId + ".trashinfo"); const QString filesItem(m_trashDir + "/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() + "fileFromHome"; QVERIFY(QFile::exists(destPath)); } void TestTrash::restoreFileFromSubDir() { const QString fileId = QStringLiteral("trashDirFromHome (1)/testfile"); QVERIFY(!QFile::exists(homeTmpDir() + "trashDirFromHome (1)")); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + "/info/trashDirFromHome (1).trashinfo"); const QString filesItem(m_trashDir + "/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() + "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 + "/info/" + fileId + ".trashinfo"); const QString filesItem(m_trashDir + "/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() + "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, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), - SLOT(slotEntries(KIO::Job*,KIO::UDSEntryList))); + 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, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), - SLOT(slotEntries(KIO::Job*,KIO::UDSEntryList))); + 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_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); } 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, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), - SLOT(slotEntries(KIO::Job*,KIO::UDSEntryList))); + 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); 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 b5367f69..6e91ce58 100644 --- a/src/ioslaves/trash/trashimpl.cpp +++ b/src/ioslaves/trash/trashimpl.cpp @@ -1,1432 +1,1432 @@ /* 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 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.truncate(name.length() - 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; if (srcPath == QLatin1String(".") || 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() + KIO::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.truncate(fileId.length() - 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="; info += QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1(); info += '\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, SIGNAL(leaveModality()), - &eventLoop, SLOT(quit())); + connect(this, &TrashImpl::leaveModality, + &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } QString TrashImpl::infoPath(int trashId, const QString &fileId) const { QString trashPath = trashDirectoryPath(trashId); trashPath += QLatin1String("/info/"); trashPath += fileId; trashPath += QLatin1String(".trashinfo"); return trashPath; } QString TrashImpl::filesPath(int trashId, const QString &fileId) const { QString trashPath = trashDirectoryPath(trashId); trashPath += QLatin1String("/files/"); trashPath += 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('/'); src += 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, SIGNAL(result(KJob*)), - this, SLOT(jobFinished(KJob*))); + 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('/'); src += 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, SIGNAL(result(KJob*)), - this, SLOT(jobFinished(KJob*))); + 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, SIGNAL(result(KJob*)), - this, SLOT(jobFinished(KJob*))); + connect(chmodJob, &KJob::result, + this, &TrashImpl::jobFinished); enterLoop(); } KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); - connect(job, SIGNAL(result(KJob*)), - this, SLOT(jobFinished(KJob*))); + 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"); Q_FOREACH (const QString &fileName, listDir(filesDir)) { if (fileName == QLatin1String(".") || 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 == QLatin1String(".") || fileName == QLatin1String("..")) { continue; } if (!fileName.endsWith(QLatin1String(".trashinfo"))) { qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName; continue; } fileName.truncate(fileName.length() - 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); } 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('/'); filePath += 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(); } // 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(); 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); #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 = QStringLiteral("/"); path += QString::number(trashId); path += QLatin1Char('-'); path += fileId; if (!relativePath.isEmpty()) { path += QLatin1Char('/'); path += 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"